[Spring 5 Recipes] Spring 5 Recipes 2장 정리 #2
POJO 구성 방식
1. 자바로 POJO 구성
@Configuration, @Bean 을 붙인 자바 클래스를 만들거나 @Component, @Repository, @Service, Controller 를 붙인 자바 컴포넌트를 구성함으로써 POJO 클래스를 설계한다. IoC 컨테이너는 어노테이션이 붙은 자바 클래스를 탐색해 애플리케이션의 일부인 것처럼 POJO 인스턴스/빈을 구성한다.
@Configuration
public class ConfigurationClass {
@Bean
public BeanClass beanClass() {
BeanClass bean = new BeanClass();
bean.setName("Hello");
bean.setNick("World");
return bean;
}
}
@Configuration 어노테이션이 붙은 구성 클래스에서 @Bean 메소드를 통해 빈을 생성한다. 그 후 어노테이션을 붙인 자바 클래스를 스캐닝하기 위해 IoC 컨테이너를 인스턴스화 해야 한다. 스프링에서는 기본 구현체인 BeanFactory 와 이와 호환이 가능한 고급 구현체인 ApplicationContext, 두 가지 IoC 컨테이너를 제공한다.
ApplicationContext context = new AnnotationConfigApplicationContext(ConfigurationClass.Class);
구성 클래스에 선언된 빈을 BeanFactory / ApplicationContext 에서 가져오려면 유일한 빈 이름을 getBean() 메소드의 인자로 호출한다. getBean() 메소드는 Object 타입을 반환하므로 실제 타입에 맞게 캐스팅하거나, 메소드의 인자로 빈 클래스 타입을 명시할 수 있다. 빈 이름은 기본적으로는 메소드명을 사용하고, @Bean(name = "") 과 같이 이름을 명시한 경우는 어노테이션에 명시한 이름을 사용한다. 해당 클래스의 빈이 하나뿐일 때는 빈 이름을 생략할 수도 있다.
BeanClass bean = (BeanClass) context.getBean("beanClass");
// 클래스를 명시함으로써 캐스팅을 하지 않을 수 있음
// bean = context.getBean("beanClass", BeanClass.class);
// 빈 클래스가 하나뿐일 때는 빈 이름을 명시하지 않아도 됨
// bean = context.getBean(BeanClass.class);
스프링에는 Persistence, Service, Presentation 이렇게 세 계층이 존재하는데 이를 가리키는 어노테이션이 각각 @Repository, @Service, @Controller 이다. 이 외 범용으로 사용할 수 있는 어노테이션으로 @Component 가 있는데, 쓰임새가 명확하지 않을 때는 @Component 를 사용할 수 있지만 구체적으로 명시하는 편이 용도에 맞는 혜택을 누릴 수 있는 장점이 있다고 한다. (@Repository 는 발생한 Exception 을 DataAccessException 으로 감싸 던지는 등 디버깅 시 유리한 점이 있다.)
2. 생성자 호출을 통한 POJO 생성
POJO 클래스에 생성자를 하나 이상 정의한 뒤, 구성 클래스에서 IoC 컨테이너가 사용할 POJO 인스턴스값을 생성자로 설정한다. 그 뒤 IoC 컨테이너를 인스턴스화해 어노테이션을 붙인 자바 클래스를 스캐닝하도록 한다. 그리하면 POJO 인스턴스 / 빈을 애플리케이션 일부처럼 접근할 수 있게 된다.
// POJO 클래스 선언 (생성자 정의)
public class Product {
private String name;
private double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
}
// 구성 클래스 정의
public class ShopConfiguration {
@Bean
public Product aaa() {
Product p = new Product("AAA", 2.5);
return p;
}
}
3. POJO 레퍼런스와 자동 연결을 통한 상호 작용
POJO / Bean 인스턴스들 사이의 참조 관계는 위와 같이 자바 코드로도 연결을 맺어줄 수 있다. 스프링에서는 필드, Setter 메소드, 생성자, 또는 아무 메소드에 @Autowired 를 붙임으로써 POJO 레퍼런스를 자동으로 연결할 수 있다.
@Service
public class TemplateService {
@Autowired
private BeanDao beanDao;
// Getter, Setter
}
서비스 객체를 생성하는 서비스 클래스틑 실제로 자주 쓰이는 Best Practice 로, DAO 에 대한 직접 호출을 하지 않고 일종의 관문을 두는 것이다. 서비스 객체는 내부적으로 DAO 와 연동해 요청받은 작업을 처리하게 된다.
위에서는 필드에 @Autowired 를 적용해 의존성을 주입했지만 다음과 같이 생성자나 메소드와도 자동으로 연결할 수 있다. 또한 컬렉션이나 배열에도 @Autowired 를 적용할 경우 매칭되는 빈을 모두 찾아 자동으로 연결해 준다.
public class StringGenerator {
// 필드 주입
@Autowired
private PrefixGenerator prefixGenerator;
// 배열형, 컬렉션에 Autowired 적용
@Autowired
private PrefixGenerator[] prefixGenerators;
// 생성자 주입
@Autowired
public StringGenerator(PrefixGenerator prefixGenerator) {
this.prefixGenerator = prefixGenerator;
}
// Setter 주입
@Autowired
public void setPrefixGenerator(PrefixGenerator prefixGenerator) {
this.prefixGenerator = prefixGenerator;
}
}
스프링은 기본적으로 @Autowired 를 붙인 프로퍼티에 해당하는 빈을 찾지 못하면 예외를 던진다. 선택적으로 프로퍼티를 적용하고자 할 경우에는 @Autowired(required=false) 와 같이 required 속성값을 지정함으로써 빈을 찾지 못하더라도 그냥 지나치도록 할 수 있다.
@Autowired 는 타입을 기준으로 자동으로 빈을 연결하는데, 컨테이너에 호환되는 타입이 여러개 존재할 경우 제대로 연결되지 않을 수 있다. 이러한 문제를 해결하기 위해 @Primary, @Qualifier 를 적용할 수 있다.
// @Primary 를 활용
@Component
@Primary
public class PrefixGenerator1 implements PrefixGenerator {
// Methods..
}
// @Qualifier 활용
public class StringGenerator {
@Autowired
@Qualifier("prefixGenerator1")
private PrefixGenerator prefixGenerator;
}
애플리케이션의 규모가 커질수록 POJO 설정들을 하나의 구성 클래스에 담기 어려워지는데, 보통 POJO 의 기능에 따라 여러 구성 클래스로 나누어 관리한다. 그런데 구성 클래스가 여러 개 공존할 경우 상이한 클래스에 정의된 POJO 를 자동으로 연결하거나 참조하는 것이 쉽지 않다.
한 가지 방법은, 구성 클래스가 위치한 경로마다 애플리케이션 컨텍스트를 초기화하는 방법이다. 각 자바 구성 클래스에 선언된 POJO 를 컨텍스트와 레퍼런스로 읽으면 POJO 간 자동으로 연결이 가능하다.
ApplicationContext context =
new AnnotationConfigApplicationContext(Configuration1.class, Configuration2.class);
혹은 @Import 로 구성 파일을 나누는 방법도 존재한다.
@Configuration
public class Configuration1 {
@Bean
private PrefixGenerator prefixGenerator() {
// ...
}
}
@Configuration
@Import(Configuration1.class)
public class Configuration2 {
@Value("#{prefixGenerator}")
private PrefixGenerator prefixGenerator;
@Bean
private StringGenerator stringGenerator() {
// ...
}
}
StringGenerator 빈을 설정하기 위해 prefixGenerator 빈을 설정해야 하는데, 이 구성 클래스 내에 존재하지 않고 다른 자바 구성 클래스에 정의되어 있는 경우에 @Import 를 사용한다. @Import 를 붙이면 해당 클래스에 정의한 POJO 를 모두 현재 구성 클래스의 스코프로 가져올 수 있는데, 그런 뒤 위와 같이 @Value 와 SpEL (Spring Expression Language) 를 사용해 외부 구성 클래스에 선언된 빈을 필드에 주입할 수 있다.
4. @Resource 와 @Inject 를 사용한 POJO 자동 연결
@Resource 와 @Inject 는 자바 표준(JSR) 에 근거한 해법으로, @Autowired 와 거의 유사하게 사용이 가능하다.
@Resource 는 우선적으로 @Autowired 와 같이 타입을 기준으로 POJO 를 찾아 자동으로 연결한다. 하지만 타입이 같은 POJO 가 여럿일 때 @Autowired 는 가리키는 대상이 모호해져 @Qualifier 를 추가적으로 사용해야 하지만, @Resource 는 기능상 @Qualifier 와 @Autowired 를 합한 것과 같아 자체적으로 대상을 명확하게 지정할 수 있다.
@Inspect 는 @Autowired, @Resource 와 같이 타입으로 우선 POJO 를 찾지만 타입이 같은 POJO 가 여럿일 경우에는 다음과 같이 커스텀 어노테이션을 작성해 적용해야 한다.
@Qualifier
@Target({ElementType.TYPE, ...})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomAnnotation {
}
이 커스텀 어노테이션에 적용된 @Qualifier 는 스프링에서 사용되는 @Qualifier 가 아닌, javax.inject 패키지에 속한 어노테이션이다. 커스텀 어노테이션을 위와 같이 작성한 뒤, 빈 인스턴스를 생성하는 POJO 클래스에 다음과 같이 어노테이션을 붙인 뒤, @Inject 어노테이션과 같이 커스텀 어노테이션을 사용하면 더이상 모호해지는 상황이 발생하지 않는다.
@CustomAnnotation
public class PojoClass {
}
public class ServiceClass {
@Inject @CustomAnnotation
private PojoClass pojoClass;
}
@Autowired, @Resource, @Inject 중 어떤 것을 사용하더라도 결과는 같다. 차이점은 이름을 기준으로 할 경우에 구문이 가장 단순한 @Resource 가 낫고, 타입을 기준으로 할 경우 셋 중 어느 것을 골라도 간편하게 사용이 가능하다.
* 개인적인 생각을 덧붙이자면, 어떤 어노테이션을 쓰느냐는 개인의 마음이라고 볼 수 있겠지만 프로젝트에서 사용함에 있어 하나로 통일해서 사용해야 할 필요는 있을 것 같다. 기능적으로 여러 개를 혼용해 써도 문제가 될 것 같지는 않지만, 추후에 코드를 봤을 경우에 어노테이션이 뒤죽박죽 사용되어 있다면 불필요한 혼란을 가져올 수 있을 것 같다.
#Reference.