스프링 빈 스코프
스프링 빈의 스코프는 빈의 생명주기, 즉 빈이 생성되고 소멸되는 기간을 말한다. 스프링 프레임워크는 여러 가지 스코프를 제공하여 개발자가 다양한 상황에 맞춰 적절한 빈의 생명주기를 선택할 수 있게 도와준다.
1. 싱글톤 (Singleton) 스코프
스프링 컨테이너의 시작부터 종료될 떄까지 빈의 인스턴스를 하나만 생성하여 사용하는 스코프이다.
스프링의 대부분의 빈은 싱글톤 스코프로 설정되어 있으며, 같은 스프링 빈을 호출하면 항상 동일한 인스턴스가 반환된다.
2. 프로토 타입(ProtoType) 스코프
빈을 요청할 때마다 새로운 인스턴스를 생성하는 스코프이다.
스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입을 책임지지만, 그 이후의 라이프사이클은 스프링에서 관리하지 않는다.
3. 웹 관련 스코프
3.1 Requset 스코프 : HTTP 요청이 들어올 때 생성되어 요청이 완료될 때까지 유지되는 스코프이다.
각기 다른 사용자의 HTTP요청 마다 별도의 인스턴스가 생성된다.
3.2 Session 스코프 : HTTP 세션이 생성될 때부터 세션이 종료될 때까지 유지되는 스코프이다.
사용자 별로 세션은 별도로 생성되므로, 사용자마다 별도의 빈 인스턴스가 유지된다.
3.3 Application 스코프 : 웹 애플리케이션이 시작되고 종료될 때까지 유지되는 스코프이다.
서블릿 컨텍스트와 같은 범위를 가진다. 애플리케이션 전반에 걸쳐 공유해야하는 빈에 사용된다.
빈 스코프는 다음과 같이 지정할 수 있다.
컴포넌트 스캔 자동 등록
@Scope("prototype")
@Component
public class HelloBean {}
수동 등록
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}
프로토 타입 스코프
싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환하지만, 프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이는 항상 새로운 인스턴스를 생성해서 반환한다.
싱글톤 vs 프로토 타입
싱글톤 빈 요청

싱글톤 스코프의 빈을 스프링 컨테이너에 요청한다.
동일한 빈을 요청하면 항상 같은 객체 인스턴스의 스프링 빈을 반환한다.
프로토타입 빈 요청


- 프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.
- 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.
- 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환한다.
- 이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환한다.
프로토타입 스코프의 동작원리
스프링 컨테이너는 프로토타입 빈을 생성, 의존관계 주입 및 초기화까지만 관여한다. 이후에 반환한 빈 인스턴스의 생명주기는 스프링이 관리하지않는다. 따라서 "@Prodstory" 와 같은 종료 메서드는 프로토타입 빈에서 호출되지 않는다.
싱글톤 스코프 빈 테스트
package hello.core.scope;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import static org.assertj.core.api.Assertions.*;
public class SingletonTest {
@Test
void singletonBeanFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
System.out.println("singletonBean1 = " + singletonBean1);
System.out.println("singletonBean2 = " + singletonBean2);
assertThat(singletonBean1).isSameAs(singletonBean2);
ac.close();
}
@Scope("singleton")
static class SingletonBean{
@PostConstruct
public void init() {
System.out.println("SingletonBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("SingletonBean.destroy");
}
}
}
SingletonBean.init
singletonBean1 = hello.core.scope.SingletonTest$SingletonBean@5f9edf14
singletonBean2 = hello.core.scope.SingletonTest$SingletonBean@5f9edf14
SingletonBean.destroy
빈 초기화 메서드가 실행되며, 동일한 인스턴스를 반환한다. 스프링 컨테이너가 종료될 때 종료 메서드도 호출된다.
프로토타입 스코프 빈 테스트
package hello.core.scope;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import static org.assertj.core.api.Assertions.assertThat;
public class PrototypeTest {
@Test
void prototypeFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
System.out.println("PrototypeTest.prototypeBean1");
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
System.out.println("PrototypeTest.prototypeBean2");
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
System.out.println("prototypeBean1 = " + prototypeBean1);
System.out.println("prototypeBean2 = " + prototypeBean2);
assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
ac.close();
}
@Scope("prototype")
static class PrototypeBean {
@PostConstruct
public void init() {
System.out.println("SingletonBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("SingletonBean.destroy");
}
}
}
PrototypeTest.prototypeBean1
SingletonBean.init
PrototypeTest.prototypeBean2
SingletonBean.init
prototypeBean1 = hello.core.scope.PrototypeTest$PrototypeBean@5f9edf14
prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@68746f22
프로토타입 빈이 스프링 컨테이너에 의해 요청될 때마다 새로운 인스턴스가 생성된다. "@PreDestroy" 와 같은 종료 메서드는 호출되지 않는다.
프로토 타입 스코프 빈과 싱글톤 빈의 함께 사용시 문제점
프로토타입 스코프의 빈을 스프링 컨테이너에 요청할 때마다 새로운 인스턴스가 생성된다. 이러한 특성은 여러 클라이언트가 동일한 빈을 요청할 때, 각 클라이언트에게 새로운 객체 인스터를 제공하기 위해 사용된다.
싱글톤 스코프와 조합
하지만, 싱글톤 스코프의 빈이 프로토타입 스코피의 빈을 참조할 경우 문제가 발생할 수 있다. 싱글톤 빈은 생성 시점에 한 번만 초기화되고, 이후에는 동일한 인스턴스가 재사용된다. 따라서 싱글톤 빈이 프로토타입 빈을 의존성 주입받을 경우, 프로토타입 빈 역시 한 번만 생성되어 싱글톤 빈 내부에 저장된다.
테스트 코드
package hello.core.scope;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import static org.assertj.core.api.Assertions.assertThat;
public class SingletonWithPrototype {
@Test
void prototype() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
prototypeBean1.addCount();
assertThat(prototypeBean1.getCount()).isEqualTo(1);
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
prototypeBean2.addCount();
assertThat(prototypeBean2.getCount()).isEqualTo(1);
}
@Test
void singletonClientPrototype() {
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");
}
}
}

싱글 빈에서 프로토타입 빈을 사용하게 되면, 프로토타입 빈의 새로운 인스턴스를 매번 생성해주는 특성이 무용지물이 된다. 싱글톤 빈은 항상 동일한 프로토타입 빈 인스턴스를 참조하기 때문에,
clientBean 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다. 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성된 것이지, 사용할 때 마다 새로 생성된 것은 아니다.
스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토 빈을 사용하게 된다. 그런데 싱글톤 빈은 생성시점에만 의존관계 주입을 받기 때문에, 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지된다.
프로토 타입 빈과 싱글톤 빈의 함께 사용시 문제점 - Provider로 문제 해결
스프링의 싱글톤 빈과 프로토타입 빈을 함께 사용할 때마다 새로운 프로토타입 빈을 어떻게 생성할 수 있을까? 이 문제에는 여러 해결책이 있다.
스프링 컨테이너 요청
public class SingletonWithPrototype {
@Test
void singletonClientPrototype() {
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(1);
}
@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;
}
}
@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");
}
}
}
PrototypeBean.inithello.core.scope.SingletonWithPrototype$PrototypeBean@81d9a72
PrototypeBean.inithello.core.scope.SingletonWithPrototype$PrototypeBean@27d5a580
- 실행해보면 ac.getBean() 을 통해서 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
- 의존관계를 외부에서 주입(DI)를 받는게 아니라, 직접 필요한 의존관계를 찾는 것을 Dependucy Lookup(DL) 의존관계 조회(탐색) 이라 말한다.
- 그러나 ApplicationContext 방법으로 주입받게되면 스프링 컨테이너에 종속적이며, 단위 테스트가 어렵다는 단점이 있다.
ObjectFactory, ObjectProvider
지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 OjbectProvider 이다. 과거에는 ObjectFactroy 가 있었는데, 편의 기능을 추가해서 ObjectProvider 가 만들어졌다.
@Scope("singleton")
static class ClientBean {
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
PrototypeBean.inithello.core.scope.SingletonWithPrototype$PrototypeBean@3b5fad2d
PrototypeBean.inithello.core.scope.SingletonWithPrototype$PrototypeBean@1fb19a0
- 실행해보면 prototypeBeanProvider.getObject() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
- ObjectProvider 의 getObject() 를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)
- 스프링이 제공하는 기능을 사용하지만, 기능이 단순하므로 단위테스트를 만들거나 mock 코드 작성이 용이하다.
특징
- ObjectFactory : 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존
- ObjectProvider : ObjectFactory 상속, 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 없음
JSR - 330 Provider
'javax.inject.Provider' 는 JSR-330 이라는 자바 표준에 따라 제공되는 인터페이스이다. 이를 사용하면 스프링이 아닌 다른 컨테이너에서도 코드를 실행할 수 있게 된다. 다만 별도의 라이브러리 의존성이 추가로 필요하다.
//스프링부트 3.0 미만
javax.inject:javax.inject:1
//스프링부트 3.0 이상
implementation 'jakarta.inject:jakarta.inject-api:2.0.1'
import jakarta.inject.Provider;
@Scope("singleton")
static class ClientBean {
@Autowired
private Provider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
PrototypeBean.inithello.core.scope.SingletonWithPrototype$PrototypeBean@34b9f960
PrototypeBean.inithello.core.scope.SingletonWithPrototype$PrototypeBean@302f7971
- 실행해보면 provider.get()을 통해서 항상 새로운 프로토타입 빈이 생성된다.
- provider 의 get() 을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)
- 자바 표준이며, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기 용이하다.
특징
- get() 메서드 하나로 기능이 매우 단순하다.
- 별도의 라이브러리가 필요하다.
- 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
정리
실무에서는 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에, 프로토타입 빈을 직접 사용하는 경우는 드물다. 그러나 'Spring - ObjectProvider' 나 JSP_330 - 'Prodiver' 와 같은 기능은 DL이 필요한 경우 언제든지 활용할 수 있다.
스프링에서 제공하는 기능과 자바 표준 사이에 선택을 해야하는 경우, 특별한 이유가 없다면 스프링이 제공하는 기능을 사용하는 것이 좋다. 스프링은 다양한 편의 기능을 제공하고, 별도의 의존성을 추가할 필요가 없기 때문이다. 다만, 다른 컨테이너에서의 사용성을 고려해야하는 경우에는 자바 표준을 따르는 것이 바람직하다.
웹 스코프
웹 환경에 특화된 스코프이며, 웹 환경에서만 동작한다.
웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다. 따라서 종료 메서드가 호출된다.
웹 스코프 종류request : HTTP 요청이 들어올때 부터 응답이 나갈 떄까지의 생명 주기를 가진다. 각 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.session : HTTP session 의 생명 주기 와 동일하다. 사용자 세션마다 별도의 빈 인스턴스가 관리 된다.application : ServletContext 와 동일한 생명 주기를 가진다. Websocket : 웹 소켓 연결의 생명 주기와 동일하게 동작한다.
HTTP request 요청 당 각각 할당되는 request 스코프

Request Scope in Spring Web
웹 애플리케이션에서 다양한 HTTP 요청이 발생할 때, 각 요청을 구별하거나 특정 요청에 대한 정보를 보존하고 싶을 때, 사용되는 것이 "request scope" 이다.
1. 웹 환경 설정
웹 스코프를 사용하려면 우선 웹 환경이 동작해야 한다. 이를 위해 Build.gralde 파일에 웹 라이브러리를 추가한다.
// web 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
이제 'hello.core.CoreApplicaiton' 의 main 메서드를 실행하면 웹 애플리케이션이 동작함을 확인할 수 있다.
Tomcat started on port(s): 8080 (http) with context path ''
Started CoreApplication in 0.914 seconds (JVM running for 1.528)
내장 톰캣을 통해 웹 서버가 함께 실행된다. 웹 라이브러리를 추가하면 스프링 부트는 "AnnotationConfigServletWebServerApplicationContext"를 사용하여 애플리케이션을 실행한다.
request 스코프 예제
다음과 같이 로그가 남도록 request scope 를 활용해 추가 기능 개발
[d06b992f...] request scope bean create
[d06b992f...][http://localhost:8080/log-demo] controller test
[d06b992f...][http://localhost:8080/log-demo] service id = testId
[d06b992f...] request scope bean close
기대하는 공통 포멧 : UUID {message}
UUID(Universally Unique Identifier) : 전 세계에서 고유한 ID를 만들기 위한 표준, 이를 사용하면 각 HTTP 요청을 유일하게 구분할 수 있게된다.
requestURL 로 요청 출처 파악
사용자가 어떤 URL를 통해 요청했는지 정보 출처를 파악할 수 있다.
MyLoger 구현
package hello.core.common;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.UUID;
@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);
}
}
- 로그를 출력하기 위한 MyLogger 클래스이다.
- @Scope(value = "request") 를 사용해서 request 스코프로 지정했다. 이제 이 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸된다.
- 이 빈이 생성되는 시점에 자동으로 @PostConstruct 초기화 메서드를 사용해서 uuid를 생성해 저장한다. 이 빈은 HTTP 요청당 하나씩 생성되므로, uuid를 저장해두면 다른 HTTP 요청과 구분할 수 있다.
- 소멸되는 시점에 @PreDestroy 를 사용해서 종료 메시지를 남긴다.
- requestURL 은 이 빈이 생성되는 시점에 알 수 없으므로, 외부에서 setter로 입력 받는다.
컨트롤러 구현
package hello.core.web;
import hello.core.common.MyLogger;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@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.getRequestURI().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
- Logger가 잘 작동하는지 확인용 테스트 컨트롤러
- HttpServletRequest를 통해서 URL을 받는다. - requestURL 값 http://localhost:8090/log-demo
- 컨트롤에서 controller test라는 로그를 남긴다.
서비스 구현
package hello.core.web;
import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
- 비즈니스 로직이 있는 서비스 계층 로그 출력
- 여기서 중요한 점은 request scope 를 사용하지 않고 파라미터로 이 모든 정보를 서비스 계층에 넘긴다면 파라미터가 지저분해진다. requestURL 같은 웹 관련된 정보가 웹과 관련없는 서비스 계층까지 넘어가게된다. 웹과 관련된 부분은 컨트롤러까지만 사용해야 한다. 서비스 꼐층은 웹 기술에 종속되지 않고, 유지하는 것이 유지보수 관점에서 좋다.
- request scope의 MyLogger 덕분에 이런 부분을 파라미터로 넘기지 않고, MyLogger의 멤버변수에 저장해서 코드와 계층을 깔금하게 유지할 수 있다.
하지만 위 애플리케이션을 실행하는 시점에서 싱글톤 빈은 생성되지만, request scope Bean 은 아직 생성되지 않는다.
오류
Error creating bean with name 'myLogger': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton;
이는 실제 요청이 들어와야만 request 스코프 빈이 생성되기 때문이다.
스코프와 ObjectProvider
HTTP 요청마다 로그를 출력해야 하는 상황에서, 요청이 발생하지 않는 시점에서는 "requestscope" 빈이 생성되지 않는다. 이로 인해 애플리케이션 실행 시점에 "request scope" 빈에 의존하는 다른 빈들을 주입받을 수 없는 문제가 발생한다.
"ObjectProvider"을 활용하여 지정된 빈의 의존성 주입을 지연시킬 수 있게 도와준다. 즉, 빈을 실제로 사용하는 시점까지 생성을 지연시키는 게 가능해진다.
ObjectProvider 사용
LogDemoController
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
MyLogger myLogger = myLoggerProvider.getObject();
String requestURL = request.getRequestURI().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
LogDemoService
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " + id);
}
}
[d06b992f...] request scope bean create
[d06b992f...][http://localhost:8080/log-demo] controller test
[d06b992f...][http://localhost:8080/log-demo] service id = testId
[d06b992f...] request scope bean close
ObjectProvider 를 통해 getObject() 메서드를 호출하는 시점까지 request scope 빈의 생성을 지연시킬 수 있다. 따라서, HTTP 요청이 발생하는 시점에 "request scope" 빈이 정상적으로 생성되어 사용된다.
또한 같은 HTTP 요청 내에서는 "ObjectProvider.getObject()" 를 여러 번 호출해도 동일한 "request scope" 빈이 반환된다
스코프와 프록시
proxyMode 속성을 추가하여 웹 스코프를 편리하게 사용할 수 있다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger { ... }
여기서 "TARGET_CLASS" 는 적용 대상이 클래스일 경우, "INTERFACES" 는 적용 대상이 인터페이스일 경우 선택한다. 이렇게 설정하면 스프링은 MyLogger의 가짜 프록시 클래스를 생성하며, HTTP request와 상관없이 이 가짜 프록시 클래스를 다른 빈에 주입할 수 있게 된다.
proxyMode 사용
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
//...
}
@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.getRequestURI().toString();
System.out.println("myLogger = " + myLogger.getClass());
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);
}
}
코드는 Provider 사용 전과 동일하다.
웹 스코프와 프록시 동작 원리
위 설정으로 인해 주입되는 "myLogger" 객체의 실제 타입을 출력해보면, 이 객체가 CGLIB를 이용한 프록시 객체임을 알 수 있다.
System.out.println("myLogger = " + myLogger.getClass());
myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d
CGLIB 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 생성하여 주입한다.
- @Scope 의 proxyMode = ScopeProxyMode.TARGET_CLASS 를 설정하면 스프링 컨테이너는 바이트 코드를 조작하는 CGLIB 라이브러리를 사용해서, MyLogger를 상속받은 가짜 프록시 객체를 생성한다.
- 그리고 스프링 컨테이너에 "myLogger"라는 이름으로 진짜 객체대신 가짜 프록시 객체를 등록한다.
- ac.getBena("myLogger", MyLogger.class) 로 조회해도 프록시 객체가 조회되는 것을 확인할 수 있다.
- 그래서 의존관계 주입도 가짜 프록시 객체가 주입된다.

가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
- 가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에, 이 객체를 사용하는 클라이언트 입장에서는 동일하게 사용할 수 있다.(다형성)
동작 원리
- CGLIB 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.
- 이 가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다.
- 가짜 프록시 객체는 실제 request scope와는 관계가 없다. 내부에 단순한 위임 로직만 있고, 싱글톤 처럼 동작한다.
정리
- 프록시 객체 덕분에 클라이언트가 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 있다.
- 사실 Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다.
- 단지 어노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 이것이 바로 다형성과 DI 컨테이너가 가진 큰 강점이다.
- 꼭 Web Scope가 아니여도 Proxy를 사용할 수 있다.
주의점
- 싱글톤을 사용하는 것 같지만 다르게 동작하기 때문에 결국 주의해서 사용해야 한다.이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용해야하며, 무분멸하게 사용하면 유지보수하기 어려워진다.

출처 : 인프런 - 🔗 스프링 핵심원리 - 기본편by 우아한형제 김영한님