스프링 레시피 CH2.8 애너테이션을 이용해 POJO 초기화/폐기 커스터마이징하기

2023. 12. 14. 00:16spring

과제

어떤 POJO는 사용하기 전에 특정한 초기화 작업을 거쳐야 한다.

예를 들면 파일을 열거나, 네트워크/DB에 접속하거나, 메모리를 할당하는 등 선행 작업이 필요한 경우이다.

대개 이런 POJO는 그 생명을 다하는 순간에도 폐기 작업을 해줘야 한다.

IoC 컨테이너에서 빈을 초기화 및 폐기하는 로직을 커스터마이징 하라

 

해결책

자바 구성 클래스의 @Bean 정의부에서 initMethod, destroyMethod 속성을 설정하면 스프링은 이들을 각각 초기화, 폐기 콜백 메서드로 인지한다. POJO 메서드에 각각 @PostConstruct 및 @PreDestroy를 붙여도 마찬가지다.

또 스프링에서 @Lazy를 붙여 느긋한 초기화 (주어진 시점까지 빈 생성을 미루는 기법)를 할 수 있고 @DependsOn으로 빈을 생성하기 전에 다른 빈을 먼저 생성하도록 강제할 수 있다.

 

풀이

POJO 초기화/폐기 메서드는 @Bean으로 정의한다. 쇼핑몰 애플리케이션에서 체크아웃 기능을 구현해보자. 카트에 담긴 상품 및 체크아웃 시각을 텍스트 파일로 기록하는 기능을 Cashier 클래스에 추가하자

 

public class Cashier {
    
    private String fileName;
    private String path;
    private BufferedWriter writer;

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public void setPath(String path) {
        this.path = path;
    }

    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 BufferedWriter(new OutputStreamWriter(new FileOutputStream(checkoutFile, true)));
    }
    
    public void checkout(ShoppingCart cart) throws IOException {
        writer.write(new Date() + "\t" + cart.getItems() + "\r\n");
        writer.flush();
    }
    
    public void closeFile() throws IOException {
        writer.close();
    }
}

 

openFile() 메서드는 우선 데이터를 써넣을 대상 디렉터리와 파일이 있는지 확인한 뒤, 주어진 시스템 경로에 있는 텍스트 파일을 열어 writer 필드에 할당한다.

checkout() 메서드를 호출할 때마다 날짜와 카트 항목을 이 텍스트 파일에 덧붙인다.

closeFile() 메서드는 파일을 닫고 시스템 리소스를 반납한다.

 

Cashier 빈 생성 이전에 openFile() 메서드를, 폐기 직전에 closeFile() 메서드를 각각 실행하도록 자바 구성 클래스에 빈 정의부를 설정한다.

 

@Configuration
@PropertySource("classpath:discounts.properties")
@ComponentScan("com.spring.study.chapter02.shop")
public class ShopConfiguration {

    @Value("classpath:banner.txt")
    private Resource banner;

    @Bean(initMethod = "openFile", destroyMethod = "closeFile")
    public Cashier cashier() {
        String path = System.getProperty("java.io.tmpdir") + "/cashier";
        Cashier c1  = new Cashier();
        c1.setFileName("checkout");
        c1.setPath(path);
        return c1;
    }
}

 

@Bean의 initMethod, destroyMethod 속성에 각각 초기화, 폐기 작업을 수행할 메서드를 지정한다.

 

 

@PostConstruct와 @PreDestroy로 POJO 초기화/폐기 메서드 지정하기

자바 구성 클래스 외부에(@Component를 붙여) POJO 클래스를 정의할 경우에는 @PostConstruct와 @PreConstruct를 붙여 해당 클래스에 초기화/폐기 메서드를 지정한다.

 

@Component
public class Cashier {

    @Value("checkout")
    private String fileName;
    
    @Value("c:/Temp/cashier")
    private String path;
    private BufferedWriter writer;

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public void setPath(String path) {
        this.path = path;
    }

    @PostConstruct
    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 BufferedWriter(new OutputStreamWriter(new FileOutputStream(checkoutFile, true)));
    }

    public void checkout(ShoppingCart cart) throws IOException {
        writer.write(new Date() + "\t" + cart.getItems() + "\r\n");
        writer.flush();
    }

    @PreDestroy
    public void closeFile() throws IOException {
        writer.close();
    }
}

 

@Component를 붙였기 때문에 스프링의 관리 대상이 된다.

 

스프링은

openFile() 메서드에 @PostConstruct가 달려있기 때문에 빈 생성 이후에 이 메서드를 실행하고,

closeFile() 메서드에 @PreDestroy가 달려있기 때문에 빈 폐기 이전에 이 메서드를 실행한다.

 

@Lazy로 느긋하게 POJO 초기화하기

기본적으로 스프링은  모든 POJO를 시동과 동시에 초기화한다. 하지만 환경에 따라 빈을 처음으로 요청하기 전까지 초기화 과정을 미루는 게 더 나을 때도 있다.

 

느긋하게 초기화하면 시동 시점에 리소스를 집중 소모하지 않아도 되므로 전체 시스템 리소스를 절약할 수 있다.

@Component
@Scope("prototype")
@Lazy
public class ShoppingCart {
    private List<Product> items = new ArrayList<>();

    public void addItem(Product item) {
        items.add(item);
    }

    public List<Product> getItems() {
        return items;
    }
}

 

@DependsOn으로 초기화 순서 정하기

POJO가 늘어나고 분산 선언된 많은 POJO가 서로를 참조하다 보면 경합 조건이 일어나기 쉽다.

그럴때 @DependsOn 애너테이션은 빈의 초기화 순서를 보장한다.

 

@Configuration
@Import(SequenceConfig.class)
public class SequenceGeneratorConfiguration {


    @Value("#{uniqueSequence}")
    private Sequence sequence;

    @Bean
    @DependsOn("datePrefixGenerator")
    public Sequence sequence() {
        sequence.setId(sequenceGenerator().getSequence());
        return sequence;
    }

    @Bean
    SequenceGenerator sequenceGenerator() {
        SequenceGenerator seqgen = new SequenceGenerator();
        seqgen.setPrefix("30");
        seqgen.setSuffix("A");
        seqgen.setInitial(100000);

        return seqgen;
    }
}