7. 빈 스코프
2025. 4. 15. 15:57ㆍSpring/Core
1. 빈 스코프란?
- 빈 스코프란 빈이 존재할 수 있는 범위를 뜻한다.
- 스프링은 다음와 같은 다양한 빈 스코프를 지원한다.
- 싱글톤: 디폴트 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
- 프로토타입: 스프링 컨테이너가 빈의 생성과 의존관계 주입, 초기화까지만 관여하고 더는 관리하지 않는 짧은 범위의 스코프
- 웹 관련 스코프
- request: 웹 요청이 들어오고 나갈 때까지 유지되는 스코프
- session: 웹 세션이 생성되고 종료될 때까지 유지되는 스코프
- application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
- 빈 스코프 별 용도
singleton | 대부분의 서비스, 리포지토리 등 | 재사용 + 메모리 효율 좋음 |
prototype | 매번 새로운 객체가 필요할 때 예: 사용자 요청마다 새로운 데이터 저장, 실험이나 테스트 객체 만들때 등 |
상태 분리(보호) 가능 |
- 빈 스코프 지정 방법: @Scope 애노테이션
@Scope("prototype")
@Component
public class HelloBean {}
2. 프로토타입 스코프
1) 프로토타입 스코프란
- 스프링 컨테이너가 빈의 생성과 의존관계 주입, 초기화까지만 관여하고 더는 관리하지 않는 짧은 범위의 스코프
- 클라이언트에 빈을 반환하고, 이후 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않는다.
따라서 @PreDestroy 같은 종료 메서드가 호출되지 않는다. - 프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있다.
따라서 클라이언트가 종료 메서드를 직접 호출해줘야 한다.
- 클라이언트에 빈을 반환하고, 이후 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않는다.
-
프로토타입 빈은 스프링 컨테이너에 요청할 때 마다 새로 생성된다.
- 싱글톤 스코프는 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다.
- 반면에 프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다.
- DI와 DL
대부분은 DI를 사용하는게 좋지만 프로토타입 빈처럼 매번 새로운 객체가 필요한 경우, 특정 조건에서만 의존성이 선택적으로 필요할 때, 스코프나 라이프사이클이 엇갈려서 DI로는 곤란할 때에는 DL을 사용한다.
용어 | 의미 | 예시 | 특징 |
DI (Dependency Injection) | 스프링이 의존성을 “넣어줌” | @Autowired | 자동, 편리함 |
DL (Dependency Lookup) | 개발자가 직접 “찾아씀” | getBean() | 유연함, 컨트롤 가능 |
2) 프로토타입 빈을 싱글톤 빈과 함께 사용시 문제점
- 프로토타입 빈은 매번 새로운 객체가 필요한 상황에서 사용되는데, 프로토타입 빈을 싱글톤 객체에 주입해서 사용하는 경우 프로토타입 빈을 사용하는 의도와 다르게 해당 빈이 싱글톤과 함께 상태가 유지되며 사용되는 문제점이 생긴다.
- 원래 프로토타입 빈 사용 의도: 요청시마다 새로운 객체 반환

- 싱글톤 빈에 주입 하여 사용시 작동: 싱글톤 빈에 해당 객체 주입 시점에 생성되고 이후부터는 새롭게 생성되지 않고 재사용됨

public class SingletonWithPrototypeTest {
@Test
void singletonClientUsePrototype() {
AnnotationConfigApplicationContext ac
= new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int count1 = clientBean1.logic();
assertThat(count1).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int count2 = clientBean2.logic();
assertThat(count2).isEqualTo(2);
}
@Scope("singleton")
static class ClientBean {
private final PrototypeBean prototypeBean;
@Autowired
public ClientBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic() {
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
@Scope("prototype")
static class PrototypeBean {
private int count = 0;
public void addCount() {
count++;
}
public int getCount() {
return count;
}
@PostConstruct
public void init() {
System.out.println("PrototypeBean.init " + this);
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}
3) Provider로 문제 해결
@Scope("singleton")
static class ClientBean {
@Autowired private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
- 가장 간단한 방법은 싱글톤 빈이 프로토타입을 사용할 때 마다 getBean()으로 스프링 컨테이너에 새로 요청하는 것이다
그런데 빈 내부에 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다. - 직접 필요한 의존관계를 찾는 DL 정도의 기능을 제공하는 무언가가 필요!
→ 스프링에서 제공하는 ObjectProvider와 자바 표준으로 제공하는 JSR-330 Provider - ObjectProvider
import org.springframework.beans.factory.ObjectProvider;
@Scope("singleton")
static class ClientBean {
//private final PrototypeBean prototypeBean;
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
@Autowired
public ClientBean(ObjectProvider<PrototypeBean> prototypeBeanProvider) {
//this.prototypeBean = prototypeBean;
this.prototypeBeanProvider = prototypeBeanProvider;
}
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
-
JSR-330 Provider
//jakarta.inject:jakarta.inject-api:2.0.1 라이브러리 gradle에 추가해줘야함
import jakarta.inject.Provider;
@Scope("singleton")
static class ClientBean {
@Autowired private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
4) 실무에서 프로토타입 사용
- 프로토타입 빈을 언제 사용할까? 매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용하면 된다.
그런데 실무에서 웹 애플리케이션을 개발해보면, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다. - ObjectProvider, JSR330 Provider 등은 프로토타입 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있다.
- ObjectProvider는 DL을 위한 편의 기능을 많이 제공해주고 스프링 외에 별도의 의존관계 추가가 필요 없기 때문에 편리하다.
- 거의 그럴일은 없겠지만 코드를 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야 한다면 JSR-330 Provider를 사용한다.
- 참고: 자바 표준 vs. 스프링 제공 라이브러리
- 스프링을 사용하다 보면 이 기능 뿐만 아니라 다른 기능들도 자바 표준과 스프링이 제공하는 기능이 겹칠때가 많이 있다.
대부분 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에, 특별히 다른 컨테이너를 사용할 일이 없다면, 스프링이 제공하는 기능을 사용하면 된다. - 단, 앞서 배운 @PostConstruct나 @PreDestroy 같은 자바 표준 라이브러리 처럼 딱히 스프링을 사용해야할 이유가 없을 정도로 간단한 경우에는 자바 표준을 쓰기도 한다.
- 스프링을 사용하다 보면 이 기능 뿐만 아니라 다른 기능들도 자바 표준과 스프링이 제공하는 기능이 겹칠때가 많이 있다.
3. 웹 스코프
1) 웹 스코프
- 웹 스코프는 웹 환경에서만 동작한다.
- 웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다. 따라서 종료 메서드가 호출된다.
- 웹 스코프 종류
- request: HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프
각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리된다. - session: HTTP Session과 동일한 생명주기를 가지는 스코프
- application: 서블릿 컨텍스트와 동일한 생명주기를 가지는 스코프
- websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프
- request: HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프
- request 스코프는 HTTP request 요청 당 각각 할당된다.
클라이언트로부터 request 요청이 왔을때 request 빈 인스턴스가 생성된다!

2) request 스코프에서의 오류 상황
@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; //request 스코프!
@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; //request 스코프!
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
- 로그를 출력하기 위한 MyLogger 클래스를 request 스코프로 지정
이제 이 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸된다. - 이때 해당 Controller와 Service를 통해 스프링 애플리케이션을 실행 시키면 오류가 발생한다.
"Scope 'request' is not active for the current thread" - 스프링 애플리케이션을 실행하는 시점에 싱글톤 스코프 빈은 생성해서 주입이 가능하지만,
request 스코프 빈은 아직 생성되지 않는다. 이 빈은 실제 고객의 요청이 와야 생성할 수 있다!
따라서 현재 애플리케이션 실행 시점에 스코프 빈이 아직 실행되지 않았다는 오류가 발생하는 것이다.
3) request 스코프 오류 해결 (1) - Provider
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
//private final MyLogger myLogger; //request 스코프!
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerProvider.getObject(); //추가됨
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
//private final MyLogger myLogger; //request 스코프!
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject(); //추가됨
myLogger.log("service id = " + id);
}
}
- ObjectProvider 덕분에 ObjectProvider.getObject()를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다.
- 또한 ObjectProvider.getObject() 를 호출하시는 시점에는 HTTP 요청이 진행 중이므로 request scope 빈의 생성이 되었기 때문에 getObject()를 해도 오류가 나지 않는다.
- 이 정도에서 끝내도 될 것 같지만... ObjectProvider쓰는 것도 귀찮다. 개발자들의 욕심은 끝이 없다 → 프록시 사용
4) request 스코프 오류 해결 (2) - 프록시
@Component
//스코프에 proxyMode 설정
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
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랑 Service에 provider 사용하지 않아도 더이상 오류나지 않음
- @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
적용 대상이 인터페이스가 아닌 클래스면 TARGET_CLASS를 선택
적용 대상이 인터페이스면 INTERFACES를 선택 - proxyMode를 설정하고 객체 이름을 출력해보면 EnhancerBySpringCGLIB가 붙어있다.
CGLIB라는 라이브러리가 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입했다는 것을 알 수 있다. 이 가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다. - 가짜 프록시 객체는 실제 request scope와는 관계가 없다. 그냥 가짜이고, 내부에 단순한 위임 로직만 있고, 싱글톤 처럼 동작한다. 프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 있다.
- Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다.