Coderoad

빈 생명주기와 콜백

2023-01-23 at Spring category

본 문서는 인프런에서 수강할 수 있는 스프링 핵심 원리 - 기본편을 수강한 후, 공부한 내용을 정리한 문서입니다. 본 문서의 모든 저작권은 해당 강의의 저자이신 김영한 우아한형제들 기술이사님께 있습니다.

Spring 빈의 생명주기와 콜백

우리가 웹 애플리케이션을 작성하다보면 다른 객체들과의 연결이 필요하다는 것을 알 수 있습니다. 특히, 데이터베이스와의 연결이나 네트워크 소켓 연결 같은 작업은 웹 애플리케이션 개발 시 필수에 가까운 작업입니다. 그러나, 이렇게 외부와 연결하는 작업들은 상당히 긴 시간이 소요됩니다. 개발자는 사용자의 요청이 들어왔을 경우 바로 응답할 수 있도록 미리 연결(DB 커넥션 풀 등)된 인스턴스들을 사용해야하며, 예기치 못한 오류를 피하기 위해 외부 연결들을 애플리케이션 종료 시점보다 먼저 연결 종료하는 안전한 프로그램을 작성해야합니다. 따라서, 개발자는 객체의 초기화와 종료 과정(생명주기)을 직접 제어해야합니다.

Spring 빈은 객체가 생성되고 의존관계가 주입된 후에 사용할 준비가 완료됩니다. 즉, 초기화 작업(구현 객체 주입)은 빈이 Spring 컨테이너에 등록되고, 의존관계까지 주입이 모두 끝난 다음 진행해야합니다. 당연한 이야기지만 초기화 작업을 위해선 Spring 빈의 의존관계 주입이 완료되는 시점을 알아야합니다. Spring 빈이 사용될 준비를 다 마치지도 않은 시점에서 사용하려하면 아무런 구현 객체도 가지지 않는 빈을 사용하게 되어 애플리케이션이 의도한대로 작동하지 않을 것입니다.

다행히, Spring은 의존관계 주입을 완료하면 콜백 메소드를 통해 빈의 초기화가 가능하다는 것을 알려줍니다. 또한, Spring 컨테이너가 소멸되기 전에도 소멸 콜백 메소드를 호출해 애플리케이션이 종료된다는 것을 알려주는데, 개발자는 이 콜백 메소드들 덕분에 초기화와 종료 전 로직들을 정상적으로 수행할 수 있습니다.

지금까지 살펴본 내용을 토대로 Spring 빈의 생명주기를 정리하자면 다음과 같습니다.

컨테이너 생성 -> 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 빈 사용 -> 소멸 전 콜백 -> Spring 종료

이 중, 초기화 콜백과 소멸 전 콜백을 생명주기 콜백이라고 합니다. Spring은 크게 3가지 방식으로 생명주기 콜백을 지원합니다.

  • 인터페이스 (InitializingBean, DisposableBean)
  • 설정 정보에 초기화 메소드, 종료 메소드 직접 작성
  • @PostConstruct, @PreDestroy 어노테이션

인터페이스를 통한 생명주기 콜백 다루기

첫 번째로 인터페이스를 통해 콜백 메소드를 다루는 방법에 대해 알아보겠습니다. 먼저, 초기화는 InitializingBean 인터페이스의 afterPropertiesSet() 메소드를 활용해서 진행합니다. 이 메소드는 의존관계가 주입이 모두 완료된 후에 호출되기 때문에, 초기화 관련 코드를 넣어두면 초기화가 누락되는 등의 오류 없이 진행할 수 있습니다.

다음으로 빈 소멸은 DisposableBean 인터페이스의 destroy() 메소드를 통해 구현합니다. 이 메소드는 Spring 컨테이너가 종료 로직이 시작될 때, 호출되어 애플리케이션이 완전 종료되기 전에 필요한 로직들을 먼저 수행할 수 있도록 해줍니다.

인터페이스만 구현해주면 되는 이 방식에는 큰 문제가 있는데, Spring 전용 인터페이스이기 때문입니다. 다시 말해서 코드가 Spring에 강하게 의존하게 되고, DIP를 위반할 가능성이 높습니다.

요즘은 인터페이스를 활용하는 방식보다 더 좋은 방식들이 나오게 되어 잘 사용하지 않는 방식입니다.

빈 등록 시 초기화와 소멸 메소드 지정

다음은 Java 설정 클래스에서 @Bean 어노테이션을 통해 Spring 빈을 등록할 때 해당 빈의 초기화 메소드와 소멸 메소드를 직접 지정하는 방식입니다.

@Configuration
static class AppConfig {
    //객체를 빈으로 등록할 때,
    //해당 객체에 작성한 메소드들을 각각 초기화와 소멸 메소드로 지정할 수 있습니다.
    @Bean(initMethod = "init", destroyMethod = "close")
    public DatabaseClient databaseClient() {
        DatabaseClient databaseClient = new DatabaseClient();
        return databaseClient;
    }
}

DatabaseClient 객체의 init 메소드와 close 메소드를 각각 초기화와 소멸 메소드로 지정했습니다. 당연히 직접 작성한 메소드를 활용하기 때문에 메소드 이름은 자유롭게 정할 수 있습니다. 또한, Spring 빈이 더 이상 Spring에 의존적이지 않기 때문에 외부 라이브러리에서도 우리가 직접 지정한 생명주기 콜백 메소드들을 활용할 수 있게 됩니다.

이 방식의 정말 특별한 기능이 하나 있는데, 바로 소멸 메소드 추론입니다. 소멸 메소드를 지정하는 @Bean 어노테이션의 destroyMethod 속성은 소멸 콜백 메소드의 이름을 말 그대로 '추론'합니다. 개발자나 라이브러리는 주로 소멸 메소드의 이름으로 close, shutdown 등의 이름을 사용합니다. destroyMethod는 이러한 이름들이 확인되면 소멸 메소드를 직접 지정해주지 않아도 해당 이름을 가진 메소드를 소멸 메소드로 '추론'하고 자동으로 호출합니다.

어노테이션을 통한 콜백 다루기

마지막으로 어노테이션을 통한 초기화와 소멸 콜백 메소드 구현 방식입니다. 초기화 콜백에는 @PostConstruct를, 소멸 콜백에는 @PreDestroy 어노테이션을 사용합니다.

public class DatabaseClient {
    @PostConstruct
    public void init() {
        System.out.println("Database connection init");
        connect();
    }

    @PreDestroy
    public void close() {
        System.out.println("Database connection close");
        disconnect();
    }
}

예시 코드를 보면, 초기화 로직을 구현한 메소드에 @PostConstruct를 붙이고 소멸 로직을 구현한 메소드에 @PreDestroy를 붙인 것을 확인할 수 있습니다. 이렇게 어노테이션을 붙이는 것만으로 간단하게 생명주기 콜백을 구현할 수 있습니다. 또한, 이러한 편리함과 Java 표준이라는 점 덕분에 최신 Spring에서 가장 권장하는 방법이라고 합니다.

거기다 자동 빈 등록 방식인 컴포넌트 스캔과도 가장 잘 맞는 방식입니다. 일단, 인터페이스를 이용한 방식은 Spring에 의존적이기 때문에 피하는 것이 좋습니다. Java 설정 클래스에서 @Bean의 속성을 이용하는 방법은 Spring 빈이 될 클래스를 직접 지정합니다. 이는 곧 자동 빈 등록 기능을 사용하지 않는 것이기 때문에 딱히 효율적인 방식은 아닙니다.

어노테이션을 활용하는 방식의 유일한 단점은 외부 라이브러리에서는 사용하지 못한다는 점입니다. 따라서 상황과 필요에 따라 @Bean의 속성을 활용하거나 @PostConstruct@PreDestroy 어노테이션을 통해 콜백을 다루는 것이 좋습니다.

hangillee

Personal blog by hangillee.

Road to good developer.