빈 생명주기 콜백
데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고,
애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요하다.
스프링 빈 라이프사이클
객체 생성 -> 의존관계 주입
스프링 빈의 이벤트 라이프사이클
스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸전 콜백 -> 스프링 종료
-초기화 콜백: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
-소멸전 콜백: 빈이 소멸되기 직전에 호출
=> 초기화 콜백을 빈의 생성자에서 처리하면 안될까?
객체의 생성과 초기화를 분리하자.
생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다. (동작에 필요한 데이터 세팅)
반면에 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는등 무거운 동작을 수행한다.
따라서 생성자 안에서 무거운 초기화 작업을 함께 하는 것 보다는 객체를 생성하는 부분과 초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋다.
물론 초기화 작업이 내부 값들만 약간 변경하는 정도로 단순한 경우에는 생성자에서 한번에 다 처리하는게 더 나을 수 있다.
+지연 기능 가능. 객체 생성 후 외부로부터 신호가 올 때까지 대기 -> 신호가 오면 콜백 메서드를 호출하는 방식
스프링의 빈 생명주기 콜백
- 인터페이스(InitializingBean, DisposableBean)
- 설정 정보에 초기화 메서드, 종료 메서드 지정
- @PostConstruct, @PreDestroy 애노테이션 지원
인터페이스 InitializingBean, DisposableBean
InitializingBean: afterPropertiesSet() 메서드로 초기화 지원. 의존관계 주입이 끝난 후 호출
DisposableBean: destroy() 메서드로 소멸 지원
초기화, 소멸 인터페이스 단점
이 인터페이스는 스프링 전용 인터페이스다. 해당 코드가 스프링 전용 인터페이스에 의존한다.
초기화, 소멸 메서드의 이름을 변경할 수 없다.
내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.
참고: 인터페이스를 사용하는 초기화, 종료 방법은 스프링 초창기에 나온 방법들이고, 지금은 다음의 더 나은 방법들이 있어서 거의 사용하지 않는다.
빈 등록 초기화 메서드, 소멸 메서드 지정
설정 정보에 @Bean(initMethod = "init", destroyMethod = "close") 처럼 초기화, 소멸 메서드를 지정할 수 있다.
설정 정보 사용 특징
- 메서드 이름을 자유롭게 줄 수 있다.
- 스프링 빈이 스프링 코드에 의존하지 않는다.
- 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다. (코드 내부 변경X)
종료 메서드 추론 ( @Bean(destroyMethod) )
- 외부 라이브러리는 대부분 "close" , "shutdown" 이라는 이름의 종료 메서드를 사용한다.
- @Bean의 destroyMethod 는 기본값이 (inferred) (추론)으로 등록되어 있다. 이 추론 기능은 "close" , "shutdown" 라는 이름의 메서드를 자동으로 호출해준다. 따라서 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 동작한다.
- 추론 기능을 사용하기 싫으면 destroyMethod="" 처럼 빈 공백을 지정하면 된다.
애노테이션 @PostConstruct, @PreDestroy
초기화, 종료 메서드에 직접 애노테이션을 붙인다.
- 최신 스프링에서 가장 권장하는 방법이다.
- 스프링에 종속적인 기술이 아니라 JSR-250 라는 자바 표준이다. ( javax.annotation.PostConstruct ) 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다.
- 컴포넌트 스캔과 잘 어울린다.
- 유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다.
기본적으로 이 방법을 사용하고 외부 라이브러리를 초기화, 종료해야 하면 @Bean의 기능을 사용하자.
빈 스코프
빈이 존재할 수 있는 범위
스프링이 지원하는 스코프
- 싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
- 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입(+초기화 메서드)까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.
- 웹 관련 스코프
- request: 웹 요청이 들어오고 나갈 때 까지 유지되는 스코프
- session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프 (로그인..)
- application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
스코프 지정 (@Scope)
자동 등록
@Scope("prototype")
@Component
public class HelloBean {}
수동 등록
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}
프로토타입 스코프
싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다.
반면에, 프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다.
- 스프링 컨테이너에 요청할 때 마다 새로 생성된다.
- 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입, 초기화까지만 관여한다.
- @PreDestroy 같은 종료 메서드가 호출되지 않는다.
- 프로토타입 빈은 프로토타입 빈을 조회한 클라이언트가 관리해야 한다. 종료 메서드에 대한 호출도 클라이언트가 직접 해야한다.
프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점
스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환한다.
하지만 싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않으므로 주의해야 한다.
싱글톤 빈에서 프로토타입 빈 사용
싱글톤 빈을 생성할 때, 의존관계 주입에 의해 프로토타입 빈이 생성되어 필드로 들어온다.
프로토타입 빈은 이미 "주입이 끝난 빈"으로 클라이언트가 해당 빈을 사용 할 때마다 새로 생성되지 않고 싱글톤 빈과 함께 계속 유지된다. -> 프로토타입 빈 사용 의도가 아님
싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에 이때 프로토타입 빈이 새로 생성되기는 하지만 싱글톤 빈과 함께 계속 유지되는 문제
참고: 여러 빈에서 같은 프로토타입 빈을 주입 받으면, 주입 받는 시점에 각각 새로운 프로토타입 빈이 생성된다.
예를 들어서 clientA, clientB가 각각 의존관계 주입을 받으면 각각 다른 인스턴스의 프로토타입 빈을 주입 받는다.
clientA prototypeBean@x01
clientB prototypeBean@x02
물론 사용할 때 마다 새로 생성되는 것은 아니다.
해결 방안
1. 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청
스프링 컨테이너(ac)를 직접 사용
- ac.getBean() 을 통해 스프링 컨테이너에 프로토타입 빈을 새로 요청하기
- Dependency Lookup(DL): 의존관계 조회(탐색). 의존관계를 외부에서 주입(DI)받는 것이 아니라, 직접 필요한 의존관계를 찾는 것
- 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다.
- 아래 소개되는 방법들도 결국 내부적으로 스프링 컨테이너에서 찾아서 반환한다.
2. ObjectProvider
- 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스 제공 ( ObjectFactory 상속 )
- 의존 관계 주입으로 제공받을 수 있다.
- 스프링에 의존적이지만, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.
@Autowired
private ObjectProvider<PrototypeBean> provider;
//프로토타입 빈 DL
PrototypeBean prototypeBean = provider.getObject();
3. javax.inject.Provider
- JSR-330 자바 표준을 사용하는 방법
- 이 방법을 사용하려면 javax.inject:javax.inject:1 라이브러리를 gradle에 추가해야 한다.
- get() 메서드 하나로 기능이 매우 단순하다.
- 자바 표준(JSR330)이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
//implementation 'javax.inject:javax.inject:1' gradle 추가 필수
@Autowired
private Provider<PrototypeBean> provider;
//프로토타입 빈 DL
PrototypeBean prototypeBean = provider.get();
정리
프로토타입 빈은 매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요할 때 사용한다.
그런데 실무에서는 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.
ObjectProvider , JSR330 Provider 등은 프로토타입 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있다.
참고: 스프링을 사용하다 보면 이 기능 뿐만 아니라 다른 기능들도 자바 표준과 스프링이 제공하는 기능이 겹칠때가 많이 있다. 대부분 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에, 특별히 다른 컨테이너를 사용할 일이 없다면, 스프링이 제공하는 기능을 사용하면 된다. 스프링도 표준을 장려하는 기능은 예외.