Published 2022. 1. 14. 08:33
반응형

데이터베이스의 커넥션 풀은 애플리케이션이 시작될 때 연결을 해서 풀을 생성해 놓는다.
이후 애플리케이션이 종료가 될 때 연결을 끊게 된다.
이를 스프링에서 사용한다고 하면 빈이 의존관계 주입까지 끝나면 연결을 하고, 빈이 소멸되면
연결을 끊는 행위가 필요하다. 그렇다면 스프링에서는 이를 어떻게 처리할 수 있을까?

스프링 빈의 생명 주기


스프링은 아래와 같은 생명 주기를 갖는다.

  • 스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용... → 소멸 전 콜백 → 스프링 종료

초기화 콜백 → 빈이 생성되고, 의존관계 주입이 완료된 후에 호출되는 것

소멸 전 콜백 → 빈이 소멸되기 전에 호출되는 것

당연한 것이지만 스프링은 의존관계 주입까지 끝나야 올바르게 빈을 사용할 준비가 된다.
의존관계 주입이 안됐으면 당연히 온전치 않은거니까.
아래의 코드로 온전치 않은 상황을 살펴 보자.

빈으로 등록될 대상

package hello.core.lifecycle;

public class NetworkClient {

    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
        connect();
        call("초기화 연결 메시지");
    }

    public void connect() {
        System.out.println("connect = " + url);
    }

    public void call(String message) {
        System.out.println("call = " + url + ", message = " + message);
    }

    public void disconnect() {
        System.out.println("close = " + url);
    }

    public void setUrl(String url) {
        this.url = url;
    }
}

빈 등록 테스트 하기

@Test
void lifeCycleTest() {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
    NetworkClient client = ac.getBean(NetworkClient.class);
    ac.close();
}

@Configuration
static class LifeCycleConfig {

    @Bean
    public NetworkClient networkClient() {
        NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl("http://hello-spring.dev");
        return networkClient;
    }
}

/* 결과
생성자 호출, url = null
connect = null
call = null, message = 초기화 연결 메시지
*/

코드에 대해 설명을 하자면, 생명 주기 중 빈 생성을 한 후 의존 관계를 주입 했기 때문에 빈이 생성되는 시점에
아래 코드는 결과가 null이게 된다. null인 이유는 빈을 생성 후 의존 관계 주입을 하기 때문이다.
만약 데이터베이스의 커넥션 풀을 관리하는 코드였다면 커넥션이 되지 않는 코드인 것이다.
즉, 의존 관계 주입이 끝난 시점과 빈이 소멸되기 전의 시점을 알야하는 것이다.

    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
        connect();
        call("초기화 연결 메시지");
    }

그냥 생성자 주입으로 하면 안되나?


아래처럼 생성자 주입으로 하게 되면 정상적으로 커넥션은 된다.

    public NetworkClient(String url) {
                this.url = url;
        System.out.println("생성자 호출, url = " + url);
        connect();
        call("초기화 연결 메시지");
    }

객체의 생성과 초기화를 분리하자.

하지만 이런 커넥션 작업은 실제로는 매우 비용이 비싼 작업이다. 그렇기 때문에 실제로 이렇게 단순하지 않다.
보통 MVC패턴에서 서비스를 주입받고 리파지토리를 주입 받는 것은 비용이 싸기 때문에 생성자 주입을 선호하지만
이런 작업은 아닌 것이다.

그렇다면 어떻게 그 시점을 알 수 있을까?


인터페이스(InitializingBean, DisposableBean)

가장 처음에 나온 방법으로 현재는 거의 사용하지 않는 방법이다.
코드에 두 인터페이스만 상속받고 구현만 해주면 된다. 로그를 보면 주입(초기화)이 끝난 후
사용을 하게 되므로 연결이 잘 된 것을 볼 수 있다.

public class NetworkClient implements InitializingBean, DisposableBean {

    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
                // 기존에 있던 코드가 어버라이드 메서드로 이동했다.

    }

        ...

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("NetworkClient.afterPropertiesSet");
        connect();
        call("초기화 연결 메시지");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("NetworkClient.destroy");
        disconnect();
    }
}

/*결과
생성자 호출, url = null

NetworkClient.afterPropertiesSet
connect = http://hello-spring.dev

call = http://hello-spring.dev, message = 초기화 연결 메시지

NetworkClient.destroy
close = http://hello-spring.dev
*/

단점

  • 스프링 전용 인터페이스 의존한다.
  • 초기화, 소멸 메서드 이름을 변경할 수 없기 때문에 외부 라이브러리에 적용할 수 없다.
    반드시 초기화, 소멸 메서드를 호출해야 하는데 이름이 다르다면 사용할 수 없기 때문이다.

설정 정보에 초기화 메서드, 종료 메서드 지정

이것도 거의 사용하지 않는 옜날 방식이다.
임의로 init(), close() 메서드를 선언 해준다. 이후 빈 등록 하는 곳에서 @Bean(initMethod = "init", destroyMethod = "close")
``넣어 주면 된다.

public class NetworkClient {

    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
                // 기존에 있던 코드가 어버라이드 메서드로 이동했다.

    }

        ...

    public void init() throws Exception {
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메시지");
    }

    public void close() throws Exception {
        System.out.println("NetworkClient.close");
        disconnect();
    }
}

/*결과
생성자 호출, url = null

NetworkClient.init
connect = http://hello-spring.dev

call = http://hello-spring.dev, message = 초기화 연결 메시지

NetworkClient.close
close = http://hello-spring.dev
*/

@Configuration
static class LifeCycleConfig {

    @Bean(initMethod = "init", destroyMethod = "close") // 요기
    public NetworkClient networkClient() {
        NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl("http://hello-spring.dev");
        return networkClient;
    }
}

특징

  • 스프링에 의존하지 않는다.
  • 메서드명이 자유롭기 때문에 외부 라이브러리에도 적용하기 쉽다.

@PostConstruct, @PreDestroy

public class NetworkClient {

    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
                // 기존에 있던 코드가 어버라이드 메서드로 이동했다.

    }

        ...

        @PostConstruct
    public void init() throws Exception {
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메시지");
    }

        @PreDestroy
    public void close() throws Exception {
        System.out.println("NetworkClient.close");
        disconnect();
    }
}

장점

  • 스프링에서 권장하는 방법이다.
  • javax 패키지라서 자바 표준이다. 그래서 스프링에 종속적이지 않다.
  • 일단 그냥 편하다.

단점

  • 외부라이브러리랑 사용할 수 없다.
    이럴 때는 바로 위에서 말한 @Bean(initMethod = "init", destroyMethod = "close")을 사용하면 된다.
반응형
복사했습니다!