이 글은 스프링 [핵심원리 - 기본편]을 듣고 정리한 내용입니다
@Scope("prototype")
@Component
public class HelloBean {}
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}
1. 싱글톤 스코프의 빈을 스프링 컨테이너에 요청한다.
2. 스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환한다.
3. 이후에 스프링 컨테이너에 같은 요청이 오면, 같은 객체 인스턴스의 스프링 빈을 반환한다.
1. 프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.
2. 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.
3. 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환한다.
4. 이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환한다.
package hello.core.scope;
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);
//결과: 같음
Assertions.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");
}
}
}
package hello.core.scope;
public class PrototypeTest {
@Test
void prototypeBeanFind(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
System.out.println("find prototypeBean1");
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
System.out.println("find prototypeBean1");
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
System.out.println("prototypeBean1 = " + prototypeBean1);
System.out.println("prototypeBean2 = " + prototypeBean2);
//bean1과 bean2는 다름
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");
}
}
}
스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환한다.
하지만 싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않으므로 주의해야한다.
-> 여기서 잘 동작하지 않는다는건, 프로토타입 빈을 사용할때마다 새로 생성해서 사용하는것을 기대하지만 싱글톤 빈과 함께 사용하면 프로토타입 빈을 주입 시에만 새로 생성한다는 뜻이다.
스프링 컨테이너에 프로토타입 빈 직접 요청
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");
}
} }
package hello.core.scope;
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);
}
@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");
}
}
}
*참고
- 여러 빈에서 같은 프로토타입 빈을 주입 받으면, 주입 받는 시점에 각각 새로운 프로토타입 빈이 생성된다.
- 예를 들어, clientA, clinetB가 각각 의존관계 주입을 받으면 각각 다른 인스턴스의 프로토타입을 주입 받는다.
- clientA -> prototypeBean@x01
- clientB -> prototypeBean@x02
- 물론 사용할때마다 새로 생성되는 것은 아니다.
@Autowired
private ApplicationContext ac;
public int logic() {
//그러나 굉장히 무식한 방법임.
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
ac.getBean()
을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 볼 수 있다.ObjectProvider
: 지정한 빈을 컨테이너에서 대신 찾아주는 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)ObjectFactory
: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존ObjectProvider
: ObjectFactory
상속, 옵션, 스트림 처리 등의 편의기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존javax.inject.Provider
라는 JSR-330 자바 표준을 사용하는 방법javax.inject:javax.inject:1
라이브러리를 gradle에 추가 해야한다. @Autowired
private Provider<PrototypeBean> prototypeBeanProvider;
public int logic(){
PrototypeBean prototypeBean = prototypeBeanProvider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
provider.get()
을 통해서 항상 새로운 프로토타입 빈이 생성되는것을 확인할수 있다.provider의 get()
을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다(DL)ObjectProvider
, JSR330 Provider
등은 프로토타입 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있다.//web 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
omcat started on port(s): 8080 (http) with context path ''
Started CoreApplication in 0.914 seconds (JVM running for 1.528)
*참고
- 스프링 부트는 웹 라이브러리가 없으면
AnnotationConfigApplicationContext
을 기반으로 애플리케이션을 구동한다.- 웹 라이브러리가 추가되면 웹가 관련된 추가 설정과 환경들이 필요하므로
AnnotationConfigServletWebServerApplicationContext
를 기반으로 애플리케이션을 구동한다.
[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
package hello.core.common;
@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("");
System.out.println("["+uuid+"]"+" request scope bean close: "+this);
}
}
@Scope(value="reuqest)
를 사용하여 request 스코프로 지정.@PostConstruct
, @PreDestory
메서드 추가해서 초기화 시 uuid부여하고, 초기화 및 종료 메세지를 남긴다.package hello.core.web;
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을 받는다.requesetURL
값: http://localhost:8080/log-demo
myLogger
는 HTTP 요청 당 각각 구분되므로 다른 HTTP 요청 때문에 값이 섞이는 걱정은 안해도 된다.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
같은 웹과 관련된 정보가 웹가 관련없는 서비스 계층까지 넘어가게 된다. 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
스코프 빈은 실제 고격의 요청이 와야 생성할 수 있다!!package hello.core.web;
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider; //ObjectProvider 사용
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) throws InterruptedException {
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
Thread.sleep(1000);
logDemoService.logic("testId");
return "OK";
}
}
package hello.core.web;
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;//ObjectProvider 사용
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = "+ id);
}
}
ObjectProvider
덕분에 ObjectProvider.getObject()
를 호출하는 시점까지 request scope빈의 생성을 지연할 수 있다.ObjectProvider.getObject()
를 호출하는 시점에서는 HTTP 요청이 진행중이므로 request scope빈의 생성이 정상 처리된다.ObjectProvider.getObject()
를 LogDemoController
, LogDemoService
에서 각각 한번씩 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환된다!프록시란?
- 프록시(Proxy)란 '대신'이라는 의미를 가지고 있다.
- 프로토콜에 있어서 대리 응답 등에서 사용하는 개념이다.
- 보안상의 문제로 직접 통신을 주고 받을 수 없는 사이에서 프록시를 이용해서 중계를 하는 개념이다.
- 이렇게 중계 기능을 하는것을 프록시 서버 라고 부른다.
프록시 서버의 특징
- 프록시 서버는 클라이언트와 서버의 입장에서 볼 때 서로 반대의 역할을 하는것 처럼 보여지게 된다.
- 클라이언트가 프록시를 바라보면 '서버'처럼 동작하게 되는 거고, 서버가 프록시를 바라보면 '클라이언트'처럼 작동을 하는것과 같다.
- 프록시는 프록시 서버에 요청이 된 내용들을 '캐시'를 이용해 저장해 둔다. 이렇게 캐시로 저장을 해 두면 다시 데이터를 가져올 상황이 발생하지 않으므로 전송시간을 절약할 수 있다.
@Component
@Scope(value="request", proxyMode= ScopedProxyMode.TARGET_CLASS)
//proxy 설정, MyLogger가 class이므로 TARGET_CLASS로 설정.
//적용 대상이 인터페이스면 INTERFACE 선택
public class MyLogger {
}
package hello.core.web;
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) throws InterruptedException {
String requestURL = request.getRequestURL().toString();
System.out.println("myLogger.getClass() = " + myLogger.getClass());
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
Thread.sleep(1000);
logDemoService.logic("testId");
return "OK";
}
}
package hello.core.web;
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = "+ id);
}
}
MyLogger
클래스가 아니라 CGLIB
가 적힌 결과가 출력된다.CGLIB
라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.Provider
를 사용하던, 프록시
를 사용하던 중요한 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 것이다.