지난 번에 Spring Bean이 어떻게 생성되는지 알아보았다. 이번엔 이 빈들이 어떻게 관리되는지 알아보자.
Bean의 Lifecycle Callback
데이터베이스 커넥션 풀이나 네트워크 소켓처럼 애플리케이션 시작 시점에 피료한 연결을 미리 해두고 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면 객체의 초기화과 종료 작업이 필요하다. 스프링에서는 이런 초기화 작업과 종료 작업을 어떻게 진행하는지 확인해보자.
예제로 외부 네트워크에 연결하는 객체인 NetworkClient를 생성한다고 가정하고 진행해보자. NetworkClient는 애플리케이션 시작 시점에 connect() 메서드를 호출해서 연결하고 애플리케이션이 종료되면 disconnect()를 호출해야한다고 가정해보자.
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 disconnect() {
System.out.println("close: " + url);
}
}
@Configuration
public class NetworkConfig {
@Bean
public NetworkClient networkClient() {
NetworkClient nc = new NetworkClient();
nc.setUrl("http://hello-dev.spring");
return nc;
}
}
해당 클래스를 Bean으로 등록하고 애플리케이션을 실행하면 connect()에 null이 찍힌다. 당연하게도 빈을 등록하는 시점에서는 connect()를 호출해도 비어있는 url 필드만 있기 때문에 정상적으로 실행되지 않는다. 그렇다면 어떻게 해당 문제를 해결할 수 있을까?
스프링은 의존관계가 끝나 Bean이 사용할 수 있는 상태가 되면 콜백 메서드를 통해 초기화 시점을 알려주는 다양한 기능들을 제공한다. 그 기능들은 다음과 같다.
1. InitializingBean, DisposableBean 인터페이스
@RequiredArgsConstructor
public class NetworkClient implements InitializingBean, DisposableBean {
private String url;
public NetworkClient() {
System.out.println("생성자 호출, url = " + url);
}
public void setUrl(String url) {
this.url = url;
}
@Override
public void afterPropertiesSet() throws Exception {
this.connect();
}
@Override
public void destroy() throws Exception {
this.disconnect();
}
// 서비스 시작할 때 호출
public void connect() {
System.out.println("connect: " + url);
}
// 애플리케이션 종료 시 호출
public void disconnect() {
System.out.println("close + " + url);
}
}
Spring은 초기에 위 두 인터페이스를 활용하여 Bean의 LifeCycle을 관리하도록 지원하였다. 하지만 이 방법은 초창기에 나온 방식이기에 다음과 같은 이유로 현재는 잘 사용되지 않는다.
* 스프링 전용 인터페이스로 코드가 스프링 전용 인터페이스에 의존한다는 단점과
* 초기화, 소멸 메서드의 이름을 변경할 수 없다.
* 외부 라이브러리에 적용하기 힘들다.
2. Bean의 초기화, 소멸 메서드 지정
@Configuration
public class NetworkConfig {
@Bean(initMethod = "connect", destroyMethod = "disconnect")
public NetworkClient networkClient() {
NetworkClient nc = new NetworkClient();
nc.setUrl("http://hello-dev.spring");
return nc;
}
}
Config 파일에 @Bean으로 초기화, 소멸 메서드를 지정하는 방법이다. 메서드 이름을 자유롭게 줄 수 있고, 스프링 빈이 스프링 코드에 의존하지 않는다. 또한 Config를 활용하기 때문에 코드를 수정할 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다는 장점이 있다.
또한 기본적으로 @Bean의 destroyMethod 속성은 명시하지 않아도 빈으로 등록된 클래스의 close(), shutdown() 이라는 이름의 메서드를 자동으로 호출해준다. 이 기능을 종료 메서드 추론이라고 한다. 대부분의 외부 라이브러리들은 해당 이름의 메서드로 연결을 종료하기 때문에 직접 @Bean으로 등록한 경우에는 종료 메서드를 따로 지정해주지 않아도 잘 동작한다.
3. @PostConstruct, @PreDestroy
@RequiredArgsConstructor
public class CacheClient {
private final String url;
public NetworkClient() {
System.out.println("생성자 호출, url = " + url);
}
public void setUrl(String url) {
this.url = url;
}
// 서비스 시작할 때 호출
@PostConstruct
public void connect() {
System.out.println("connect: " + url);
}
// 애플리케이션 종료 시 호출
@PreDestroy
public void disconnect() {
System.out.println("close + " + url);
}
}
마지막으로 @PostConstruct, @PreDestroy이다. 이 두 애노테이션은 JSR-250이라는 자바 표준이다. 따라서 스프링에 종속적인 기술이 아니라 다른 컨테이너에서도 동작한다. 최신 스프링에서 가장 권장하고 있는 방식이다. 하지만 코드를 수정할 수 없는 외부 라이브러리에는 적용할 수 없다는 단점이 있다.
Bean의 Scope
위에서 Bean의 초기화와 소멸자에 대해 알아보았다. 이제는 Bean이 실제로 언제 생성되고 언제까지 컨테이너가 관리하는지를 담당하는 @Scope에 대해서 알아보자.
그동안 우리는 스프링 컨테이너의 시작과 동시에 빈이 생성되고 종료될 때까지 유지된다고 학습했다. 이것은 기본적으로 스프링 빈이 싱글톤 Scope로 생성되기 때문이다. 스프링은 다음과 같은 다양한 Scope를 지원한다.
1. 싱글톤(Singleton): 기본 스코프로 스프링 컨테이너의 시작과 함께 생성되어 종료될 때까지 유지되는 가장 넓은 범위의 Scope이다.
2. 프로토타입(prototype): 컨테이너가 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 Scope이다.
3. 웹 관련 스코프
- request: 웹 요청이 들어오고 나갈 때까지 유지되는 Scope이다.
- session: 웹 세션이 생성되고 종료될 때까지 유지되는 Scope이다.
- application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 Scope이다.
사실 싱글톤 이외에는 거의 사용되지 않지만 가끔 사용되는 프로토타입과 request 스코프까지만 알아보자.
빈 스코프는 다음과 같이 지정할 수 있다.
1. 컴포넌트 스캔
@Scope("prototype")
@Component
public class HelloBean {}
2. 수동 등록
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new PrototypeBean();
}
prototype Scope
싱글톤 스코프의 빈은 조회할 때마다 같은 인스턴스의 빈을 반환한다. 하지만 프로토타입 스코프를 조회하면 항상 새로운 인스턴스를 만들어서 반환한다.



위 그림처럼 싱글톤 스코프의 빈은 컨테이너가 올라가고 생성된 단 하나의 빈으로 모든 요청을 처리하지만 프로토타입 빈은 컨테이너에 “요청할 때마다(getBean 호출마다)” 새 인스턴스를 생성한다. 여기서 핵심은 컨테이너는 프로토타입 스코프의 빈을 생성하고 의존관계 주입, 초기화까지만 처리한다는 것이다. 이후 컨테이너는 생성된 프로토타입 빈을 관리하지 않는다. 프로토타입 빈을 관리할 책임을 해당 빈을 받은 클라이언트에게 있기 때문에 @PreDestroy와 같은 소멸자가 호출되지 않는다. 소멸자와 같은 종료 메서드의 호출도 클라이언트가 직접 해야한다.
싱글톤과 프로토타입을 같이 사용할 때 생기는 문제
여기서 싱글톤 빈과 프로토타입 빈을 같이 사용할 때 생기는 문제점이 있다. 각 호출마다 0으로 초기화된 필드를 가지도록 프로토타입 빈으로 설계된 클래스가 있다고 가정해보자. 하지만 이 프로토타입 빈을 싱글톤 스코프의 서비스 클래스가 의존하고 있다면 문제가 발생한다. 프로토타입 빈을 의존하고 있는 싱글톤 빈은 컨테이너가 시작될 때 해당 프로토타입 빈을 주입받아 가지고 있을 것이다. 그리고 해당 빈을 주입받은 싱글톤 빈은 컨테이너가 내려갈 때까지 소멸되지 않는다. 그 얘기는 주입받은 프로토타입 빈도 계속 살아있다는 의미가 된다.



해당 문제를 해결하기 위해서는 로직이 호출될 때마다 컨테이너에 새로운 빈을 요청하면 된다. 사실 스프링의 ApplicatoinContext를 직접 주입받아 로직이 호출되면 컨테이너에서 getBean(원하는 Bean)을 하는 방법도 있다. 하지만 이 방법은 컨테이너에 종속적인 코드가 되고 단위 테스트도 어려워진다. 우리는 컨테이너에서 필요한 의존성을 찾는(Dependency Lookup) 수준의 기능만 필요하다. Provider가 그 역할을 수행해준다.
Provider에도 다음과 같은 종류가 있다.
1. ObjectFactory, ObjectProvider
이 두 방법은 모두 스프링에서만 사용 가능하다. ObjectFactory는 ObjectProvider가 나오기 이전 버전이다. ObjectFactory에 여러 편의 기능이 추가되어서 나온 것이 ObjectProvider이다.

2. Provider
두 번째는 JSR-330 자바 표준인 Provider를 사용하는 것이다. 이 방법을 사용하려면 다음 라이브러리를 gradle에 추가해야 한다.
Spring boot 3.0 미만: javax.inject:javax.inject:1
Spring boot 3.0 이상: jakarta.inject:jakarta.inject-api:2.0.1
https://jakarta.ee/specifications/platform/8/apidocs/javax/inject/provider

간단하게 .get() 메서드를 통해서 빈을 생성할 수 있다. 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있지만 별도의 라이브러리가 필요하다.
* 추가적으로 스프링이 제공하는 메서드인 @Lookup 애노테이션을 사용하는 방법도 있지만, 이전 방법들로도 충분하고 고려해야할 내용들이 많아 제외했다.
request Scope
request Scope는 지금까지 학습했던 Scope들과는 다르게 웹 환경에서만 동작하고, 컨테이너에 요청하는 것이 아니라 HTTP 요청이 들어오고 끝날 때까지 관리하는 Scope이다. 매 HTTP 요청마다 새로운 빈 인스턴스가 생성된다.

싱글톤 빈과 request Scope를 함께 사용할 때 주의해야할 점
위에서는 프로토타입 빈과 싱글톤 빈을 함께 사용했을 때 생기는 문제점에 대해서 알아보았다. 싱글톤 빈은 컨텍스트가 시작함과 동시에 생성되고 끝까지 유지되기 때문에 다른 타입의 빈과 함께 사용할 때 주의해야한다. reqeust 빈과 함께 사용하는 경우에는 싱글톤 빈이 생성될 때 HTTP 요청이 없기 때문에 request 빈이 생성되지 않는다. 따라서 부팅 시점에 “request 스코프가 활성화되지 않았다”는 에러가 난다. 그래서 추가적인 해결 방법이 필요하다.
1. Provider
위에서 프로토타입 빈을 사용할 때 필요할 때만 주입받기 위해 Provider를 사용하였다. request 빈도 동일하다. 요청이 실행되는 도중에는 빈이 생성되고 그 때의 빈을 Provider로 찾아서 주입해주는 방법이다. provider는 해당 빈을 호출하는 시점까지 실제 빈의 생성 시점을 늦출 수 있다는 것이 핵심이다.
2. Proxy
하지만 provider도 사용하기 싫은 개발자들이 proxy를 이용하여 더 간편하게 만들었다!
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestBean {}
적용 대상이 인터페이스라면 TARGET_CLASS 대신 INTERFACE를 넣어주면 된다. 이렇게 하면 가짜 프록시 클래스를 빈으로 등록하여 미리 주입해둘 수 있다.
이 방법은 이전에 @Configuration이 달린 클래스가 CGLIB이라는 바이트 코드 조작 라이브러리로 프록시 객체가 빈으로 등록된 것과 같다. 가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다. 내부에 진짜 빈을 찾는 로직이 들어있기 때문에 클라이언트가 프록시 객체의 메서드를 호출하면 프록시 객체는 그 때 원본 메서드를 호출하게 된다.
동작 정리
1. CGLIB이라는 라이브러리로 내 클래스를 상속받은 프록시 객체를 만들어서 주입
2. 프록시 객체는 요청이 들어오면 실제 빈을 요청하는 위임 로직으로 실제 빈을 만들어서 메서드 호출
3. 프록시 객체는 실제 request Scope와는 전혀 관련 없는 가짜이고 단순히 위임 로직이 있는 싱글톤처럼 동작
프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 request scope를 사용할 수 있다. 사실 provider와 proxy 모두 실제 사용 시점까지 빈의 생성을 늦추는 것이다.
'Computer Science > Spring Boot' 카테고리의 다른 글
| JPA N+1 문제 (0) | 2025.09.02 |
|---|---|
| Spring MVC와 Servlet (0) | 2025.08.29 |
| 스프링 컨테이너와 빈(Bean) - 싱글톤과 의존관계 주입 방법 (0) | 2025.08.21 |
| 스프링 컨테이너와 빈(Bean) - ApplicationContext 동작 과정 (0) | 2025.08.17 |
| 대댓글 페이지네이션 (0) | 2025.08.14 |