이전 포스팅을 통해 스프링 컨테이너와 빈에 대해서 간단하게 살펴봤다.
이번 포스팅에서는 분량 조절에 실패하여, 이전에 다루지 못한 스프링 컨테이너가 Bean을 어떻게 관리하는지를 다뤄보려고 한다 👀
스프링 컨테이너는 빈의 생성부터 소멸까지 다음과 같은 관리 기능을 제공한다.
우선 Bean을 컨테이너에 등록하는 것부터 살펴보자
Sample Code
@Configuration public class AppConfig { @Bean public MyService myService() { return new MyServiceImpl(); } }
위 코드를 통해 Bean이 어떻게 등록되는지 살펴보자.
먼저 @Configuration
어노테이션은 해당 클래스가 스프링의 설정 파일임을 나타낸다.
이는 이전 포스팅에서 살펴본 AnnotationConfigApplicationContext(AppCtx.class)
에서 매개변수로 전달했던 설정 파일과 동일하다.
@Bean
어노테이션이 붙은 메서드는 새로운 빈 객체를 생성하고 반환하는 역할을 한다.
여기서 myService() 메서드가 매번 호출될 때마다 new 연산자로 새로운 MyServiceImpl 인스턴스를 생성하는 것처럼 보이지만, @Bean
어노테이션으로 인해 스프링은 이를 싱글톤으로 관리한다.
즉, 실제로는 처음 한 번만 객체가 생성되고, 이후에는 동일한 객체가 재사용된다.
스프링은 빈 객체가 생성되고 소멸되는 시점에 특정 작업을 수행할 수 있도록 생명주기 콜백을 제공한다.
빈은 기본적으로 다음과 같은 라이프사이클을 갖는다.
빈 생명주기
객체 생성 -> 의존 설정 -> 초기화 -> 소멸
여기서 우리는 초기화와 소멸 과정을 조금 더 자세히 살펴보자
위 2가지의 인터페이스를 활용하면 개발자는 빈의 라이프사이클을 조작할 수 있다.
예를들어, 빈의 초기화 단계와 빈의 소멸 단계에서 개발자가 추가 작업을 원할 경우 위 인터페이스의 메서드를 오버라이드하여 사용하면 된다.
간단한 코드를 통해 살펴보자
우선 인터페이스를 상속받아서 클래스를 하나 구현해보자
Sample Code
public class Client implements InitializingBean, DisposableBean { private String host; public void setHost(String host) { this.host = host; } public void send() { System.out.println("Client.send() is called... " + this.host); } @Override public void afterPropertiesSet() throws Exception { System.out.println("Client.afterPropertiesSet() is called ... "); } @Override public void destroy() throws Exception { System.out.println("Client.destroy() is called ... "); } }
이전에 설명한 2개의 인터페이스를 상속받아서 초기화 단계와 소멸 단계에서 처리할 작업을 구현했다.
이제 사용하기 위해 빈으로 등록해보자
Sample Code
@Configuration public class AppCtx { @Bean public Client client() { Client client = new Client(); client.setHost("www.test.com"); return client; } ... }
마지막으로 빈을 사용해서 사용해보자
Sample Code
public class MainForClient { private static ApplicationContext ctx; public static void main(String[] args) { ctx = new AnnotationConfigApplicationContext(AppCtx.class); Client client = ctx.getBean(Client.class); client.send(); } }
Result View
생각했던 것과는 다르게 Client.destroy()
메서드가 호출되지 않음을 확인할 수 있었다.
그 이유를 찾아보니 스프링 컨테이너가 종료되지 않아서였다.
그래서 실행 코드를 다음과 같이 수정보고 결과를 살펴보자
Sample Code
public class MainForClient { private static AbstractApplicationContext ctx; public static void main(String[] args) { ctx = new AnnotationConfigApplicationContext(AppCtx.class); Client client = ctx.getBean(Client.class); client.send(); ctx.close(); } }
Result View
사진과 같이 명시적으로 컨테이너를 close해주니까 Client.destory()
까지 모두 호출되는 모습을 확인할 수 있었다.
인터페이스를 상속받아서 직접 구현하는 것이 아니라, @Bean
어노테이션에서 제공하는 기능을 사용하여 생명 주기를 조작할 수 있다.
우선 테스트를 위해 Client2를 정의해보자
Sample Code
public class Client2 { private String host; public void setHost(String host) { this.host = host; } public void send() { System.out.println("Client.send() is called... " + this.host); } public void connect() { System.out.println("Client.connect() is called... "); } public void close() { System.out.println("Client.close() is called... "); } }
마찬가지로 빈 등록 & 실행 코드를 작성하고 결과를 살펴보자
Sample Code
// 빈 등록 @Configuration public class AppCtx { @Bean(initMethod = "connect", destroyMethod = "close") public Client2 client2() { Client2 client2 = new Client2(); client2.setHost("www.test.com"); return client2; } ... }
// 실행 코드 public class MainForClient { private static AbstractApplicationContext ctx; public static void main(String[] args) { ctx = new AnnotationConfigApplicationContext(AppCtx.class); Client2 client = ctx.getBean(Client2.class); client.send(); ctx.close(); } }
Result View
위 코드에서 빈 등록과정을 살펴보면 @Bean(initMethod = "connect", destroyMethod = "close")
과 같이 선언된 모습을 확인할 수 있다.
이처럼 초기화 과정에서 호출하고 싶은 메서드명과 소멸 과정에서 호출하고 싶은 메서드명을 넘겨주면 우리가 기대한 결과를 확인할 수 있다!
스프링 컨테이너는 빈의 생성과 소멸 시점을 직접 관리한다.
왜냐하면 빈이 기본적으로 싱글톤 패턴으로 관리되고, 의존성 주입 순서와 리소스 관리의 일관성을 보장해야 하기 때문이다.
이러한 생명주기 관리 기능을 통해 데이터베이스 연결, 파일 시스템 연결, 네트워크 소켓 연결, 외부 리소스 획득 및 반납 등의 작업을 안전하고 효율적으로 처리할 수 있다는 걸 기억하자!
스프링 컨테이너는 빈의 스코프를 제어하여 빈이 생성되고 관리되는 방식을 제어할 수 있다.
스프링은 다양한 빈 스코프를 지원하는데, 주로 사용되는 두 가지 스코프에 대해 자세히 알아보자.
싱글톤은 스프링의 기본 스코프로, 별도로 명시하지 않으면 모든 빈은 싱글톤으로 관리된다.
우선 간단한 코드를 먼저 살펴보자
Sample Code
// 빈 등록 @Bean(initMethod = "connect", destroyMethod = "close") @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) // @Scope('singleton') public Client2 client2() { Client2 client2 = new Client2(); client2.setHost("www.test.com"); return client2; }
// 빈 사용 public class MainForClient { private static AbstractApplicationContext ctx; public static void main(String[] args) { ctx = new AnnotationConfigApplicationContext(AppCtx.class); Client2 client1 = ctx.getBean(Client2.class); Client2 client2 = ctx.getBean(Client2.class); System.out.println(client1 == client2); // true ctx.close(); } }
Result View
기본적으로 싱글톤 스코프를 적용한 빈이 등록되기 때문에 명시할 필요는 없지만, 빈 등록 코드에서 @Scope
어노테이션을 사용해서 스코프를 지정하고 있다.
지정 방식은 2가지로 다음과 같다.
싱글톤 스코프 적용 방법
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
@Scope('singleton')
Result View에 첨부된 사진과 같이 싱글톤이므로 true를 반환함을 확인할 수 있다.
또한, 빈이 싱글톤으로 관리되기 때문에 Client.connect()
가 오직 한번만 호출되는 모습을 볼 수 있다!
다음으로 자주 사용되는 스프링의 스코프는 프로토타입 스코프이다.
우선 예제 코드를 먼저 살펴보자
Sample Code
// 빈 등록 @Bean(initMethod = "connect", destroyMethod = "close") @Scope("prototype") public Client2 client2() { Client2 client2 = new Client2(); client2.setHost("www.test.com"); return client2; }
public class MainForClient { private static AbstractApplicationContext ctx; public static void main(String[] args) { ctx = new AnnotationConfigApplicationContext(AppCtx.class); Client2 client1 = ctx.getBean(Client2.class); Client2 client2 = ctx.getBean(Client2.class); System.out.println(client1 == client2); // false ctx.close(); } }
Result View
싱글톤 Sample Code에서 빈 등록 부분에 있는 @Scope
의 사용만 조금 달라졌다.
마찬가지로 프로토타입 스코프를 지정하는 방식은 두 가지이다.
프로토타입 스코프 적용 방법
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Scope('prototype')
실행 결과를 통해 프로토타입 스코프의 특징을 살펴보면 다음과 같다.
Result View 분석
- Client.connect()가 2번 호출됨
-> 매번 새로운 인스턴스 생성
- 두 인스턴스 비교 시 false 반환
- Client.close()가 호출되지 않음
-> 생명주기 관리의 차이점
즉, 프로토타입 스코프는 컨테이너에서 빈을 꺼내서 사용할 때마다 새로운 인스턴스를 반환함을 의미한다.
또한, 빈의 라이프사이클에서 호출되어야하는 Client.close()
가 호출되지 않음을 통해 프로토타입 스코프는 라이프 사이클을 따르지 않음을 알 수 있다.
따라서 빈 객체의 소멸을 개발자가 직접 처리해야 하며, 이는 스프링의 자동화된 빈 관리 이점을 활용할 수 없다는 것을 의미한다!
이번 포스팅에서는 스프링 컨테이너가 Bean을 어떻게 관리하는지 자세히 살펴보았다.
Bean의 등록부터 생명주기 관리, 그리고 스코프 관리까지 살펴보면서 스프링이 Bean을 얼마나 체계적으로 관리하는지를 정리하고 싶었다.
다음 포스팅에서는 의존성 주입(DI)에 대해 자세히 다뤄보도록 하겠다 👊