POJO 구성 방식
8. 어노테이션을 이용한 POJO 초기화 / 폐기
어떤 POJO 는 사용하기 전 특정한 초기화 작업, 사용한 후 폐기 작업이 필요하다. 예를 들어 파일을 열거나, 네트워크 / DB 에 접속하거나, 메모리를 할당하는 등 선행 작업이 필요한 경우이다. IoC 컨테이너에서 빈을 초기화하거나 폐기하는 로직을 커스터마이징할 수 있다.
자바 구성 클래스의 @Bean 정의 시 initMethod, destroyMethod 속성을 설정함으로써 스프링이 각각을 초기화 / 폐기 콜백 메소드로 인지한다. 혹은 POJO 클래스의 메소드에 @PostConstruct / @PreDestroy 를 붙이는 방법이 있다. 또 @Lazy 를 붙임으로써 Lazy Initialization (주어진 시점까지 빈 생성을 미뤄둠) 을 할 수도 있고, @DependsOn 으로 빈을 생성하기 전에 다른 빈을 먼저 생성하도록 강제할 수 있다.
다음과 같이 작업을 수행하고 난 기록을 파일로 기록하는 POJO 클래스를 정의한다.
public class Cashier {
private String fileName;
private String path;
private BufferedWriter writer;
// Setter
public void openFile() throws IOException {
File targetDir = new File(path);
if (!targetDir.exists()) {
targetDir.mkdir();
}
File checkoutFile = new File(path, fileName + ".txt");
if (!checkoutFile.exists()) {
checkoutFile.createNewFile();
}
writer = new BufferedWirter(new OutputStreamWriter(
new FileOutputStream(checkoutFile, true)));
}
// Writer Method
public void closeFile() throws IOException {
writer.close();
}
}
openFile() 메소드는 데이터를 저장할 대상 디렉토리와 파일이 존재하는 지 확인한 뒤, 주어진 시스템 경로에 있는 텍스트 파일을 열어 writer 필드에 할당한다. 기록을 모두 마친 뒤 closeFile() 메소드를 통해 파일을 닫고 시스템 리소스를 반납한다. (기록을 하기 위한 메소드는 생략되었다.)
Cashier 빈 생성 이전에 openFile() 메소드를, 폐기 직전에 closeFile() 메소드를 각각 실행하도록 자바 구성 클래스에 빈 정의부를 다음과 같이 설정한다.
@Configuration
public class ShopConfiguration {
@Bean(initMethod = "openFile", destroyMethod = "closeFile")
public Cashier cashier() {
Cashier c1 = new Cashier();
// Call Setter ...
return c1;
}
}
이로써 Cashier 인스턴스를 생성하기 전에 openFile() 메소드를 먼저 트리거한다. 그리고 빈을 폐기할 때 closeFile() 메소드를 자동 실행하게 된다. 이러한 동작은 다음과 같이 POJO 클래스를 작성해도 똑같이 동작한다.
@Component
public class Cashier {
// Fields and Methods ...
@PostConstruct
public void openFile() throws IOException {
// ... Body
}
@PreDestroy
public void closeFile() {
// ... Body
}
}
기본적으로 스프링은 모든 POJO 를 미리 초기화한다. 하지만 환경에 따라 빈을 처음으로 요청하기 전까지 초기화 과정을 미루는 게 나을 때도 있다. 이렇듯 나중에 초기화하는 방식을 Lazy Initialization 이라 한다.
느긋한 초기화는 시동 시점에 리소스를 집중 소모하지 않아도 되므로 전체 시스템 리소스를 절약할 수 있다. 특히 무거운 작업을 처리하는 POJO 는 느긋한 초기화가 더 어울리는 경우가 있다. 사용 방법은 다음과 같이 POJO 클래스에 @Lazy 를 붙임으로써 느긋한 초기화를 적용할 수 있다. @Lazy 덕분에 다음 클래스는 애플리케이션이 요구하거나 다른 POJO 가 참조하기 전까지는 초기화되지 않는다.
@Component
@Scope("prototype"
@Lazy
public class Product {
// ... Class Body
}
POJO 가 늘어나면 그에 따라 초기화 횟수도 증가하고, 여러 자바 구성 클래스에 분산 선언된 많은 POJO 가 서로를 참조하게 되면 Race Condition 이 발생하기 쉽다. B, F 라는 빈의 로직이 C 라는 빈에서 필요하다고 할 때, 아직 스프링이 B, F 빈을 초기화하지 않았는데 C 빈이 먼저 초기화되면 원인불명의 에러가 발생하게 된다.
@DependsOn 어노테이션은 어떤 POJO 가 다른 POJO 보다 먼저 초기화되도록 강제하며 설사 그 과정에서 에러가 발생한다 하더라도 헤아리기 쉬운 메시지를 보여준다. 또 @DependsOn 은 빈을 초기화하는 순서를 보장한다.
@Configuration
public class SequenceConfiguration {
@Bean
@DependsOn("prefixGenerator")
public SequenceGenerator sequenceGenerator() {
SequenceGenerator generator = new SequenceGenerator();
return generator;
}
}
@DependsOn("prefixGenerator") 를 적용했기 때문에 PrefixGenerator 빈은 항상 SequenceGenerator 보다 먼저 생성된다. @DependsOn 의 속성값은 {} 로 감싼 리스트 형태로 여러 개의 의존성을 설정할 수도 있다.
9. PostProcessor 를 통한 POJO 검증 / 수정
빈 후처리기를 이용하면 초기화 콜백 메소드 전후에 원하는 로직을 빈에 적용할 수 있다. 빈 후처리기의 주요한 특징은 IoC 컨테이너 내부의 모든 빈 인스턴스를 대상으로 한다는 점이다. 보통 빈 후처리기는 빈 프로퍼티가 올바른지 체크하거나 어떤 기준에 따라 빈 프로퍼티를 변경 또는 전체 빈 인스턴스를 상대로 어떤 작업을 수행하는 용도로 사용된다.
@Required 는 스프링에 내장된 후처리기 RequiredAnnotationBeanPostProcessor 가 지원하는 어노테이션이다. 이 후처리기는 @Required 를 붙인 모든 빈 프로퍼티가 설정되었는지 확인한다.
빈 후처리기는 BeanPostProcessor 인터페이스를 구현한 객체이다. 이 인터페이스를 구현한 빈을 발견하면 스프링은 자신이 관장하는 모든 빈 인스턴스에 아래의 두 메소드를 적용한다. 따라서 빈 상태를 조사, 수정, 확인하는 등의 어떠한 로직도 이 메소드에 넣을 수 있다. 이 메소드들은 하는 동작이 없어도 반드시 원본 빈 인스턴스를 반환해야 한다.
@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
@Override
publi Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
}
IoC 컨테이너는 자신이 생성한 빈 인스턴스를 모두 하나씩 빈 후처리기에 넘긴다. 만일 특정 타입의 빈만 후처리기를 적용하려면 인스턴스 타입을 체크하는 필터를 이용해 원하는 빈에만 후처리 로직을 적용할 수 있다. 예를 들어 Product 형 빈 인스턴스에만 빈 후처리기를 적용하는 예제는 다음과 같이 작성할 수 있다.
@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof Product) {
// ... Some Code
}
return bean;
}
@Override
publi Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof Product) {
// ... Some Code
}
return bean;
}
}
위의 두 메소드는 처리한 빈 인스턴스를 반드시 반환해야 한다. 이를 바꿔 말하면, 원본 빈 인스턴스를 다른 인스턴스로 바꿔치기할 수도 있다는 뜻이다.
10. 정적 메소드, 인스턴스 메소드, 스프링 FactoryBean 으로 POJO 생성
자바 구성 클래스의 @Bean 메소드는 정적 팩토리를 호출하거나 인스턴스 팩토리 메소드를 호출해 POJO 를 생성할 수 있다. 다음 ProductCreator 클래스에서 정적 팩토리 메소드 createProduct 는 productId 에 해당하는 상품 객체를 생성한다. 주어진 productId 에 따라 인스턴스화할 실제 상품 클래스를 내부 로직으로 결정한다. 맞는 케이스가 없으면 IllegalArgumentException 예외를 던진다.
public class ProductCreator {
public static Product createProduct(String productId) {
if (productId.equals("aaa")) {
return new Product("AAA", 2.5);
} else if (productId.equals("cdrw")) {
return new Product("CD-RW", 1.5);
}
throw new IllegalArgumentException("Unknown Product ID");
}
}
자바 구성 클래스의 @Bean 메소드에서는 일반 자바 구문으로 정적 팩토리 메소드를 호출해 POJO 를 생성한다.
@Configuration
public class ShopConfiguration {
@Bean
public Product aaa() {
return ProductCreator.createProduct("aaa");
}
@Bean
public Product cdrw() {
return ProductCreator.createProduct("cdrw");
}
}
다음과 같이 클래스 내에 맵을 구성해 상품 정보를 담아두는 방법도 있다. 인스턴스 팩토리 메소드 createProduct() 는 productId 에 해당하는 상품을 맵에서 찾아 반환한다. 마찬가지로 맞는 케이스가 없으면 예외를 던진다.
public class ProductCreator {
private Map<String, Product> products;
public void setProducts(Map<String, Product> products) {
this.products = products;
}
public Product createProduct(String productId) {
Product product = products.get(productId);
if (product != null) {
return product;
}
throw new IllegalArgumentException("Unknown Product ID");
}
}
ProductCreator 에서 상품을 생성하려면 먼저 @Bean 을 선언해 팩토리값을 인스턴스화하고 이 팩토리의 퍼사드 역할을 하는 두 번째 빈을 선언한다. 마지막으로 팩토리를 호출하고 createProduct() 메소드를 호출해 다른 빈들을 인스턴스화한다.
@Configuration
public class ShopConfiguration {
@Bean
public ProductCreator productCreatorFactory() {
ProductCreator factory = new ProductCreator();
Map<String, Product> products = new HashMap<>();
products.put("aaa", new Product("AAA", 2.5));
products.put("cdrw", new Product("CD-RW", 1.5));
factory.setProducts(products);
return factory;
}
// ... Bean Methods
}
다음과 같이 팩토리 빈을 직접 작성하는 방법도 가능하다. 할인율과 상품이 주어지면 할인가가 적용된 상품을 생성하는 팩토리 빈을 작성해 보자. 이 빈은 product, discount 두 프로퍼티를 받아 주어진 상품에 할인가를 계산해 적용하고 상품 빈을 새로 만들어 반환한다.
public class DiscountFactoryBean extends AbstractFactoryBean<Product> {
private Product product;
private double discount;
public void setProduct(Product product) {
this.product = product;
}
public void setDiscount(double discount) {
this.discount = discount;
}
@Override
public Class<?> getObjectType() {
return product.getClass();
}
@Override
protected Product createInstance() throws Exception {
product.setPrice(product.getPrice() * (1 - discount));
return product;
}
}
팩토리 빈은 제네릭 클래스인 AbstractFactoryBean<T> 를 상속하고, createInstance() 메소드를 오버라이드해 대상 빈 인스턴스를 생성한다. 또 자동 연결 기능이 작동하도록 getObjectType() 메소드로 대상 빈 타입을 반환한다.
이제 상품 인스턴스를 생성하는 팩토리 빈에 @Bean 을 붙여 DiscountFactoryBean 을 적용한다.
@Configuration
@ComponentScan("com.apress.springrecipes.shop")
public class ShopConfiguration {
@Bean
public Product aaa() {
Product aaa = new Product("AAA", 2.5);
return aaa;
}
@Bean
public DiscountFactoryBean discountFactoryBeanAAA() {
DiscountFactoryBean factory = new DiscountFactoryBean();
factory.setProduct(aaa());
factory.setDiscount(0.2);
return factory;
}
}
#Reference.
'Study > Spring' 카테고리의 다른 글
[Spring] AOP 란? (0) | 2021.09.24 |
---|---|
[Spring 5 Recipes] Spring 5 Recipes 2장 정리 #5 (0) | 2021.09.23 |
[Spring 5 Recipes] Spring 5 Recipes 2장 정리 #3 (0) | 2021.09.17 |
[Spring 5 Recipes] Spring 5 Recipes 2장 정리 #2 (0) | 2021.09.16 |
[Spring 5 Recipes] Spring 5 Recipes 2장 정리 #1 (0) | 2021.09.15 |