스코프는 번역 그대로 빈이 존재할 수 있는 범위를 뜻한다.
싱글톤
프로토타입
웹 관련 스코프
request
Session
Application
@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()
메서드 하나로기능이 매우 단순
별도의 라이브러리가 필요하다.
자바 표준 →스프링이 아닌 다른 컨테이너
에서도 사용할 수 있다.
스프링을 사용하다 보면 자바 표준과 스프링이 제공하는 기능이 겹칠때가 많이 있다.
대부분 스프링이 더 다양하고 편리한 기능을 제공한다.
특별히 다른 컨테이너를 사용할 일이 없다면, 스프링이 제공하는 기능을 사용하면 된다.
Request
Session
HTTP 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 컨테이너
가 가진 큰 강점주의점
꼭 필요한 곳에만 최소화
해서 사용하자