인프런 김영한님의 스프링 핵심 원리 - 기본편 강의 내용을 바탕으로 작성한 글입니다.
데이터베이스와의 커넥션은 연결을 설정하는 데 오래 걸리기 때문에, 애플리케이션 서버와 DB 사이의 커넥션을 미리 생성해둔다. 그리고 DB 연결 요청이 오면 연결해두었던 커넥션을 빌려주는 방식으로 동작한다.
이 커넥션은 보통 애플리케이션이 시작되는 시점에 생성되고, 애플리케이션이 종료될 때 함께 종료된다. 때문에 초기화와 종료 작업이 필요하다.
💡초기화 작업을 생성자에서 진행하면 안되는 이유?
생성자는 이름 그대로 객체 생성을 위한 메서드이다. 생성자는 초기화 작업보단 객체 생성에 집중하는 것이 SRP에 더 알맞고, 유지보수적인 측면에서 좋다. 초기화 작업에는 외부 커넥션 연결 등 무겁고 예외를 발생시킬 수 있는 작업이 포함되기 때문이다. 이러한 이유로 생성자에서 초기화 작업을 모두 수행해줄 수 없기 때문에, 별도의 초기화 메서드가 필요하다.
위와 같은 이유로, 커넥션 url 등은 초기화 메서드를 통해 별도로 세팅해주어야 한다.
이제 코드를 통해 웹 서버가 떠있는 상태에서, url로 요청이 들어온 상황을 살펴보자.
public class NetworkClient {
private String url;
public NetworkClient() {
System.out.println("생성자 호출, url = " + url);
connect();
call("초기화 연결 메세지");
}
public void setUrl(String url) {
this.url = url;
}
// 서비스 시작시 호출
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 class BeanLifeCycleTest {
@Test
public void lifeCycleTest() {
ConfigurableApplicationContext 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;
}
}
}
💡 ConfigurableAppicationContext?
close()
는 잘 쓰지 않는 메서드라서,ApplicationContext
인터페이스에서는 제공해주지 않는다.ConfigurableApplicationContext
나AnnotationConfigApplicationContext
인터페이스로 교체하자.
위 코드를 실행하면 아래와 같은 결과가 나온다.
당연한 결과다. NetworkClient
의 생성자가 실행되는 시점에서는 url 정보가 없고, 인스턴스 생성 후에야 외부에서 수정자 주입을 통해 url을 넣어주고 있기 때문이다.
이처럼 스프링 빈은 객체를 다 생성하고, 의존관계 주입을 끝낸 뒤에야 필요한 데이터를 사용할 수 있다. 그래서 초기화 작업은 의존관계 주입 이후에 진행된다.
개발자는 의존관계 주입이 언제 완료되는지 알 수 없다. 그래서 개발자가 의존관계 주입 끝나면 이 메서드 실행해줘!
라는 의미로 메서드를 만들어두면, 의존관계 주입이 완료된 시점에 스프링이 해당 메서드를 호출하여 초기화 작업을 진행한다. 이러한 메서드를 콜백 함수라고 하는데, 초기화 작업 때 실행되는 메서드를 초기화 콜백, 빈 소멸 때 실행되는 메서드를 소멸전 콜백이라고 한다.
정리하면, 스프링 빈은 아래와 같은 라이프 사이클을 가진다. (단, 생성자 주입은 객체를 생성할 때 파라미터로 스프링 빈을 받기 때문에, 객체 생성과 의존관계 주입이 동시에 일어난다.)
스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸 전 콜백 → 스프링 종료
스프링에서는 크게 3가지 방식으로 콜백을 지원한다.
public class ExampleBean implements InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() throws Exception {
// 의존관계 주입이 끝나면 호출되는 메서드
}
@Override
public void destroy() throws Exception {
// 빈이 종료되기 직전에 호출되는 메서드
}
첫 번째 방법은 인터페이스를 상속받아 콜백 메서드를 구현하는 것이다.
InitializingBean
은 afterPropertiesSet()
메서드로 초기화를 지원하고, DisposableBean
은 destroy()
메서드로 소멸을 지원한다.
설정 파일에서 빈을 등록할 때, @Bean(initMethod = "init", destroyMethod = "close")
처럼 속성을 주어 초기화 메서드와 소멸 메서드를 지정할 수 있다.
예시 코드로 살펴보자.
public class ExampleBean {
String url;
public ExampleBean() {
System.out.println("create ExampleBean, url = " + url);
}
public void setUrl(String url) {
this.url = url;
}
public void init() throws Exception {
System.out.println("ExampleBean.init, url = " + url);
}
public void close() throws Exception {
System.out.println("ExampleBean.close, url = " + url);
}
}
public class ExampleTest {
@Test
public void exampleTest() {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(ExampleConfig.class);
ExampleBean exampleBean = ac.getBean(ExampleBean.class);
ac.close();
}
@Configuration
static class ExampleConfig {
// 초기화 메서드로 init(), 소멸 메서드로 close() 지정
@Bean(initMethod = "init", destroyMethod = "close")
public ExampleBean exampleBean() {
ExampleBean exampleBean = new ExampleBean();
exampleBean.setUrl("exmapleUrl");
return exampleBean;
}
}
}
위와 같은 코드를 실행하면, 아래와 같이 정상적으로 실행되는 것을 확인할 수 있다.
이 방법은 종료 메서드 추론 기능도 제공한다. 라이브러리에서는 대부분 close
, shutdown
같은 이름의 종료 메서드를 사용하는데, destroyMethod
속성은 close
, shutdown
이라는 이름을 가진 메서드를 자동으로 종료 메서드로 등록해준다.
세 방법 중에 가장 권장되는 방법으로, 빈 초기화 시 호출하고 싶은 메서드에 @PostConstruct
어노테이션을, 빈 소멸 직전에 호출하고 싶은 메서드에 @PreDestroy
어노테이션을 추가하는 방법이다.
마찬가지로 예시 코드를 통해 확인해보자.
package hello.core.lifecycle;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
public class ExampleBean {
String url;
public ExampleBean() {
System.out.println("create ExampleBean, url = " + url);
}
public void setUrl(String url) {
this.url = url;
}
// 초기화 메서드
@PostConstruct
public void init() throws Exception {
System.out.println("ExampleBean.init, url = " + url);
}
// 소멸 메서드
@PreDestroy
public void close() throws Exception {
System.out.println("ExampleBean.close, url = " + url);
}
}
public class ExampleTest {
@Test
public void exampleTest() {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(ExampleConfig.class);
ExampleBean exampleBean = ac.getBean(ExampleBean.class);
ac.close();
}
@Configuration
static class ExampleConfig {
@Bean
public ExampleBean exampleBean() {
ExampleBean exampleBean = new ExampleBean();
exampleBean.setUrl("exmapleUrl");
return exampleBean;
}
}
}
아래와 같이 정상적으로 실행되는 것을 확인할 수 있다.
javax
패키지에 속한 어노테이션이기 때문에, 스프링에 종속적이지 않다.결론적으로 외부 라이브러리에 초기화 및 종료 메서드를 적용해야 한다면 @Bean
의 initMethod
,destoryMethod
속성을 사용하고, 그 외에는 @PostConstruct, @PreDestroy 어노테이션을 사용하는 것이 좋다.