반응형

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.

 

스프링 5 레시피(4판)

이 책은 스프링 5에 새로 탑재된 기능 및 다양한 구성 옵션 등 업데이트된 프레임워크 전반을 실무에 유용한 해법을 제시하는 형식으로 다룹니다. IoC 컨테이너 같은 스프링 기초부터 스프링 AOP/A

www.hanbit.co.kr

 

+ Recent posts