스코프는 번역 그대로 빈이 존재할 수 있는 범위를 뜻한다.
싱글톤프로토타입웹 관련 스코프requestSessionApplication@Scope("prototype")
@Component
public class HelloBean {}@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}프로토타입 스코프를 스프링 컨테이너에 조회하면 항상 새로운 인스턴스를 생성해서 반환



📝 스프링 컨테이너는 프로토타입 빈을
생성하고, 의존관계 주입, 초기화까지만처리
클라이언트에 빈을 반환 →
이후 스프링 컨테이너는 생성된 프로토타입 빈 관리 X
프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있음
따라서, @PreDestroy 같은 종료 메서드가 호출되지 않는다.
package hello.core.scope;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import static org.assertj.core.api.Assertions.assertThat;
public class SingletonTest {
@Test
public 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.PrototypeTest$SingletonBean@54504ecd
singletonBean2 = hello.core.scope.PrototypeTest$SingletonBean@54504ecdorg.springframework.context.annotation.AnnotationConfigApplicationContext -
Closing SingletonBean.destroy
📝 빈 초기화 메서드를 실행
같은 인스턴스의 빈을 조회
종료 메서드 정상 호출
package hello.core.scope;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import static org.assertj.core.api.Assertions.*;
public class PrototypeTest {
@Test
public void prototypeBeanFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
System.out.println("find prototypeBean1");
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
System.out.println("find 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("PrototypeBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}find prototypeBean1
PrototypeBean.init
find prototypeBean2
PrototypeBean.init
prototypeBean1 = hello.core.scope.PrototypeTest$PrototypeBean@13d4992d
prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@302f7971
org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing
📝
프로토타입 스코프 빈은 스프링 컨테이너에서빈을 조회할 때생성 및 초기화 메서드 실행
프로토타입 빈을 2번 조회 →완전히 다른 스프링 빈이 생성 →초기화도 2번 실행
프로토타입 빈은 스프링 컨테이너가생성과 의존관계 주입 그리고 초기화까지만 관여
→ 프로토타입 빈은 스프링 컨테이너가 종료될 때 @PreDestroy 같은 종료 메서드가 실행 X

프로토타입 빈을 요청한다.(해당 빈의 count 필드 값은 0)addCount() 를 호출하면서 count 필드를 +1 한다.(해당 빈의 count 필드 값은 0)addCount() 를 호출하면서 count 필드를 +1 한다.public class SingletonWithPrototypeTest1 {
@Test
void prototypeFind() {
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);
}
@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 은 싱글톤이므로 스프링 컨테이너 생성 시점에 함께 생성 + 의존관계 주입

clientBean 은 의존관계 자동 주입을 사용한다.
주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청한다.
스프링 컨테이너는 프로토타입 빈을 생성해서 clientBean에 반환
프로토타입 빈의 count 필드 값은 0
clientBean은 프로토타입 빈을 내부 필드에 보관
정확히는 참조값을 보관
클라이언트 A는 clientBean을 스프링 컨테이너에 요청
싱글톤이므로 항상 같은 clientBean이 반환
클라이언트 A는 clientBean.logic() 을 호출한다.
clientBean은 prototypeBean의 addCount() 를 호출해서 프로토타입 빈의 count 증가(count= 1)
클라이언트 B는 clientBean 을 스프링 컨테이너에 요청
싱글톤이므로 항상 같은 clientBean이 반환
📝
clientBean이 내부에 가지고 있는프로토타입 빈은 이미 과거에 주입이 끝난 빈이다.
주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성된 것이다.
사용 할 때마다 새로 생성되는 것이 아니다!
클라이언트 B는 clientBean.logic() 을 호출한다.
clientBean 은 prototypeBean의 addCount()를 호출해서 프로토타입 빈의 count를 증가한다.
원래 count 값이 1이었으므로 2가 된다
package hello.core.scope;
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 javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import static org.assertj.core.api.Assertions.*;
public class SingletonWithPrototypeTest1 {
@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);
}
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");
}
}
}@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}종속적인 코드 →단위 테스트 어려움싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 된다.ObjectProvider의존관계를 외부에서 주입(DI) 받는게 아니라 직접 필요한 의존관계를 찾는 것을
Dependency Lookup (DL),의존관계 조회(탐색)이라한다.
과거
ObjectFactory에 편의 기능을 추가한 것이ObjectProvider
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
prototypeBeanProvider.getObject() 을 통해서 항상 새로운 프로토타입 빈이 생성ObjectProvider의getObject() 를 호출 → 스프링 컨테이너를 통해 해당 빈을 찾아서 반환(DL)기능이 단순 → 단위테스트, mock 코드를 만들기 용이ObjectProvider 는 지금 딱 필요한 DL 정도의 기능만 제공한다.📝
ObjectFactory: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존
ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리 등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존
javax.inject.Provider 라는 JSR-330 자바 표준을 사용하는 방법package javax.inject;
public interface Provider<T> {
T get();
}
//implementation 'javax.inject:javax.inject:1' gradle 추가 필수
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
prototypeBeanProvider.getObject() 을 통해서 항상 새로운 프로토타입 빈이 생성provider 의 get() 호출 → 스프링 컨테이너를 통해 해당 빈을 찾아서 반환(DL)Provider는 지금 딱 필요한 DL 정도의 기능만 제공한다.📝
get()메서드 하나로기능이 매우 단순
별도의 라이브러리가 필요하다.
자바 표준 →스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
스프링을 사용하다 보면 자바 표준과 스프링이 제공하는 기능이 겹칠때가 많이 있다.
대부분 스프링이 더 다양하고 편리한 기능을 제공한다.
특별히 다른 컨테이너를 사용할 일이 없다면, 스프링이 제공하는 기능을 사용하면 된다.
RequestSessionHTTP Session과 동일한 생명주기를 가지는 스코프Application서블릿 컨텍스트(ServletContext)와 동일한 생명주기를 가지는 스코프Websocket
동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다.
공통 포멧 : [UUID][requestURL] {message}
MyLogger Code
package hello.core.common;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
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);
}
}
@Scope(value = "request") 를 사용해서 request 스코프로 지정
해당 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸
생성되는 시점에 자동으로 @PostConstruct 초기화 메서드를 사용해서 uuid를 생성 및 저장
이 빈은 HTTP 요청 당 하나씩 생성 → uuid를 저장해두면 다른 HTTP 요청과 구분 가능
이 빈이 소멸되는 시점에 @PreDestroy를 사용해서 종료 메시지를 남긴다.
requestURL 은 이 빈이 생성되는 시점에는 알 수 없으므로, 외부에서 setter로 입력 받는다.
LogDemoController Code
package hello.core.web;
import hello.core.common.MyLogger;
import hello.core.logdemo.LogDemoService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
@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";
}
}
HttpServletRequest를 통해서 요청 URL을 받았다.
requestURL 값을 myLogger에 저장해둔다.
myLogger는 HTTP 요청 당 각각 구분되므로 다른 HTTP 요청 때문에 값이 섞일 걱정 X
컨트롤러에서 controller test라는 로그를 남긴다.
LogDemoService Code
package hello.core.logdemo;
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);
}
}
웹과 관련된 부분은 컨트롤러까지만 사용해야 한다.
서비스 계층은 웹 기술에 종속되지 않고, 가급적 순수하게 유지 → 유지보수 관점에서 좋음
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;
But, request 스코프 빈은 아직 생성 X, 이 빈은 실제 고객의 요청이 와야 생성할 수 있다!
ObjectProvider를 사용해보자.package hello.core.web;
import hello.core.common.MyLogger;
import hello.core.logdemo.LogDemoService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
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";
}package hello.core.logdemo;
import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LogDemoService {
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 scopeObjectProvider.getObject()를 LogDemoController , LogDemoService 에서 각각 한번씩 따로@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
proxyMode = ScopedProxyMode.TARGET_CLASS 추가📝 MyLogger의
가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시
클래스를 다른 빈에 미리 주입해 둘 수 있다

CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입
@Scope 의 proxyMode = ScopedProxyMode.TARGET_CLASS)를 설정📝우리가 등록한
순수한 MyLogger 클래스가 아니라MyLogger$$EnhancerBySpringCGLIB이라는 클래스로 만들어진 객체가대신 등록된 것을 확인할 수 있다.
ac.getBean("myLogger", MyLogger.class) 로 조회해도 프록시 객체가 조회된다.동작 정리
CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.실제 빈을 요청하는 위임 로직이 들어있다.특징 정리
진짜 객체 조회를 꼭 필요한 시점까지 지연한다는 것원본 객체를 프록시 객체로 대체할 수 있다.다형성과 DI 컨테이너가 가진 큰 강점주의점
꼭 필요한 곳에만 최소화해서 사용하자