01. 스프링 코어

01. 스프링 코어

스프링 코어 #

아래 클래스는 스프링 IoC 컨테이너 (이하 IoC 컨테이너) 가 스캐닝한다.

자바 구성 클래스 (Java Configuration Class)

  • @Configuration
  • @Bean

자바 컴포넌트 클래스 (Java Component Class)

  • @Component
  • @Controller
  • @Service
  • @Repository

@Configuration
public class SequenceGeneratorConfiguration {

    @Bean
    public SequenceGenerator sequenceGenerator() {
        SequenceGenerator seqGen = new SequenceGenerator();

        ...

        return seqGen
    }
}

스프링은 @Configuration 의 클래스를 스캐닝하면, 그 안에서 (Bean 인스턴스를 생성해 반환하는) @Bean 자바 메소드를 찾는다.
기본적으로 Bean 의 이름은 메소드 명을 따라간다. Bean 의 이름을 따로 명시하려면 @Beanname 값을 설정한다.



IoC 컨테이너 #

위의 Annotation(@Configuration, @Bean, @Component, …) 를 스캐닝하기 위해선, 우선 Ioc 컨테이너를 인스턴스화 해야 한다.
즉, IoC 컨테이너를 먼저 생성해야 한다.

스프링은 2 종류의 IoC 컨테이너(구현체, 인터페이스 등)를 제공한다.

  1. Bean Factory
  2. Application Context

(* Application Context 는 빈 팩토리를 확장하기에, 되도록 Application Context 를 사용하는 것을 권장한다.)

Application Context (인터페이스)의 구현체는 여러 개 있지만, 그 중 AnnotationConfigApplicationContext 를 권장한다.

(IoC 인터페이스)                          (IoC 구현체)                              (자바 구성 클래스)
ApplicationContext context = new AnnotationConfigApplicationContext(SequenceGeneratorConfiguration.class);



IoC 컨테이너에서 POJO 인스턴스(이하 빈) 가져오기 #

// context.getBean("빈 이름") 은 Object 형태를 return 한다.
SequenceGenerator generator = (SequenceGenerator) context.getBean("sequenceGenerator);

// context.getBean("빈 이름", "타입") 의 형태도 가능하다.
SequenceGenerator generator = context.getBean("sequenceGenerator, SequenceGenerator.class);

( * Bean 이 한 개라면 빈 이름을 생략할 수 있다. )



@Component 로 Bean 생성하기 #

public class Sequence {
    ...
}

public interface SequenceDao {
    ...
}

@Component("sequenceDao")   <-- @Component 사용, Bean ID 는 sequenceDao
public class SequenceDaoImpl implements SequenceDao {
    ...
}

@Component 에 이름을 설정하지 않으면 class 명의 camel-case 형태로 bean ID 가 설정된다.

@Component 는 Spring 이 스캐닝할 수 있도록 POJO 에 붙이는 범용 Annotation 이다.

Spring 에는 세 계층이 있다.

  1. Persistence Layer
  2. Service Layer
  3. Persentation Layer
  • @Component : 범용 Annotation
  • @Repository : Persistence(영속성) 계층을 위한 Annotation
  • @Service : Service(서비스) 계층을 위한 Annotation
  • @Controller : Presentation(표현) 계층을 위한 Annotation

POJO 의 쓰임새(목적)이 명확하지 않을 땐 @Component 를 사용해도 된다. 그러나 되도록 (목적에 맞는) 구체적인 Annotation 을 사용하는 것을 권장한다.



컨테이너 스캐닝 시, 필터(필터링) 적용 (필터로 IoC 컨테이너 초기화) #

( * 스캐닝 하는 객체가 많아 질 때, 모든 패키지를 스캐닝하면 불필요하게 속도가 느려질 수 있다. )

Spring 이 지원하는 필터 표현식 은 4 종류이다.

  1. annotation
  2. assignable (assignable_type)
  3. regex
  4. aspectj
@ComponentScan(
    includeFilters = {
        @ComponentScan.Filter(
            type = FilterType.REGEX,
            pattern = {
                "com.gg-pigs.*Dao", <-- 해당 패키지 내의 Dao 로 끝나는 클래스 스캐닝 (@Component 등의 Annotation 이 없어도 스캐닝 된다.)
                "com.gg-pigs.*Service"  <-- 해당 패키지 내의 Service 로 끝나는 클래스 스캐닝
            }
        )
    },
    excludeFilters = {
        @ComponentScan.Filter(
            type = FilterType.ANNOTATION,
            classes = {org.springframework.stereotype.Controller.class} <-- @Controller 클래스 제외
        )
    }
)



POJO 의 의존성 해결 #

  1. Setter 이용
  2. @Autowired 이용 (+ @Primary, @Qualifier)
    • @Primary : 타입이 같은 클래스(구현체)가 여럿일 때, 우선순위를 부여한다.
    • @Qualifier : 타입이 같은 클래스(구현체)가 여럿일 때, 이름으로 무엇을 사용할지 결정할 수 있다.
  3. @Resource 이용
  4. @Inject 이용
// Setter 사용

@Configuratin
public class SequenceConfiguration {

    @Bean
    public DatePrefixGenerator datePrefixGenerator() {
        DatePrefixGenerator dpg = new DatePrefixGenerator();
        dpg.setPattern("yyyyMMdd");

        return dpg;
    }

    @Bean
    public SequenceGenerator sequenceGenerator() {
        SequenceGenrator sequence = new SequenceGenrator();
        ...
        sequence.setPrefixGenerator(datePrefixGenerator());  <-- 위의 datePrefixGenerator 메소드 (Bean 이름)

        return sequence;
    }
}
// Autowired 사용

@Service
public class SequenceService {

    @Autowired
    private SequenceDao sequenceDao;

    public void setSequenceDao(SequenceDao sequenceDao) {   <-- @Autowired 를 위해, 이 setter 가 필요한 것으로 알고 있음
        this.sequenceDao = sequenceDao;
    }

    ...
}
// Autowired 사용

@Service
public class SequenceService {

    @Autowired  <-- 위와 동일한 예제, Setter 메소드에 적용했을 경우이다.
    public void setSequenceDao(SequenceDao sequenceDao) {   <-- @Autowired 를 위해, 이 setter 가 필요한 것으로 알고 있음
        this.sequenceDao = sequenceDao;
    }

    ...
}

// 생성자의 경우에도 가능하다.
// Spring 4.3 이상의 버전부터는 생성자가 하나뿐인 클래스에서는 @Autowired 를 생략해도 된다. (기본으로 적용된다.)
// Resource 사용

public calss SequeceGenerator {
    
    @Resource
    private PrefixGenerator prefixGenerator;
}
// Inject 사용

public class SequenceGenerator {

    @Inject
    pirvate PrefixGenerator prefixGenerator;
}



POJO Scope 지정 #

  • singleton : IoC 컨테이너 당 Bean 인스턴스 1 개 생성
  • prototype : 요청할 때마다 Bean 인스턴스 새롭게 생성
  • request : HTTP 요청 당 1 개의 Bean 인스턴스 생성 (WebApplicationContext 만 해당)
  • session : HTTP 세션 당 1 개의 Bean 인스턴스 생성 (WebApplicationContext 만 해당)
  • globalSession : 전역 HTTP 세션 당 1개의 Bean 인스턴스 생성 (PortalApplicationContext 만 해당 (?))



외부 리소스(text, xml, property, image) 사용 #

  • @PropertySource : .properties 파일 로드 가능
  • Resource 인터테이스 (+ @Value) : 리소스 로드 가능
// discounts.properties

specialcustomer.discount=0.1
endofyear.discount=0.2
...


@Configuration
@PropertySource("classpath:discounts.peroperties")
@ComponentScan("~")
public class ShopConfiguration {

    @Value("${endofyear.discount:0}")
    private double specialEndofyearDiscountField;

    @Bean
    public static PropertySourcePlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }

    @Bean
    public Product dvdrw() {
        ~~
    }
}
// POJO class
public class BannerLoader {
    
    private Resource banner;

    public void setBanner(Resource banner) {
        this.banner = banner;
    }

    @PostConstruct  <-- Bean 생성 후 실행된다.
    public void showBanner() throws IOException {
        Files.lines(Paths.get(banner.getURI()), Charset.forName("UTF-8)).forEachOrdered(System.out::println);
    }
}

// Configuration class
@Configuration
@PropertySource("classpath:discounts.properties")
@ComponentScan("~")
public class ShopConfiguration {

    @Value("classpath:banner.txt")  <-- classpath 에 위치한 banner.txt 파일
    private Resource banner;

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        ~~
    }

    @Bean
    public BannerLoader bannerLoader() {
        BannerLoader bl = new BannerLoader();
        bl.setBanner(banner);
        return bl;
    }
}
public class Main {
    public static void main(String[] args) throws Exception {
        ...

        Resource resource = new ClassPathResource("discounts.properties");  <-- .properties 파일 읽어들인 후, Resource 객체로 캐스팅
        Properties props = PropertiesLoaderUtils.loadProperties(resource);  <-- Resource 객체를 Properteis 객체로 변환

        System.out.println(props);  <-- Properties 내용 출력
    }
}
  • ClassPathResource : classpath 내부의 리소스 로딩
  • FileSystemResource : 외부 파일시스템의 리소스 로딩
  • URIResource : URL 상의 외부 리소스 로딩



Locale 별 다국어 지원 (with Properties) #

MessageSource 인터페이스

  • Resource bundle 메세지를 처리하는 메서드가 몇 가지 정의되어 있다.
  • 가장 많이 사용되는 구현체 : ResourceBundleMessageSource, 로케일 별로 분리된 Resource bundle message 를 해석
@Configuration
public class ShopConfiguration {

    // Bean 이름(메소드명)은 반드시 'messageSource' 로 작성해야, ApplicationContext 가 알아서 감지한다.
    @Bean
    public ReloadableResourceBundleMessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasenames("classpath:messages");
        messageSource.setCacheSeconds(1);

        return messageSource;
    }
}

‘영어’ 의 로케일을 사용할 때, (자동으로) 아래와 같은 순서를 탐색한다. (?)

  1. messages_en_US.properties
  2. messages_en.properties
  3. messages.properties



POJO 초기화/폐기 커스터마이징 (with Annotation) #

특정한 POJO 는 사용하기 전, 특정한 초기화 작업을 거친다. 예를 들어 File 열기, DB 연결, 메모리 할당 등의 선행 작업이 필요한 경우가 있을 수 있다. (대개 이런 경우 폐기 할 때에도 특정한 작업을 해주어야 한다.)

  1. @Bean 의 속성에 initMethod, destroyMethod 속성을 사용할 수 있다. (초기화, 폐기 콜백 메서드로 인지한다.)
  2. @PostConstruct, @PreDestroy 어노테이션을 사용할 수 있다.
  3. 이 외에 @Lazy, @DependsOn 의 어노테이션 등이 있다.
// 1) @Bean 속성의 initMethod, destroyMethod 속성을 사용한다.

public class Cashier {
    ...

    public void openFile() throws IOException {
        ...
    }

    ...

    public void closeFile() throws IOException {
        ...
    }
}

@Configuration
public class ShopConfiguration {

    @Bean(initMethod = "openFile", destroyMethod = "closeFile") <-- 해당하는 POJO 클래스의 메소드이다.
    public Cashier cashier() {
        ...
    }
}
// 2) @PostConstruct, @PreDestroy 어노테이션을 사용한다.

@Component <-- 1) 번 예제와의 차이점이다. 해당 클래스는 직접 @Component 를 붙인 클래스이다.
public class Cashier {

    ...

    @PostConstruct
    public void openFile() throws IOException {
        ...
    }

    ...

    @PreDestroy
    public void closeFile() throws IOExcetpion {
        ...
    }
}

기본적으로 스프링은 모든 POJO 를 eager 초기화한다. @Lazy 를 붙이면 lazy 초기화로 변경할 수 있다.

lazy 초기화를 하면, 애플리케이션이 요구하거나 다른 POJO 가 참조하여 사용되기 전까지 초기화되지 않는다.

@Component
@Scope("prototype")
@Lazy
public class ShoppingCart {

    private List<Product> items = new ArrayList<>();

    ...
}

@DependsOn 을 사용하여 초기화 순서를 지정할 수 있다. 예를 들어 어떤 POJO 가 다른 POJO 보다 먼저 초기화되도록 지정할 수 있다.

@Configuration
public class SequenceConfiguration {

    @Bean
    @DependsOn("datePrefixGenerator")
    public SequenceGenerator sequenceGenerator() {
        // datePrefixGenerator 를 DependsOn 한다고 해서 여기에 datePrefixGenerator 빈을 사용할 필요는 없다.
        SequenceGenerator s = new SequenceGenerator();
        return s;
    }
}



후처리기를 이용하여 POJO 검증/수정하기 #

Bean post-processor(Bean 후처리기) 를 사용하여 초기화 콜백 메소드(initMethod, @PostConstruct) 전후에 원하는 로직을 추가할 수 있다.

Bean 후처리기의 주요한 특징은, IoC 컨테이너 내부의 모든 Bean 인스턴스를 대상으로 한다는 점이다.

보통 Bean property 가 올바른지 검사하거나, 어떤 기준에 따라 Bean property 를 변경하거나, 전체 Bean 인스턴스를 상대로 어떤 작업을 수행하기 위한 목적으로 사용된다.

Bean 후처리기는 BeanPostProcessor 인터페이스를 구현한 객체이다. 스프링은 이 인터페이스를 구현한 Bean 을 발견하면 자신이 관리하는 모든 Bean 인스턴스에 postProcessBeforeInitialization(), postProcessAfterInitalization() 메소드를 적용한다.

@Component
public class AuditCheckBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeanException {
        ...

        return bean; <-- 인자로 받은 원본 bean 을 무조건 반환해야 한다.
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeanException {

        ...

        // 만약 특정한 Bean 만 처리하고 싶다면 아래와 같은 로직을 사용할 수 있을 것이다.
        if (bean instanceof 특정클래스) {
            ~~~
        }

        return bean; <-- 인자로 받은 원본 bean 을 무조건 반환해야 한다.
    }
}

@Required 로 속성이 설정되었는지 검사할 수 있다.

설정 여부만 검사할 수 있다. null인지, 다른 값인지는 검사하지 않는다.

스프링의 Bean 후처리기 RequiredAnnotationBeanPostProcessor@Required 를 붙인 값들이 설정되었는지 체크한다.

값이 설정되어 있지 않다면, BeanInitializationException 예외를 던진다.

public class SequenceGenerator {

    private PrefixGenerator prefixGenerator;
    private String suffix;

    ...

    @Required
    public void setPrefixGenerator(PrefixGenerator prefixGenerator) {
        this.prefixGenerator = prefixGenerator;
    }

    @Required
    public void setSuffix(String suffix) {
        this.suffix = suffix;
    }
}



팩토리(정적 메소드, 인스턴스 메소드, Spring FactoryBean)로 POJO 생성 #

  1. @Bean 메소드 내부에서 팩토리 메소드를 호출해서 POJO 를 생성할 수 있다.

  2. Spring 은 FactoryBean 인터페이스를 상속한 템플릿 클래스 AbstractFactoryBean 을 제공한다


정적 팩토리 메소드로 POJO 생성

일반 자바 구문을 활용한 케이스이다.

public class ProductCreator {

    // 정적 팩토리 메소드
    public static Product createProduct(String productId) {
        if("aaa".equals(productId)) {
            return new Battery("AAA", 2.5);
        }
        else if("bbb".equals(productId)) {
            return new Disc("CD-RW", 1.5);
        }
        ...
    }

    throw new IllegalArgumentException("Unknown product");
}


@Configuration
public class ShopConfiguration {

    @Bean
    public Product aaa() {
        return ProductCreator.createProduct("aaa"); <-- 정적 팩토리 메소드 사용
    }

    ...

}

인스턴스 팩토리 메소드로 POJO 생성

public class ProductCreator {

    // Map 을 사용하여 팩토리 메소드를 구현할 수도 있다.
    // private Map<String, Product> products;

    public Product createProduct(String productId) {
        if("aaa".equals(productId)) {
            return new Battery("AAA", 2.5);
        }
        else if("bbb".equals(productId)) {
            return new Disc("CD-RW", 1.5);
        }
        ...
    }
}

@Configuration
public class ShopConfiguration {

    @Bean
    public ProductCreator productCreatorFactory() {
        ProductCreator factory = new ProductCreator();  <-- 인스턴스 팩토리 메소드를 사용한다.

        ...

        return factory;
    }

    @Bean
    public Product aaa() {
        return productCreatorFactory().createProduct("aaa");    <-- 위에서 정의된 Bean 을 사용한다.
    }

    ...
}

스프링 팩토리 빈으로 POJO 생성하기

(Custom)FactoryBeanAbstractFactoryBean 제네릭 클래스를 상속한다.

  • createInstance() 메소드를 오버라이드 하여 Bean 인스턴스를 생성한다.
  • getObject() 메소드를 오버라이드 하여 자동으로 주입될 수 있도록 한다. (?)
  • 위에서 언급된 팩토리 메소드들과 유사하지만, Bean 생성 도중 IoC 컨테이너가 식별할 수 있는 스프링의 전용 Bean 이다. (?)
public class DiscountFactoryBean extend AbstractFactoryBean<Product> {

    private Product product;
    private double discount;

    // setter
    ...

    @Override
    public Class<?> getObjectType() {
        return product.getClass();
    }

    @Override
    protected Product createInstance() throws Exception {
        product.setPrice(product.getPrice() * (1 - discount));

        return product;
    }
}

@Configuration
public class ShopConfiguration {

    @Bean
    public Battery aaa() {
        Battery aaa = new Battery(~);

        return aaa;
    }

    ...

    @Bean
    public DiscountFactoryBean discountFactoryBeanAAA() {
        DiscountFactoryBean factory = new DiscountFactoryBean();
        factory.setProduct(aaa());  <-- 위에서 정의된 Bean (aaa)
        factory.setDiscount(0.2);
        return factory;
    }

    ...

}



Spring environment, profile 마다 다른 POJO 로딩 #

(예를 들어, 동일한 POJO 인스턴스를 개발/테스트/운영 환경별로 초기값을 달리하고 싶을 때)

  1. 자바 Configuration Class 를 여러 개 생성하고, 각 클래스마다 Bean 을 정리한다.
  2. 의도를 잘 표현할 수 있게 Profile 을 명명한다.
  3. ApplicationContext 에서 해당하는 Profile 을 설정하여, 해당 POJO 들을 갖고온다.

@Profile 활용

@Configuration
@Profile("global")
public class ShopConfigurationGlobal {

    ...

}

@Configuration
@Profile({"summer", "winter"})
public class ShopConfigurationSumWin {

    ...

}

자바 Configuration Class 에 속한 Bean 들은 해당 profile 에 편입된다.

Profile 로드하기

  • Profile을 Application에 로드하기 위해서 먼저 해당 profile 을 활성화(activation)한다.
  • 프로파일 여러 개를 한 번에 로드하는 것도 가능하다.
  • Java Runtime Flag, WAR 파일 초기화 매개변수를 지정해 로드할 수 있다.
  • 기본 프로파일이 있어야 하며, 스프링은 활성화시킬 프로파일이 없다면 기본 프로파일을 찾아 적용시킨다.
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.getEnvironment().setActiveProfiles("global", "winter"); <-- 프로파일을 활성화한다.
context.scan(~);
countext.refresh();
-Dspring.profiles.active=global,winter  <-- 프로파일을 활성화한다.



POJO에서 IoC 컨테이너의 리소스 확인 #

* 컴포넌트(POJO) -> IoC 컨테이너 에 의존관계를 맺는(직접 접근하는(?)) 설계는 바람직하지 않다. 그러나 때로는 Bean(POJO)에서 컨테이너의 리소스를 인지해야 할 수 있다.

Bean 이 IoC 컨테이너 리소스를 인지하려면 Aware 인터페이스를 구현해야 한다.

자주 쓰이는 Aware 인터페이스 종류

  • BeanNameAware : IoC 컨테이너에 구성한 인스턴스의 Bean 이름
  • BeanFactoryAware : 현재의 BeanFactory 를 호출하는 데 쓰인다.
  • ApplicationContextAware : 현재의 ApplicationContext 를 호출하는 데 쓰인다.
  • MessageSourceAware : MessageSource, 텍스트 메세지를 해석하는 데 쓰인다.
  • ApplicationEventPublisherAware : ApplicationEvent(Publisher) 를 발행하는 데 쓰인다.
  • ResourceLoaderAware : ResourceLoader 를 로드하는 데 쓰인다.
  • EnvironmentAware : ApplicationContext 인터페이스에 엮인 Environment 인스턴스를 인지하기 위해 쓰인다.

* ApplicationContextMessageSource, ApplicationEventPublisher, ResourceLoader 를 상속한 인터페이스이다. 따라서 ApplicationContext 만 인지하면 이들(MessageSource, ApplicationEventPublisher, ResourceLoader)도 액세스할 수 있다. 그러나 요건을 충족하는, 최소한의 범위 내에서 Aware 인터페이스를 사용하는 것이 바람직하다.

* 현재 Spring 최신 버전에서는 Aware 를 사용하지 않아도 된다. @Autowired 등을 통해 ApplicationContext 를 주입받을 수 있기 때문이다.

* Aware 인터페이스를 구현한 클래스는 스프링과 엮이게 되므로 IoC 컨테이너 외부에서는 제대로 작동하지 않는다.


Aware 호출 과정(순서)

  1. 생성자, 팩토리 메소드를 호출하여 Bean 인스턴스를 생성한다.
  2. Bean 의 속성들에 값을 주입한다.
  3. Aware 인터페이스에 정의한 setter 메소드를 호출한다.
  4. Bean 인스턴스들을 각 Bean 후처리기(postProcessBeforeInitialization()) 메소드로 넘겨 초기화 콜백 메소드를 호출한다.
  5. Bean 인스턴스들을 각 Bean 후처리기(postProcessAfterInitialization()) 메소드로 넘긴다.
  6. Bean 을 사용한다.
  7. 컨테이너가 종료되면 폐기 콜백 메소드를 호출한다.

public class Cashier implements BeanNameAware {

    private String fileName;    <-- Aware 인터페이스의 setter 메소드가 실행되면서 해당 속성 값이 채워진다.

    @Override
    public void setBeanName(String beanName) {
        this.fileName = beanName;
    }
}

@Configuration
...

    @Bean(~)
    public Cashier cashier() {
        final String path = System.getProperty("java.io.tmpdir") + "cashier";
        Cashier cashier = new Cashier();
        // cashier.setFileName("filename"); <-- Aware setter 메소드를 통해 채워졌다.
        cashier.setPath(path);

        return cashier;
    }

(* 제대로 이해한것인지 모르겠다.)



AOP (with Annotation) #

Aspect 를 정의하려면, 클래스에 @Aspect 를 붙이고 메소드별로 Advice 를 만든다.

Advice Annotation 은 다음의 종류가 있다.

  • @Before
  • @After
  • @AfterReturning
  • @AfterThrowing
  • @Around

IoC 컨테이너에서 Aspect Annotation 기능을 활성화하려면 Configuration Class 중 하나에 @EnableAspectJAutoProxy 를 붙인다.