1. 빈 생명주기 콜백 시작
데이터메이스 커넥션 풀이나 네트워크 소켓처럼 어플리케이션 시작 시점에 필요한 연결을 미리 해두고, 어플리케이션 종료 시점에 연결을 모두 종료하는 작업을 하려면 객체의 초기화와 종료 작업이 필요하다. 스프링은 이러한 초기화 및 종료 작업을 어떻게 처리하는지 살펴보자. 아래 예시 코드에서 우리는 외부 네트워크에 연결하는 객체를 생성한다고 가정하자 이때 조건은 다음과 같다.
- 어플리케이션 시작 시점에 connect() 메서드를 호출하여 연결되어야 한다.
- 어플리케이션이 종료되면 disConnect() 메서드를 호출하여 연결을 끊어야 한다.
public class NetworkConnection {
private String url;
public NetworkConnection() {
System.out.println("생성자 호출, url = " + this.url);
connect();
call("초기화 연결 메시지");
}
public void setUrl(String url) {
this.url = url;
}
public void connect() {
System.out.println("connect: " + this.url);
}
public void call(String message) {
System.out.println("call: "+this.url+" / message: "+message);
}
public void disconnect() {
System.out.println("disconnect: " + this.url);
}
}
이때 아래와 같은 테스트 코드를 작성했다.
public class BeanLifeCycleTest {
@Test
public void lifeCycleTest() {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
NetworkConnection connection = ac.getBean(NetworkConnection.class);
ac.close(); // 보통 close를 사용할 일이 없어서 applicationContext에서는 해당 기능을 제공하지 않아 하위 클래스를 사용한 것임.
}
@Configuration
static class LifeCycleConfig {
@Bean
public NetworkConnection networkConnection() {
NetworkConnection networkConnection = new NetworkConnection();
networkConnection.setUrl("example.com");
return networkConnection;
}
}
}
이때 결과는 다음과 같다.
결과를 보면 url이 null로 들어간 것을 확인할 수 있다. 우리는 빈을 생성할 때 먼저 생성자를 호출하고 이후에 setUrl을 통해 url을 설정했다. 하지만 networkConnection 클래스 내부를 보면 생성자를 호출한 단계에서 connect() 메서드를 수행한다. 즉, url이 설정되기 전에 이미 connect() 메서드를 수행하기 때문에 위와 같은 문제가 발생한 것이다. 스프링 빈은 아래 라이프 사이클을 따른다.
- 스프링 빈은 "객체 생성" 후 "의존 관계 주입"를 수행한다.
위와 같이 스프링은 객체 생성 후 의존 관계 주입을 수행한다. 따라서 초기화 작업은 의존 관계 주입까지 모두 종료된 후 수행해야 한다. 이를 위해 스프링은 콜백 메서드를 통해 초기화 시점을 알려주는 다양한 기능을 제공한다. 또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 따라서 종료 작업 또한 안전하게 진행할 수 있다. 결국 스프링 빈의 라이프사이클은 다음과 같다고 볼 수 있다.
- 스프링 컨테이너 생성 → 스프링 빈 생성 → 의존 관계 주입 → 초기화 콜백 → 사용 → 소멸 전 콜백 → 스프링 종료
- 초기화 콜백: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
- 소멸전 콜백: 빈이 소멸되기 직전에 호출
- 이때 위에서 url을 생성자에 넘기면 되지 않는지 생각할 수 있다. 하지만 "객체의 생성과 초기화는 분리"해야한다. 객체를 생성하는 것은 단지 객체를 생성하는 것에만 집중해야 한다. 실제 객체가 초기화 작업을 한다는 것은 객체가 동작한다는 것이다. 예를 들어, 외부와 커넥션을 하는 작업은 큰 작업이다. 그래서 객체 생성은 필요한 데이터만을 세팅하고 실제 동작과 같은 행위는 별도의 메서드로 분리해야 한다. 즉, 생성자는 필수 정보만 파라미터로 받고, 메모리를 할당해 객체를 생성해야 하며 초기화는 이렇게 생성된 값을 활용하여 커넥션과 같은 무거운 동작을 수행해야 한다. 단, 초기화 작업이 값들만 약간 변경하는 가벼운 작업이라면 생성자에서 한 번에 처리하는 것이 좋다.
스프링은 이후 배울 3가지 방법으로 빈 생명주기 콜백을 지원한다.
2. 인터페이스 InitializingBean, DisposableBean
public class NetworkConnection implements InitializingBean, DisposableBean {
private String url;
public NetworkConnection() {
System.out.println("생성자 호출, url = " + this.url);
}
public void setUrl(String url) {
this.url = url;
}
public void connect() {
System.out.println("connect: " + this.url);
}
public void call(String message) {
System.out.println("call: "+this.url+" / message: "+message);
}
public void disconnect() {
System.out.println("disconnect: " + this.url);
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("-----afterPropertiesSet()-----");
connect();
call("초기화 연결 메시지");
}
@Override
public void destroy() throws Exception {
System.out.println("-----destroy()-----");
disconnect();
}
}
- InitializingBean: afterPropertiesSet() 메서드를 사용할 수 있다. 즉, 의존관계 주입 이후 초기화 작업을 진행할 때 해당 메서드에 작성된 코드를 수행한다.
- DisposableBean: destroy() 메서드를 사용할 수 있다. 빈을 사용하고 스프링 종료 전에 처리해야할 작업을 해당 메서드 안에 작성하면 된다.
위처럼 코드를 수정하고 이전 테스트 코드를 수행시켜보면 결과는 다음과 같다. 생성자로 객체를 생성하는 단계에는 당연히 url 정보가 없으므로 null이 출력된다. 하지만 이후 connect 단계에서는 이미 setUrl을 통해 의존 관계 주입을 수행했으므로 url이 올바르게 출력되는 것을 볼 수 있다. 또한 스프링 종료 전에 disconnect 되는 것도 확인할 수 있다. 하지만 해당 방법의 문제는 다음과 같다.
- 해당 인터페이스는 스프링 전용 인터페이스다. 즉, 코드가 스프링 전용 인터페이스에 의존하게 된다.
- 초기화, 소멸 메서드의 이름을 변경할 수 없다.
- 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.
이러한 인터페이스를 활용한 초기화, 종료 방법은 스프링 초창기 방법이고, 최근에는 더 좋은 방법들을 사용하여 거의 사용하지 않는다.
3. 빈 등록 초기화, 소멸 메서드
해당 방법은 빈 생성시 초기화, 소멸 메서드를 등록하는 방법이다. 아래는 테스트 코드의 @Bean의 initMethod와 destroyMethod 속성을 활용하여 메서드를 등록하도록 수정한 코드다.
public class BeanLifeCycleTest {
@Test
public void lifeCycleTest() {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
NetworkConnection connection = ac.getBean(NetworkConnection.class);
ac.close(); // 보통 close를 사용할 일이 없어서 applicationContext에서는 해당 기능을 제공하지 않아 하위 클래스를 사용한 것임.
}
@Configuration
static class LifeCycleConfig {
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkConnection networkConnection() {
NetworkConnection networkConnection = new NetworkConnection();
networkConnection.setUrl("example.com");
return networkConnection;
}
}
}
위 테스트가 잘 동작하기 위해서는 NetworkConnection() 코드를 아래와 같이 init(), close() 메서드를 올바르게 작성해주면 된다.
public class NetworkConnection {
private String url;
public NetworkConnection() {
System.out.println("생성자 호출, url = " + this.url);
}
public void setUrl(String url) {
this.url = url;
}
public void connect() {
System.out.println("connect: " + this.url);
}
public void call(String message) {
System.out.println("call: "+this.url+" / message: "+message);
}
public void disconnect() {
System.out.println("disconnect: " + this.url);
}
public void init() {
System.out.println("-----afterPropertiesSet()-----");
connect();
call("초기화 연결 메시지");
}
public void close() {
System.out.println("-----destroy()-----");
disconnect();
}
}
그러면 아래와 같이 정상적으로 동작하는 것을 볼 수 있다.
이렇게 설정 정보를 통해 초기화 작업과 소멸 작업 메서드를 지정하는 방식은 아래와 같은 특징을 가진다.
- 메서드 이름을 바꿀 수 있다.
- 스프링 빈이 스프링 코드에 의존하지 않는다.
- 코드가 아닌 설정 정보를 사용하기 때문에 외부 라이브러리에도 적용할 수 있다.
그리고 아래와 같이 @Bean의 destroyMethod는 특이한 특징을 가진다.
- destoryMethod는 default값으로 "(inferred)"라는 값을 가진다. 보통 종료 메서드는 close, shotdown 등의 이름으로 사용되는데 해당 default값은 이러한 close, shotdown 메서드를 자동으로 호출해주기 때문에 종료 메서드를 추론하여 호출해준다. 즉, 종료 메서드는 따로 적어주지 않아도 잘 동작한다. 단, 추론 기능을 사용하기 싫다면 ' destoryMethod="" '와 같이 빈 공백을 지정해주면 된다.
4. 어노테이션 @PostConstruct, @PreDestroy
결론적으로 지금 배우는 방법을 사용하면 된다. 스프링에서도 이 방법을 권장한다. 해당 방법은 단지 어노테이션을 통해 초기화 혹은 소멸 메서드를 지정해주면 된다. 즉, 수정전 테스트 코드는 그대로 놔두고, 초기화 메서드, 소멸 메서드에 해당하는 것에 @PostConstruct, @PreDestroy 어노테이션을 붙여주면 된다.
public class NetworkConnection {
private String url;
public NetworkConnection() {
System.out.println("생성자 호출, url = " + this.url);
}
public void setUrl(String url) {
this.url = url;
}
public void connect() {
System.out.println("connect: " + this.url);
}
public void call(String message) {
System.out.println("call: "+this.url+" / message: "+message);
}
public void disconnect() {
System.out.println("disconnect: " + this.url);
}
@PostConstruct
public void init() {
System.out.println("-----afterPropertiesSet()-----");
connect();
call("초기화 연결 메시지");
}
@PreDestroy
public void close() {
System.out.println("-----destroy()-----");
disconnect();
}
}
그럼 테스트 코드도 아래와 같이 정상적인 결과를 출력한다.
해당 어노테이션의 패키지를 보면 jakarta.annotation이다.
- jakarta.annotation.PostConstruct
- jakarta.annotation.PreDestroy
따라서 스프링이 아니라 다른 컨테이너를 사용해도 그대로 적용할 수 있다. 해당 방법의 특징은 다음과 같다.
- 최신 스프링에서 권장한다.
- 스프링 종속적 기술이 아니라 자바 표준이므로 다른 컨테이너에서도 동작한다.
- 컴포넌트 스캔에도 어울린다. (@Bean을 통해 직접 등록하지 않아도 된다.)
- 단, 외부 라이브러리에 적용할 수는 없다. 코드를 고쳐야하기 때문이다. 따라서 외부 라이브러리를 초기화, 종료해야 한다면 @Bean을 통해 적용해야 한다.
'BackEnd > Spring' 카테고리의 다른 글
[Spring MVC-1] 웹 어플리케이션 이해 (0) | 2023.09.06 |
---|---|
[Spring] 빈 스코프 (0) | 2023.09.02 |
[Spring] 의존관계 자동 주입 + lombok 설정 (0) | 2023.08.30 |
[Spring] 컴포넌트 스캔 (0) | 2023.08.28 |
[Spring] 싱글톤 컨테이너 (0) | 2023.08.23 |