스프링 컨테이너는 설정 클래스의 정보를 읽어와 알맞은 빈 객체를 생성하고 각 빈을 연결(의존 주입)하는 작업을 수행한다. 스프링 컨테이너를 초기화를 해야 등록된 빈을 getBean()이나 다른 방법을 통해서 읽어와 사용할 수 있게된다. 컨테이너 사용을 마친 후 등록된 빈들은 모두 소멸된다. close() 메서드를 이용해서 컨테이너를 종료하고 등록된 빈들도 모두 소멸하게된다. 이렇게 스프링 컨테이너는 각 빈들의 라이프사이클을 관리하게 되는데 어떻게 빈이 초기화가 되고 소멸이 되는지 알아보자.
빈 객체를 초기화하고 소멸하는 방법은 크게 2가지이다. 첫번째는 빈 객체의 지정한 메서드를 호출하는 방법이다. 스프링의 InitializingBean, DispoableBean 인터페이스를 상속하는 것이다. 두 인터페이스를 살펴보자.
public interface InitializingBean {
void afterPropertiesSet() throws Exception;
}
public interface DisposableBean {
void destroy() throws Exception;
}
빈 객체가 이 두 인터페이스를 상속하여 위의 두 메서드를 오버라이드한다면 스프링 컨테이너는 초기화 과정에서 빈 객체에서 afterPropertiesSet()을 실행할 것이다. 그리고 스프링 컨테이너가 종료되는 시점에 destroy()를 실행된다.
이렇게 두 개의 인터페이스를 상속하여 알맞게 두 메서드를 재정의하여 사용하게 되면 초기화와 소멸 과정을 개발자가 컨트롤할 수 있게된다. 예시 코드를 통해 한번 테스트를 해보자.
public class Client implements InitializingBean, DisposableBean {
@Autowired
private Dependency dependency;
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("Client 빈 객체 초기화");
dependency.run();
}
@Override
public void destroy() throws Exception {
System.out.println("Client 빈 객체 소멸");
}
}
@Configuration
public class AppCtx {
@Bean
public Client client() {
return new Client();
}
}
@Test
void 빈_라이프사이클_테스트() {
AnnotationConfigApplicationContext appCtx = new AnnotationConfigApplicationContext(AppCtx.class,Dependency.class);
appCtx.close();
}
18:47:10.953 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'client'
Client 빈 객체 초기화
의존성 주입 성공
18:47:10.974 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@36bc55de, started on Wed Dec 11 18:47:10 KST 2024
Client 빈 객체 소멸
위의 코드를 보면 두개의 인터페이스를 상속하여 두 개의 초기화, 소멸 메서드를 오바리이드한 Client 클래스를 볼 수 있다. 그리고 이 Client 메서드를 설정 클래스에 빈으로 등록해주고 테스트를 진행했다. 테스트코드를 보면 스프링 컨테이너를 초기화하는 시점에서 Client의 afterPropertiesSet()이 실행된 것을 볼 수있다. 그리고 close() 메서드를 통해 스프링 컨테이너를 종료시키면 destroy() 메서드가 실행되어 빈 객체가 소멸되는 것을 볼 수 있다.
하지만 close() 메서드를 실행하지 않으면 스프링 컨테이너는 종료되지 않기 때문에 destroy() 메서드가 실행 자체가 되지 않아 해당 빈이 소멸되지 않는다.
두번째 빈 객체 초기화 소멸방법은 커스텀 메서드를 사용하는 방법이다. 모든 클래스가 앞서 말한 InitializingBean, DisposableBean 인터페이스를 상속받아 구현할 수 있는 것은 아니다. 예를 들어 직접 구현한 클래스가 아닌 외부에서 제공받은 클래스는 개발자가 직접 수정하기가 어렵다. 따라서 @Bean의 initMethod, destroyMethod를 이용해서 빈 객체에 커스텀 메서드를 구현하여 등록해주면 스프링 컨테이너가 자동으로 초기화와 소멸을 시켜준다. 코드를 보자.
public class ClientWithCustomMethod {
@Autowired
private Dependency dependency;
public void init() {
System.out.println("ClientWithCustomMethod 빈 객체 초기화");
dependency.run();
}
public void destroy() {
System.out.println("ClientWithCustomMethod 빈 객체 소멸");
}
}
@Configuration
public class AppCtxWithCustomMethod {
@Bean(initMethod = "init", destroyMethod = "destroy")
public ClientWithCustomMethod clientWithCustomMethod() {
return new ClientWithCustomMethod();
}
}
@Test
void 커스텀_메서드를_이용한_객체_초기화와_소멸() {
AnnotationConfigApplicationContext appCtx = new AnnotationConfigApplicationContext(AppCtxWithCustomMethod.class, Dependency.class);
appCtx.close();
}
19:37:50.606 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'clientWithCustomMethod'
ClientWithCustomMethod 빈 객체 초기화
의존성 주입 성공
19:37:50.634 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@8f4ea7c, started on Wed Dec 11 19:37:50 KST 2024
ClientWithCustomMethod 빈 객체 소멸
ClientWithCustomMethod 클래스를 보면 init(), destroy() 메서드가 있다. 그리고 설정 클래스인 AppCtxWithCustomMethod 클래스에 빈 객체로 ClinetWithCustomMethod를 등록하는데 @Bean 속성 initMethod에 init 메서드를 지정하고 destroyMethod에 destroy 메서드를 지정하여 초기화와 소멸시 해당 메서드가 실행되도록 하였다. 테스트 결과를 보녀 스프링 컨테이너가 초기화 되면 ClientWithCustomMethod의 init 메서드가 실행된 것을 볼 수 있다. 그리고 close() 메서드가 실행되면서 소멸 메서드인 destroy가 실행된 것을 볼 수 있다. 이렇게 커스텀 메서드를 통해 굳이 상속을 통해 구현하는 것이 아니라 메서드만 정의하고 @Bean 속성에 지정해주면 스프링 컨테이너가 자동으로 초기화, 소멸 메서드를 실행해준다.
언제 라이프사이클이 사용될지 한 번 생각해보자. 예를 들어 데이터베이스 연결을 위해서 커넥션 풀을 위한 빈 객체를 초기화한다고 하자. 초기화 후 데이터베이스에 연결하고 사용이 끝나면 해당 커넥션을 반납해야 하는데 이 때 해당 빈은 소멸되어야 한다. 이경우 빈을 초기화하고 소멸시켜야 한다. 또 다른 예로 채팅 클라이언트가 있다. 채팅 클라이언트는 시작할 때 서버와 연결을 하고 종료할 때 연결을 끝는다. 이때 서버와 연결을 생성하고 끊는 작업을 초기화 시점과 소멸 시점에 수행하게 된다.
이렇듯 여러 경우에 빈 객체를 초기화하고 소멸시킬 필요가 있다. 따라서 빈의 생성, 초기화, 소멸 과정을 관리함으로써 어플리케이션의 리소스를 효율적으로 관리하고 빈의 초기화 소멸 과정에서 필요한 작업을 정의할 수 있된다. 또한 빈의 의존성을 명시적으로 주입 됨으로써 초기화 소멸 과정에 어떤 의존성이 빈 객체에 들어가는 지 알 수 있어 오류로부터 빠르게 대응이 가능해진다. 따라서 빈 라이프사이클은 어플리케이션의 효율성과 안정성을 유지하는데 기여한다.
스프링 컨테이너는 빈 객체를 한 개만 생성한다고 했다. 즉, 싱글톤으로 하나의 빈만이 스프링 컨테이너에 등록되어 있기 때문에 getBean()을 통해서 동일한 이름을 갖는 빈 객체를 구하면 서로 동일한 객체를 참조하고 있는 것이다.
@Test
void 초기화된_빈_객체는_싱글톤이다() {
AnnotationConfigApplicationContext appCtx = new AnnotationConfigApplicationContext(AppCtx.class, Dependency.class);
Client client1 = appCtx.getBean("client", Client.class);
Client client2 = appCtx.getBean("client", Client.class);
appCtx.close();
Assertions.assertThat(client1).isEqualTo(client2);
}
이렇게 한 식별자에 대해 한 개의 객체만 존재하는 빈은 싱글톤 범위를 갖는다. 따로 설정을 해주지 않으면 기본값은 싱글톤 범위가 된다.
하지만 스프링에는 여러 범위를 가질 수 있는데 대표적으로 프로토타입, request, Sesseion, Application 등이 있다. 이 포스트에서 다루는 것은 프로토타입이다. 싱글톤은 앞에서 말했듯이 스프링 컨테이너에서 빈 객체가 하나만 생성되어 모든 요청에서 동일한 인스턴스가 반환된다. 프로토타입은 빈 객체가 요청될 때마다 서로 다른 객체가 반환되도록 설정하는 것이다.
프로토타입을 언제 사용할까? 일단 동일한 인스턴스를 사용하면 안되기 때문에 새로운 인스턴스를 사용할 것이다. 그러면 어떠한 상태를 유지하거나 공유하면 안되기 때문에 요청마다 새로운 인스턴스가 필요할 것이다. 따라서 프로토타입 빈 스코프는 상태를 가지거나, 독립적인 설정이 필요한 경우 사용된다.
이제 프로토타입 스코프를 설정해보자.
public class PrototypeClient {
@Autowired
private Dependency dependency;
public void init() {
System.out.println("PrototypeClient 빈 객체 초기화");
dependency.run();
}
public void destroy() {
System.out.println("PrototypeClient 빈 객체 소멸");
}
}
@Configuration
public class AppCtxPrototype {
@Bean(initMethod = "init", destroyMethod = "destroy")
@Scope("prototype")
public PrototypeClient prototypeClient() {
return new PrototypeClient();
}
}
@Test
void 프로토타입_객체는_서로_다른_객체() {
AnnotationConfigApplicationContext appCtx = new AnnotationConfigApplicationContext(AppCtxPrototype.class, Dependency.class);
PrototypeClient prototypeClient1 = appCtx.getBean("prototypeClient", PrototypeClient.class);
PrototypeClient prototypeClient2 = appCtx.getBean("prototypeClient", PrototypeClient.class);
appCtx.close();
Assertions.assertThat(prototypeClient1).isNotSameAs(prototypeClient2);
}
20:11:27.344 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'dependency'
PrototypeClient 빈 객체 초기화
의존성 주입 성공
PrototypeClient 빈 객체 초기화
의존성 주입 성공
프로토타입 스코프를 설정해주는 것은 매우 간단하다. 빈으로 객체를 스프링 컨테이너에 등록해주는 코드에 @Scope어노테이션을 붙이고 스코프 속성에 prototype이라고 명시해주면 자동으로 해당 객체를 요청할 때마다 새로운 객체를 생성해서 반환해준다. 테스트를 보면 2번 객체를 생성하는 것을 볼 수 있는데 이때 콘솔 창을 보면 2번 객체가 생성되었음을 알 수 있다.
근데 여기서 이상한 점이 있다. 분면 close()를 통해 스프링 컨테이너를 종료시켰는데 destroy() 메서드가 실행되지 않았다. 왜 그런것일까? 이는 싱글톤 스코프와 프로토타입 스코프의 빈 라이프사이클 관리 방식이 다르기 때문이다. 싱글톤 빈은 컨테이너가 종료될 때 소멸 메서드가 호출되지만 프로토타입 빈은 스프링 컨테이너가 빈을 생성하고 의존성을 주입한 후, 그 빈의 라이프사이클을 더 이상 관리하지 않는다. 따라서 컨테이너가 종료되어도 소멸 메서드를 실행하지 않는 것이다. 프로토타입 빈을 관리하기 위해서는 ObjectFactory를 이용하여 명시적으로 해당 빈들을 초기화하고 수동으로 소멸해주어야 한다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.stereotype.Component;
@Component
public class PrototypeClientManager {
@Autowired
private ObjectFactory<PrototypeClient> prototypeClientFactory;
public void createAndUsePrototypeClient() {
PrototypeClient prototypeClient = prototypeClientFactory.getObject();
prototypeClient.init(); // 초기화 메서드 호출
// ... 사용 로직 ...
prototypeClient.destroy(); // 수동으로 소멸 메서드 호출
}
}
참조 : 스프링 입문 5 - 최범균
참고 코드