인프런 김영한 강사님의 스프링 핵심 원리 - 기본편 sec09
출처 : 스프링 핵심원리 - 기본편
스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어 컨테이너 종료까지 유지
-> why? 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문
스코프 : 빈이 존재할 수 있는 범위
@Scope("스코프명")
기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
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.SingletonTest$SingletonBean@192f2f27
singletonBean2 = hello.core.scope.SingletonTest$SingletonBean@192f2f27
11:48:34.799 [Test worker] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@75cd8043, started on Thu Jan 26 11:48:34 KST 2023
SingletonBean.destroy
*/
스프링 컨테이너는 프로토타입의 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프
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@192f2f27
prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@8a589a2
11:52:45.317 [Test worker] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@75cd8043, started on Thu Jan 26 11:52:45 KST 2023
*/
clientBean
은 싱글톤이므로, 보통 스프링 컨테이너 생성 시점에 함께 생성되고, 의존관계 주입도 발생
1. clientBean
은 의존관계 자동 주입을 사용 => 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청
2. 스프링 컨테이너는 프로토타입 빈을 생성해서 clientBean
에 반환 => 프로토타입 빈의 count 필드값은 0
이제 clientBean
은 프로토타입 빈을 내부 필드에 보관(정확히는 참조값을 보관)
클라이언트 A는 clientBean 을 스프링 컨테이너에 요청해서 받음 => 싱글톤이므로 항상 같은 clientBean
이 반환됨
3. 클라이언트 A는 clientBean.logic()
을 호출
4. clientBean
은 prototypeBean의 addCount()
를 호출해서 프로토타입 빈의 count를 증가함 => count값이 1이 된다.
클라이언트 B는 clientBean
을 스프링 컨테이너에 요청해서 받음 => 싱글톤이므로 항상 같은 clientBean
이 반환됨
clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다. => 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지, 사용 할 때마다 새로 생성되는 것이 아니다!
5. 클라이언트 B는 clientBean.logic()
을 호출
6. clientBean 은 prototypeBean의 addCount()
를 호출해서 프로토타입 빈의 count를 증가함 => 원래 count 값이 1이었으므로 2가 됨
스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 되지만, 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지되는 것이 문제점!
항상 새로운 프로토타입 빈을 생성하는 방법
싱글톤 빈이 프로토타입을 사용할 때마다 스프링 컨테이너에 새로 요청하는 것
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
웹 환경에서만 동작, 스프링이 해당 스코프의 종료시점까지 관리(=> 종료 메서드가 호출됨)
@Scope(value = "스코프명")
@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.getRequestURL().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " + id);
}
}
/* 결과값
[940d934f-4610-40b8-9677-97287d28527e] request scope bean create:hello.core.common.MyLogger@247e8b67
[940d934f-4610-40b8-9677-97287d28527e][http://localhost:8080/log-demo] controller test
[940d934f-4610-40b8-9677-97287d28527e][http://localhost:8080/log-demo] service id = testId
[940d934f-4610-40b8-9677-97287d28527e] request scope bean close:hello.core.common.MyLogger@247e8b67
[78709841-7309-44fb-a0bb-acdd6fd4051d] request scope bean create:hello.core.common.MyLogger@71787860
[78709841-7309-44fb-a0bb-acdd6fd4051d][http://localhost:8080/log-demo] controller test
[78709841-7309-44fb-a0bb-acdd6fd4051d][http://localhost:8080/log-demo] service id = testId
[78709841-7309-44fb-a0bb-acdd6fd4051d] request scope bean close:hello.core.common.MyLogger@71787860
*/
ObjectProvider
덕분에 ObjectProvider.getObject()
를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있음ObjectProvider.getObject()
를 호출하시는 시점에는 HTTP 요청이 진행중이므로 request scope
빈의 생성이 정상 처리됨!ObjectProvider.getObject()
를 LogDemoController
, LogDemoService
에서 각각 한번씩 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환됨!@Scope(value ="", proxyMode = ScopedProxyMode.TARGET_CLASS)
System.out.println("myLogger = " + myLogger.getClass());
// 결과
myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$78d3e2a5
CGLIB라는 라이브러리로 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입
-> 가짜 프록시 객체는 요청이 오면 내부에서 진짜 빈을 요청하는 위임 로직이 들어있음
myLogger
라는 이름으로 진짜 대신에 이 가짜 프록시 객체를 등록한함ac.getBean("myLogger", MyLogger.class)
로 조회해도 프록시 객체가 조회됨myLogger.logic()
을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이고 이때 가짜 프록시 객체는 request 스코프의 진짜 myLogger.logic()
를 호출함
- CGLIB 라이브러리로 클래스를 상속 받은 가짜 프록시 객체를 만들어 주입
- 가짜 프록시 객체는 실제 요청이 오면 내부에서 실제 빈을 요청하는 위임 로직이 있음
- 가짜 프록시 객체는 실제 scope와는 관계 X, 내부에 단순한 위임 로직만 존재, 싱글톤처럼 동작
🚨주의점