자바 빈
Spring IoC 컨테이너에서 생성된 객체를 의미합니다. Spring IoC 컨테이너는 하나 이상의 빈을 관리하며 이러한 빈은 컨테이너에 제공하는 구성 메타데이터를 참고해 생성한다고 했습니다. 컨테이너 자체 내에서 이러한 빈 정의는 BeanDefinition 객체처럼 표기되며, 이 객체는 다음과 같은 정보를 가집니다.
- Bean이 컨테이너에서 어떻게 동작해야 하는지를 나타내는 Bean 동작 설정 요소(범위, 수명 주기 콜백 등).
- 작업을 수행하는 데 필요한 다른 Bean에 대한 참조. (이를 협력자 또는 의존관계라고 합니다.)
- 새롭게 생성된 개체를 설정할 기타 설정 (예시: 연결 풀을 관리하는 Bean에서 사용할 연결 수 또는 풀 크기 제한.)
1. Bean의 이름
빈에는 일반적으로 하나의 식별자만 있습니다. 또한 각각의 식별자는 스프링 IoC 컨테이너 내에서 유일해야 합니다. 예외적으로 식별자가 둘 이상이 필요한 경우 추가 항목은 별칭으로 간주될 수 있습니다.
네이밍
컨벤션은 보통 카멜 케이스를 따르며, Spring AOP를 사용하면 더 쉽게 만들 수 있습니다.
XML 기반
XML 기반 설정 정보에서는 id 속성와 name 속성을 사용하여 빈 식별자를 지정합니다.
보통 이름은 String타입으로 표기되며 또 다른 별칭(Alias)을 도입하려는 경우 쉼표(,), 세미콜론(;) 또는 공백으로 구분하여 이름 속성에 지정할 수도 있습니다. (참고로 Spring 3.1 이전 버전에서는 문자를 제한하는 'xsd:ID' 타입으로 이후에는 'xsd:string' 타입으로 정의됩니다.)
추가적으로 <alias/> 요소를 사용해서 별칭을 입력할 수 있습니다.
Java 기반
디폴트는 메서드 이름이 Bean의 이름으로 설정이 되고, 또는 @Bean(name ="이름")으로 설정이 가능합니다.
2. Bean 생성 (인스턴스화)
빈은 설정 정보를 참고해 크게 두 가지 방법으로 생성됩니다.
1) 생성자
컨테이너 자체가 생성자를 호출 해 Bean을 직접 생성하는 방식입니다.
가장 일반적인 방식이고, new 키워드를 사용하는 Java 코드와 동일한 방식입니다.
2) 수정자 메서드
컨테이너가 수정자 메서드를 사용해 Bean을 생성하는 방식입니다.
생성자 방식보다는 흔하지 않은 방식입니다.
둘 중 다음과 같은 이유로 생성자를 많이 사용합니다. (Spring 개발 팀이 지지)
- 애플리케이션 종료 시점까지 불변을 유지해야 하기 때문에, 한번 쓰고 말 생성자를 추천.
- 팩토리 메서드의 경우 public으로 열기 때문에 실수의 우려가 있다!
- 팩토리 메서드보다 단위 테스트에서 누락(null) 시 컴파일 단계에서 실수를 잡아내기 쉽다.
3. Bean 조회
1) 스프링 컨테이너에 존재하는 빈 조회
Spring에서는 AppConfig.class에서 설정한 Bean만 실행되는 것이 아닙니다. 스프링에서도 기본적으로 실행되는 Bean들이 있습니다! 'getBeanDefinitionNames( )'를 사용해 스프링에 등록된 모든 빈 정보를 조회할 수 있습니다.
@Test
public void 모든_빈_출력() throws Exception{
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("클래스 명 = " + beanDefinitionName + " / 인스턴스 = " + bean);
}
}
만약 스프링 내부에서 사용하는 빈을 제외하고 내가 등록한 빈만 조회하고 싶다면, Role( ) 정보를 통해 필터링이 가능 합니다.
- ROLE_APPLICATION : 사용자가 정의한 빈.
- ROLE_INFRASTRUCTURE : 스프링 내부에서 사용하는 빈.
@Test
public void 애플리케이션_빈_출력() throws Exception{
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
//Role ROLE_APPLICATION: 직접 등록한 애플리케이션 빈
//Role ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("클래스 명 = " + beanDefinitionName + " / 인스턴스 = " + bean);
}
}
}
2) 스프링 빈 조회(1) - 일반적인 형태
@Test
public void 빈_이름과타입으로_조회() throws Exception{
MemberRepository bean1 = ac.getBean("memberRepository", MemberRepository.class);
ConnectionStrategy bean2 = ac.getBean("connectionStrategy", ConnectionStrategy.class);
Assertions.assertThat(bean1).isInstanceOf(MemberRepository.class);
Assertions.assertThat(bean2).isInstanceOf(ConnectionStrategy.class);
}
@Test
public void 빈_타입만으로_조회() throws Exception{
MemberRepository bean1 = ac.getBean(MemberRepository.class);
ConnectionStrategy bean2 = ac.getBean(ConnectionStrategy.class);
Assertions.assertThat(bean1).isInstanceOf(MemberRepository.class);
Assertions.assertThat(bean2).isInstanceOf(ConnectionStrategy.class);
}
@Test
public void 빈_구현타입으로_조회() throws Exception{
MemberRepository bean1 = ac.getBean(MemberRepositoryByDB.class);
ConnectionStrategy bean2 = ac.getBean(MySQLConnection.class);
Assertions.assertThat(bean1).isInstanceOf(MemberRepository.class);
Assertions.assertThat(bean2).isInstanceOf(MySQLConnection.class);
}
@Test
public void 조회대상이_없는_경우() throws Exception{
assertThrows(NoSuchBeanDefinitionException.class, () -> ac.getBean("없는 이름", MemberRepository.class));
}
(1) 이름과 타입을 사용한 검색
Bean을 등록하면 디폴트 값이 메서드 이름이라고 했습니다. (@Bean(name = " ")으로 따로 설정 가능)
빈은 메서드 'getBean("Bean이름", 오브젝트 타입)'으로 검색이 가능합니다.
(2) 타입만으로 검색하는 경우
'getBean(오브젝트 타입)'으로 오버로드 되어 있습니다!
(3) 구현 클래스 타입으로 조회
구현 클래스는 다형성으로 인해 인터페이스와 같은 타입이기 때문에, 구현 클래스만으로도 검색이 가능합니다.
(4) 조회 대상이 없는 경우
조회 대상이 없는 경우 NoSuchBeanDefinitionException을 반환합니다.
3) 스프링 빈 조회(2) - 동일한 타입이 둘 이상인 경우
동일한 타입의 빈이 두 개 이상 있는 경우 타입 방식으로만 검색을 시도하면, Spring에서 어떤 빈을 가져와야 하는지 알 길이 없기 때문에 오류가 발생합니다. (NoUniqueBeanDefinitionException)
이럴 때는 빈의 이름을 명시적으로 입력해 주어야 합니다.
동일한 타입에 어떤 빈 이름이 있는지 모르겠다면 getBeansOfType( )를 통해 동일 타입의 모든 빈을 조회합시다!
@Test
public void 같은타입에_여러빈_이름() throws Exception{
assertThrows(NoUniqueBeanDefinitionException.class, () -> sameAc.getBean(MemberRepository.class));
}
@Test
public void 여러빈이있는경우_이름을명시적으로() throws Exception{
Map<String, MemberRepository> beansOfType = sameAc.getBeansOfType(MemberRepository.class);
// 해당 타입의 모든 빈 검색
for (String s : beansOfType.keySet()) {
System.out.println("s = " + s);
}
// 명시적으로 이름 입력
MemberRepository bean1 = sameAc.getBean("memberRepository1", MemberRepository.class);
MemberRepository bean2 = sameAc.getBean("memberRepository2", MemberRepository.class);
Assertions.assertThat(bean1).isNotEqualTo(bean2);
Assertions.assertThat(bean1).isInstanceOf(MemberRepository.class);
Assertions.assertThat(bean2).isInstanceOf(MemberRepository.class);
}
4) 스프링 빈 조회(3) - 상속관계일 경우
기본적으로 슈퍼 클래스를 조회하면, 서브 클래스 역시 조회를 실시합니다.
public class SuperClassBeanSearching {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanTypeConfig.class);
@Test
public void 부모타입으로조회시_자식이_둘이상이면_중복오류_발생() throws Exception{
Assertions.assertThrows(NoUniqueBeanDefinitionException.class,
() -> ac.getBean(MemberRepository.class));
}
@Test
public void 중복의_경우_빈_이름을_명시해야_한다() throws Exception{
MemberRepository bean1 = ac.getBean("memberRepository1", MemberRepository.class);
MemberRepository bean2 = ac.getBean("memberRepository2", MemberRepository.class);
assertThat(bean1).isInstanceOf(MemberRepository.class);
assertThat(bean2).isInstanceOf(MemberRepository.class);
assertThat(bean1).isNotEqualTo(bean2);
}
// 추천하는 방법은 아니다.
@Test
public void 특정_하위타입으로_조회() throws Exception{
MemberRepositoryByDB bean = ac.getBean(MemberRepositoryByDB.class);
assertThat(bean).isInstanceOf(MemberRepositoryByDB.class);
}
@Test
public void 부모타입_전부_조회() throws Exception{
Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
for (String s : beansOfType.keySet()) {
System.out.println("타입 = " + s + " / 인스턴스 = " + beansOfType.get(s));
}
}
@Test
public void 최상위_object타입으로_검색() throws Exception{
Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
for (String s : beansOfType.keySet()) {
System.out.println("타입 = " + s + " / 인스턴스 = " + beansOfType.get(s));
}
}
}
PLUS. BeanFactory와 ApplicationContext
BeanFactory
스프링 컨테이너의 최상위 인터페이스입니다.
스프링 빈을 관리하고 조회하는 역할을 담당합니다. (이때까지 쓴 getBean( )와 대부분의 메서드들이 이 인터페이스가 제공합니다!)
ApplicationContext
ApplicationContext는 BeanFactory 기능을 상속받아 제공할 뿐 아니라, 애플리케이션을 개발하는데 필요한 기능들을 추가로 제공합니다.
BeanFactory가 생성과 관계 설정에 초점을 둔다면, ApplicationContext는 전반에 걸친 모든 구성요소의 제어 작업을 담당합니다. (스프링이 제공하는 지원기능 모두 포함하기 때문에 Spring의 핵심 컨테이너입니다.)
그렇기 때문에 Bean 처리에 대한 완전 제어가 필요한 경우가 아니라면 일반적으로는 더욱 확장된 기능을 제공하는 ApplicationContext를 권장합니다.
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
- MessageSource : 국제화 기능을 담당. (한국 => 한국어, 영어권 => 영어)
- EnviromentCapable(환경변수) : 로컬, 개발, 운영을 구분해서 처리
- ApplicationEventPublisher : 이벤트를 발생하고 구독하는 모델을 편리하게 지원
- ResourceLoader(ResourcePatternReslover의 최상위) : 파일, 클래스 패스, 외부 등에서 리소스를 편리하게 조회
예시
Annotation 처리 및 AOP 프락시와 같은 확장 컨테이너 기능의 경우에는 BeanPostProcessor 확장이 필수이기 때문에 ApplicationContext를 권장 BeanFactory의 경우 옳은 구성의 Bean이어도 감지 및 활용이 불가능합니다!
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// populate the factory with bean definitions
// 이렇게 BeanFactory의 경우 addBeanPostProcessor 인스턴스가 필요
factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());
// applicationContext는 즉시 사용 가능함!
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(META);
정리
특징 | BeanFactory | ApplicationContext |
Bean 인스턴스화 / 의존관계 설정 | 가능 | 가능 |
통합 생명주기 관리 | 불가능 | 가능 |
자동 BeanPostProcessor 등록 | 불가능 | 가능 |
국제화, 이벤트퍼블리셔 등 확장성 | 불가능 | 가능 |
4. Bean Scope
이번에는 Bean이 존재하는 범위인 Scope에 대해 알아봅시다.
종류
싱글톤
기본 스코프, Spring IoC 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 단일 Bean 스코프입니다.
프로토타입
스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다. 싱글톤 안에 여러 프로토타입 빈이 있다고 생각하시면 됩니다.
웹 관련 스코프
[web-aware Spring ApplicationContext]에서만 유효한 빈 스코프들입니다.
- request : 웹 요청의 생명주기와 같이 도는 빈 스코프입니다. (요청이 들어오고 나갈때까지 유지되는 스코프)
- session : 웹 세션의 생명주기와 같이 도는 빈 스코프입니다. (세션이 생성되고 종료될 때 까지 유지되는 스코프)
- application : 웹의 서블릿 콘텍스트 생명주기와 같이 도는 빈 스코프입니다.
- websocket : WebSocket의 생명주기와 같이 도는 빈 스코프입니다.
1) 싱글톤 스코프
이전 포스트에서 싱글톤에 대해 설명했다시피 Spring 컨테이너(Application Context)가 단 하나의 공유 인스턴스만 생성해 해당 빈과 일치하는 Id(이름)에 대한 모든 요청은 새로운 인스턴스 대신 미리 만들어 둔 공유 인스턴스를 반환하도록 관리합니다. 이를 싱글톤 레지스트리라고 했습니다
이때까지 사용한 게 모두 싱글톤이기 때문에 따로 설정 방법을 설명하지는 않겠습니다.
2) 프로토타입 스코프
프로토타입 빈은 기존의 자바의 객체 생성과 같이 매 요청마다 새로운 인스턴스를 만들어서 반환하는 것입니다.
그에 따른 의존관계 역시 새로 주입을 실시합니다.
그래서 Stateful Bean은 프로토타입 스코프를, Stateless Bean은 싱글톤 스코프를 사용해야 합니다.
설정 방법
XML
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
자바 코드
@Scope("prototype")
@Bean
MemberService memberService() {
return new MemberServiceImpl();
}
주의사항
1) 다른 스코프들과 달리 프로토타입 스코프는 빈의 전체 수명 주기를 관리하지 않습니다!
컨테이너는 프로토타입 객체를 인스턴스화(생성) + 의존관계 주입 + 초기화까지만 처리해서 클라이언트에게 전달합니다. 그렇기 때문에 초기화 콜백 메서드는 반드시 호출되지만, 소멸 콜백 메서드는 호출되지 않습니다.
2) 생성 시점 이후(초기화까지 해서 전달 이후)의 모든 생명 주기 관리는 클라이언트에서 처리를 해야 합니다.
1)과 같은 맥락의 설명입니다. Spring 컨테이너에서는 클라이언트에게 넘겨준 이상 더 이상 관여를 하지 않습니다.
3) 프로토타입은 값이 비싸기 때문에(리소스를 많이 할당) 반드시 해제(clean up)를 해야 합니다.
Spring 컨테이너가 프로토타입 스코프의 빈을 해제하도록 하려면 Custom-BeanPostProcessor를 이용해야 합니다.
※ 싱글톤-프로토타입 스코프를 함께 사용
아무 의존관계를 가지지 않는 프로토타입 스코프 빈과, 싱글톤 빈이 있을 때 프로토타입 스코프 빈을 싱글톤 스코프 빈에 의존관계를 주입하면 새로운 프로토타입 스코프를 가지는 빈이 인스턴스화 되고, 싱글톤 스코프 빈에는 의존관계가 주입됩니다. 생성된 프로토타입 인스턴스는 싱글톤 스코프 빈에 제공되는 유일한 인스턴스입니다.
만약에 여기서 더 나아가 많은 사용자들의 요청으로 싱글톤 빈에 의존관계가 주입된 프로토타입 빈을 사용한다 해봅시다. 싱글톤 빈은 단 한 번만 초기화(생성 + 의존관계 주입)되기 때문에 Spring 컨테이너가 이미 생성된 싱글톤 인스턴스를 호출할 뿐 새로운 프로토타입 인스턴스를 생성해 초기화하는 것은 불가능한 것이죠!
이런 케이스의 문제점은 프로토타입 빈이 Stateful 할 때 발생합니다. newBean2는 계속해서 초기화가 되지 않기 때문입니다!
우리가 진정으로 원하는 방법은 사용할 때마다 새 인스턴스가 생성되는 것입니다.
이를 해결하기 위한 방식은 많습니다. 메서드 주입(Method Injection)을 사용해도 되고, 조회 메서드 주입(Lookup Method Injection)을 사용해도 됩니다. 하지만 이 기술들은 IoC를 하지 않는 것에 기반을 두지 않고, 애플리케이션 콘텍스트 전체를 주입받는 방식이기 때문에 스프링 컨테이너에 종속적인 코드가 만들어집니다.
스프링에는 이러한 상황을 예견해서 ObjectProvider을 만들었습니다!
ObjectFactory, ObjectProvider
과거의 ObjectFactory에서 편의 기능들을 추가해서 ObjectProvider을 만들었습니다.
이 ObjectProvider.getObject( )를 통해 지정한 프로토타입 빈을 컨테이너에서 찾아 반환해주는 Dependency Lookup(DL) 기능을 얻을 수 있습니다.
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
return prototypeBean.getCount();
}
이 두 클래스들은 스프링에 의존하나, 기능이 단순해 단위 테스트를 만들거나 mock 코드를 만들기에 훨씬 간단합니다.
Provider 사용 (JSR-330)
ackage javax.inject;
public interface Provider<T> {
T get();
}
Provider의 구조는 다음과 같습니다.
//implementation 'javax.inject:javax.inject:1' gradle 추가 필수
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
return prototypeBean.getCount();
}
provider 역시 내부에서 스프링 컨테이너를 통해 빈을 찾아 반환하는 DL 방식을 따르고 있습니다.
이 역시 자바 표준이기 때문에 이식성에 장벽이 있긴 하지만 기능이 단순(get( ) 단 하나) 하기 때문에 구현이 간단합니다.
3) 웹 관련 스코프
web-aware Spring ApplicationContext 구현을 사용하는 경우(웹 환경)에만 가능합니다. 일반 Spring IoC 컨테이너 같은 경우에는 IllegalStateException이 발생합니다.
추가적으로 이 스코프들을 지원하기 위해서는 서블릿 환경에 따라 약간의 사전 설정이 필요합니다.
(Spring DispathcerServlet에 의해 처리되는 Spring Web MVC 같은 경우에는 특별한 설정이 필요 없습니다.)
생성 방법은 네 가지 모두 똑같기 때문에 Request만 짚고 넘어가겠습니다.
Request 스코프
여러 HTTP에 요청(Request)이 들어오면 어떤 요청이 남긴 로그인지 구분이 어렵습니다.
이런 경우에 Request 스코프를 많이 사용합니다.
XML 방식
<bean id="loginAction" class="com.something.LoginAction" scope="request"/>
Annotation 방식
해당 클래스에 '@RequestScope' 또는 @Scope(value = "request")으로 설정하시면 됩니다.
@RequestScope
// @Scope (value = "request")
@Component
public class LoginAction {
// ...
}
예제
@Component
@Scope(value = "request")
public class MyLogger {
private String uuid;
private String requestURL;
public void setRequestURL(String requestURL) {
this.requestURL = requestURL;
}
public void log(String message) {
System.out.println("[" + uuid + "]" + "[" + requestURL + "] " + message);
}
@PostConstruct
public void init() {
uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid + "] request scope bean create:" + this);
}
@PreDestroy
public void close() {
System.out.println("[" + uuid + "] request scope bean close:" + this);
}
}
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
스코프와 프락시
CGLIB 기반 클래스 프락시가 생성됩니다. 이렇게 생성된 프락시는 다른 빈에 미리 주입할 수 있습니다. 이 CGLIB 프록시는 public 메서드 호출만을 가로챕니다. 비공개 메서드는 대상 범위가 아니기 때문에 사용에 주의해야 합니다.
동작원리
호출마다 CGLIB 기반 클래스의 프락시가 생성됩니다. 재밌는 점은 이렇게 생성된 프락시를 다른 빈에 주입할 수 있다는 것입니다! 이렇게 생성된 프록시 객체들은 요청이 들어왔을 때 진짜 빈을 요청하는 위임 로직이 들어있습니다.
(지연 처리)
- logic( ) 메서드를 호출 = 프록시 객체의 logic( ) 호출.
- 이 프락시에서 request 스코프의 진짜 빈의 logic( ) 호출
이 프락시는 원본 클래스(진짜 빈)를 상속받아 만들어졌기 때문에 다형성으로 인해 동일하게 사용할 수 있습니다.
생성
XML
<bean id="userPreferences" class="com.stuff.DefaultUserPreferences" scope="session">
<aop:scoped-proxy proxy-target-class="false"/>
</bean>
<bean id="userManager" class="com.stuff.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
<aop:scoped-proxy/>로 마크되어 있고 클래스의 경우 proxy-target-class를, 인터페이스인 경우 proxy-interfaces를 사용합시다.
자바 코드
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
@Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES)
public interface MyLogger {
}
@Scope의 proxyMode 속성을 통해 생성할 수 있습니다.
클래스의 경우 TARGET_CASS를, 인터페이스의 경우 INTERFACES를 사용하시면 됩니다.
5. Bean LifeCycle(생명주기)
앞서 설명했듯이 스프링 빈의 이벤트 생명주기는 다음과 같습니다.
[스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 사용 -> 스프링 종료]
스프링 내부적으로는 문제없지만 만약 개발자가 정확한 초기화 시점을 알기 위해서는 어떻게 해야 할까요? 스프링에서는 초기화 및 소멸 전 콜백으로 이를 알려줍니다
[스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸 전 콜백 -> 스프링 종료]
내부적으로 Spring에서는 BeanPostProcess 구현을 사용해 모든 콜백 인터페이스를 처리하고 있습니다. Spring이 제공하지 않는 사용자 정의 기능과 같은 경우에는 이를 직접 구현해야 합니다.
이외에 Spring의 기능을 이용하고 싶다면 다음과 같은 설명을 따릅시다.
InitializingBean, DisposableBean 인터페이스
Spring에서는 InitializingBean과 DisposableBean 인터페이스를 구현해 라이프사이클을 관리합니다. InitializingBean에서는 afterPropertiesSet( )을 호출해 초기화 시 빈이 특정 작업을 하도록, DisposableBean은 destory( )를 호출해 소멸 시 빈이 특정 작업을 수행하도록 합니다!
아래의 단점으로 인해 인터페이스 방식보다 어노테이션을 주로 사용하게 됩니다.
단점
"We recommend that you do not use the InitializingBean interface, because it unnecessarily couples the code to Spring."
"We recommend that you do not use the DisposableBean callback interface, because it unnecessarily couples the code to Spring."- docs.spring.io
- Spring 전용 인터페이스이기 때문에 의존성이 강합니다. (Spring 제작자들도 별로 추천하지는 않음)
- 초기화, 소멸 메서드의 이름을 변경할 수 없습니다.
- 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없습니다.
인터페이스 이외의 설정
단점이 뚜렷한 인터페이스 방식보다는 어노테이션이나 POJO 초기화 방법을 지정하는 것을 추천합니다.
1. 메타데이터(설정 정보)에서 설정
XML이든 어노테이션이든, 자바 코드든 빈을 지정할 때부터 생명주기를 설정 가능합니다.
1) XML
XML에서는 'init-method'와 'destroy-method' 속성으로 설정이 가능합니다.
이 설정 정보를 참고해서 빈 생성과 의존관계가 주입된 후 초기화 작업이, 빈의 사용 이후 소멸 작업이 시작됩니다.
<bean id="bean이름" class="classpath" init-method="초기화 콜백 메서드명"/>
<bean id="bean이름" class="eclasspath" destroy-method="소멸 콜백 메서드명"/>
메서드는 따로 생성하면 되는데 콜백 인터페이스를 사용하지 않는 초기화 및 소멸 메서드 콜백을 작성할 때는 주로
init( ), initialize( ), dispose( ), destroy( ) 등과 같은 이름으로 작성합니다. (일관성을 위해 컨벤션을 따릅시다.)
2) 자바 코드(@Bean)
@Bean에 추가 속성 'initMethod'와 'destroyMethod'를 사용하는 방법입니다.
@Bean(initMethod = "init", destroyMethod = "dispose")
빈 생성 메서드
여기서 'destroyMethod'에 주목을 해야 합니다. 기본 값을 입력하면 추론을 의미하는 (inferred)으로 등록되어 있습니다. 이 추론 기능은 빈에서 메서드를 검색해 메서드 명이 'close'나, 'shutdown'이라면 자동으로 실행해 줍니다.
이 추론 기능을 비활성화시키고 싶다면 ""으로 공백 처리하거나, 명시적으로 메서드명을 입력해야 합니다.
2. 어노테이션 (@PostConstruct, @PreDestory)
Spring ApplicationContext내에 등록된 CommonAnnotationBeanPostProcessor클래스가 JSR-250(빈의 생명주기를 담당하는 자바 플랫폼 공통 어노테이션)를 인식하며 스프링에서 생명주기를 제어합니다. JSR-250에는 @Resource, @PostConstruct, @PreDestory가 있습니다. (@Resource는 자동 주입에 사용)
일반적으로 Spring에서 생명주기 콜백을 수신하기 위한 가장 적합한 어노테이션입니다.
사용 예시 : 다음 코드에서 캐시는 초기화 시 미리 채워지고 소멸 시 미리 지워지게 됩니다.
public class CachingMovieLister {
@PostConstruct
public void populateMovieCache() {
// populates the movie cache upon initialization...
}
@PreDestroy
public void clearMovieCache() {
// clears the movie cache upon destruction...
}
}
유일한 단점으로는 자바 표준이기 때문에 외부 라이브러리에는 적용하지 못한다는 것입니다.
외부 라이브러리를 초기화 및 종료를 해야 한다면, 설정 정보를 건드리는 @Bean을 이용합시다!
우선순위
초기화 콜백에 대한 우선순위는 다음과 같습니다.
- @PostConstruct 어노테이션이 달린 메서드
- InitializingBean 콜백 인터페이스에 의해 정의된 afterPropertiesSet( )
- 사용자가 정의한 메서드 (설정 정보에서 입력)
소멸 전 콜백에 대한 우선순위는 다음과 같습니다.
- @PreDestroy 어노테이션이 달린 메서드
- DisposableBean 콜백 인터페이스에 의해 정의된 destroy( )
- 사용자가 정의한 메서드 (설정정보에서 입력)
참고자료
Spring Reference : Core Technologies (spring.io)
Core Technologies
In the preceding scenario, using @Autowired works well and provides the desired modularity, but determining exactly where the autowired bean definitions are declared is still somewhat ambiguous. For example, as a developer looking at ServiceConfig, how do
docs.spring.io
도서 : 토비의 스프링
'SPRING' 카테고리의 다른 글
컴포넌트 스캔, 의존관계 자동 주입 (0) | 2021.10.07 |
---|---|
IoC(1) - IoC 개념 및 컨테이너 (0) | 2021.10.05 |
스프링의 등장 (0) | 2021.10.04 |