728x90
싱글톤 빈
우리가 평소에 쓰던 스프링 빈은 기본적으로 싱글톤 스코프로 생성된다.
싱글톤은 클라이언트 A, B, C가 동시에 요청하든 따로 요청하던지 간에 같은 객체를 반환한다.
@Test
void singletonBeanFind() throws Exception{
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
System.out.println("================================");
SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
System.out.println("singletonBean1 = " + singletonBean1);
System.out.println("singletonBean2 = " + singletonBean2);
assertThat(singletonBean1).isEqualTo(singletonBean2);
ac.close();
}
SingletonBean.init
================================
singletonBean1 = hello.core.scope.SingletonTest$SingletonBean@36060e
singletonBean2 = hello.core.scope.SingletonTest$SingletonBean@36060e
14:08:49.372 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@fba92d3, started on Mon Mar 21 14:08:49 KST 2022
SingletonBean.destroy
- 스프링 컨테이너가 실행될 때 초기화 메서드(PostConstruct)가 실행됨
- singletonBean1과 singletonBean2는 같은 객체
- PreDestroy 메서드까지 정상적으로 호출
프로토타입 빈
@Test
void prototypeBeanFine() throws Exception{
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
System.out.println("================================");
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
System.out.println("prototypeBean1 = " + prototypeBean1);
System.out.println("prototypeBean2 = " + prototypeBean2);
assertThat(prototypeBean1).isNotEqualTo(prototypeBean2);
ac.close();
}
================================
PrototypeBean.init
PrototypeBean.init
prototypeBean1 = hello.core.scope.SingletonTest$PrototypeBean@36060e
prototypeBean2 = hello.core.scope.SingletonTest$PrototypeBean@481ba2cf
14:12:16.065 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@fba92d3, started on Mon Mar 21 14:12:15 KST 2022
- 초기화 메서드는 빈이 "조회"될 때 실행이 됨
- 싱글톤 빈과 차이점! === 출력을 보면 알 수 있음
- PreDestroy가 실행되지 않는 이유
- 프로토타입 빈은 스프링 컨테이너가 생성, 의존관계 주입 그리고 초기화까지만 관여하고 이후는 관리하지 않음
- 따라서 스프링 컨테이너가 종료될 때 종료 PreDestro가 실행되지 않음
싱글톤 빈과 프로토타입 스코프의 빈을 요청할 경우
class ClientBean{
private final PrototypeBean prototypeBean;
public ClientBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic() {
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
- ClientBean은 싱글톤 빈, PrototypeBean은 프로토타입 빈
void singletonClientUsePrototype() throws Exception{
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);
}
- clientBean1, clientBean2는 싱글톤 빈을 사용
- 하지만 내부에는 Prototype bean이 존재함
- 이런 경우에는 싱글톤 빈을 호출해도 프로토타입 빈이 새로 생성되지 않는다
- 처음 싱글톤 빈을 생성할 때는 의존관계 주입으로 인해 프로토타입 빈이 호출됨
- 하지만 그 이후론 싱글톤 빈을 조회할 때는 생성하지 않고 그대로 가져오기 때문에 따로 의존관계 주입을 위한 프로토타입이 호출되지 않으므로 프로토타입 빈이 생성되지 않음
우리가 원하는 것은 싱글톤 빈이 조회될 때마다 새로운 프로토타입 빈이 생성되야함
이런 경우에는 어떻게 해결할 수 있을까?!!?????
Provider(ObjectFactory, ObjectProvider)
- Dependency Lookup(DL)
- 의존관계를 외부에서 주입(DI) 받는게 아니라 직접 필요한 의존관계를 찾는 것
- 의존관계 조회
- 아래 코드에선 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 기능
static class ClientBean{
// private final PrototypeBean prototypeBean;
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
PrototypeBean.inithello.core.scope.SingletonTest$PrototypeBean@6c2c1385
PrototypeBean.inithello.core.scope.SingletonTest$PrototypeBean@3daf7722
PrototypeBean.inithello.core.scope.SingletonTest$PrototypeBean@4f25b795
PrototypeBean.inithello.core.scope.SingletonTest$PrototypeBean@6e950bcf
- getObject()를 호출하면 내부에서 스프링 컨테이너를 통해 해당 빈을 찾아서 반환함
- getObject()를 두 번 호출하니 프로토타입 빈이 총 4개 생기는 것을 알 수 있음
@Test
void singletonClientUsePrototype() throws Exception{
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);
}
ObjectProvider와 ObjectFactory의 차이가 뭔가요?
ObjectProvider는 ObjectFactory를 상속받고 있습니다.
ObjectFactory는 "getObjectInstance"라는 딱 DL 기능만 가지고,
ObjectProvider는 상속, 옵션, 스트림 등 편의 기능이 많습니다.
웹 스코프
- 웹 환경에서만 동작
- 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리함. 종료 메서드가 호출됨
- request, session, application, websocket 스코프 등이 있음
- 즉, request할 때마다 각각 request 스코프가 할당된다!!
@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는 request 스코프이다
- 이제 스프링 부트를 실행하게 되면 아래와 같은 오류를 볼 수 있다.
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; nested exception is java.lang.IllegalStateException:
- 위와 같은 오류는 왜 발생할까?
- MyLogger는 "request" 스코프임. 즉, request 요청이 들어오면 생성되는 빈임
- 하지만 LogDemoController, LogDemoService에선 MyLogger 를 주입받으려 하고 있음
- 당연히 MyLogger 빈은 생성되지 않았으므로 오류가 발생하게 됨!!
어떻게 해결해야할까??
해결방법
앞서 배운 ObjectProvider를 사용해보자!!
private final ObjectProvider<MyLogger> myLoggerProvider;
[35f42c17-11f7-411f-8636-8bb8fcfb8e98] request scope bean create:hello.core.common.MyLogger@75c504f0
[35f42c17-11f7-411f-8636-8bb8fcfb8e98][http://localhost:8080/log-demo] controller test
[35f42c17-11f7-411f-8636-8bb8fcfb8e98][http://localhost:8080/log-demo] service id = testId
[35f42c17-11f7-411f-8636-8bb8fcfb8e98] request scope bean close:hello.core.common.MyLogger@75c504f0
- getObject()를 호출하는 시점까지 request 스코프 빈의 생성을 "지연"할 수 있음
- getObject()를 호출하는 시점에는 http 요청이 진행 중이므로 MyLogger 빈이 생성될 수 있음!!
프록시 방식을 사용해보자!!
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
- 앞서 사용했던 ObjectProvider는 MyLogger를 주입받는 모든 곳의 코드를 변경해야한다.
- 하지만 프록시 방식은 MyLogger 클래스에 어노테이션 하나로 해결할 수 있음
- MyLogger의 "가짜 프록시 클래스"를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입
- 앞서 "Configuration 바이트 코드 조작 마법" 단원에서 배운 "CGLIB"를 볼 수 있음
myLogger.getClass() = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$e03643d5
- 즉, myLogger 빈 대신에 "가짜 프록시 객체"를 등록. 의존관계 주입도 가짜 프록시 객체가 주입됨
- 클라이언트가 myLogger를 호출하면 "가짜 프록시 객체"의 myLogger의 메서드를 호출하고, 내부에서는 이제 "가짜 프록시 객체"가 실제 객체의 메서드를 호출함
- "가짜 프록시 객체"는 "실제 객체"를 상속받기 떄문에 가능함(다형성)
정리
- ObjectProvider, 프록시 방식 모두 myLogger, 즉, 실제 객체를 조회할 때까지 "지연"처리 함
- 어노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있음
- 다형성과 DI 컨테이너가 가진 큰 장점!!!!
출처 : 인프런 스프링 핵심 원리 - 기본편 (김영한 강사님)
728x90
'Spring 강의 > 스프링 핵심 원리 - 기본편' 카테고리의 다른 글
스프링 핵심 원리 - 빈 생명주기 콜백(2022.03.12 수정) (0) | 2021.08.09 |
---|---|
스프링 핵심 원리 - 의존관계 자동 주입(2022.03.12 수정) (0) | 2021.08.06 |
스프링 핵심 원리 - 컴포넌트 스캔 (0) | 2021.08.06 |
스프링 핵심 원리 - 싱글톤 컨테이너 (0) | 2021.08.04 |
스프링 핵심 원리 - 스프링 컨테이너와 스프링 빈 (0) | 2021.08.02 |