728x90
1. bean scope
- scope: 범위, bean이 존재할 수 있는 범위
1.1. scope 종류
- 싱글톤
- 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
- 프로토타입
- 스프링 컨테이너가
- 프로토타입 빈의 생성 → 의존관계 주입 → 초기화 메서드 까지 관여 (spring bean 생명 주기)
- 이후는 관리하지 않는 매우 짧은 범위의 스코프
- 종료 callback 없음
- 스프링 컨테이너가
- 웹 관련 스코프
- request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프
- session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
- application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
- 싱글톤, 프로토타입, request 정도 알면 충분
- 웬만하면 싱글톤으로 처리를 하는 것을 권장
scope default 값 : singleton - 프로토타입, request scope 무분별 사용시 , 유지 보수 어려움
1.2. scope 등록 방식
// 자동 등록
@Component
@Scope("prototype")
public class HelloBean{}
// 수동 등록
@Bean
@Scope("prototype")
public class HelloBean{}
2. 프로토타입 스코프
- summary
- prototype 빈 : client 요청과 관계 없이 호출만 하면 생성된다.
- request 빈 : client 요청이 올 때 bean이 생성된다.
- prototype은 호출을 임의로 늦추고 싶은 것이고 request bean은 규칙상 늦게 생성되는 빈을 오류 없이 인정 받고 싶다.
-> provider가 도움을 준다.
2.1. 싱글톤, 프로토타입 차이점
- 싱글톤 타입: 요청시, 이미 생성된 공용 객체 사용
- 프로토 타입: 요청시, 요청마다 객체 생성 - 항상 새로운 프로토타입 빈 생성
- 스프링 컨테이너
- 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리하고 관리 안한다.
- @preDestroy 종료 메서드 호출 안된다.
- 종료가 필요한면 client 측에서 직접 종료를 호출해야한다.
- singleton
package hello.core.scope;
@Test
public void singletonBeanFind(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
// ac에서 spring bean 등록하면서부터 이미 객체 생성됨
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(); // @preDestroy 동작
}
@Scope("singleton")
static class SingletonBean{
@PostConstruct
public void init(){ System.out.println("SingletonBean.init"); }
@PreDestroy
public void destroy(){ System.out.println("SingletonBean.destroy"); }
}
- prototype
- 프로토타입 빈은 프로토타입 빈을 조회한 클라이언트가 관리해야 한다.
- 종료 메서드에 대한 호출도 클라이언트가 직접 해야한다.
- 요청할때마다 계속 생성을 하고 싶다 = prototype을 사용하면 된다.
package hello.core.scope;
@Test
public void PrototypeBeanFind(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
// 빈을 조회해야지 작동
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
System.out.println("prototypeBean1 = " + prototypeBean1); // 다른 객체 주소
System.out.println("prototypeBean2 = " + prototypeBean2); // 다른 객체 주소
assertThat(prototypeBean1).isSameAs(prototypeBean2);
// 굳이 빈 제거 원하면 직접 method 호출
prototypeBean1.destroy();
prototypeBean2.destroy();
ac.close(); // @PreDestroy 작동 안함
}
@Scope("prototype")
static class PrototypeBean{
@PostConstruct
public void init(){ System.out.println("PrototypeBean.init"); }
@PreDestroy
public void destroy(){ System.out.println("PrototypeBean.destroy"); }
}
2.2. 싱글톤 with 프로토타입
- prototype 사용 목적 : 사용할 때마다 새로 생성해서 사용하는 것을 원한다.
- 문제
- 프로토타입만 사용하는 bean만 존재할 시 문제가 없다.
- 싱글톤 bean 안에 의존관계 주입으로 prototype bean이 들어가면 문제가 발생
- 싱글 톤에서 prototype bean 생성 후 사용 시, prototype bean의 참조값을 들고 있기 때문에 prototype bean이 싱글톤 scope와 동일하게 유지
- 물론 여러 종류의 singleton bean이 동일 prototype bean을 주입 받을 시, prototype bean은 새로 생성
- 프로토타입만 사용하는 bean만 존재할 시 문제가 없다.
- 예제 : 클라이언트 B도 count가 1이 되어야 하는데, 싱글톤 bean은 의존관계 주입까지 다 받은 상태(이 때 이미 prototype bean이 생성)에서는 다시는 객체를 생성하지 않으므로 prototype bean이 고정이 되어버리게 된다.
@Test
void singletonClientUsePrototype() {
AnnotationConfigApplicationContext ac =
new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int logic = clientBean1.logic();
assertThat(logic).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int logic1 = clientBean2.logic();
assertThat(logic1).isEqualTo(2); // 1이 아니라 2가 된다.
}
@Scope("singleton")
static class ClientBean {
private final PrototypeBean prototypeBean; // PrototypeBean DI 주입
@Autowired
public ClientBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
// singleTon에 종속된 prototype 사용 method
public int logic() {
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
@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"); }
}
}
- 해결
- DL을 통해서 해결
- 의존관계를 외부에서 주입(DI) 받는게 아니라 의존 관계 주입을 잠시 미뤄서 필요할 때, 필요한 의존관계를 찾는 것을 Dependency Lookup (DL) 의존관계 조회(탐색)이라고 한다.
- spring 의존 DL 종류
- ObjectFactory: 과거에 사용, getObject(); - 안씀
- ObjectProvider: 요즘 사용, getObject() + 부가 기능 존재
- DL을 통해서 해결
3. Provider로 문제 해결 _ prototype bean 주입 늦추기
- DL 3가지 방법
- 안좋은 방법 (내가 직접 springApplicationContext 관리)
- spring provider (Object provider,)
- java provider
- 정리
- 웬만하면 ObjectProvider 사용
- 만약(가능성 0.1%)에 다른 컨테이너에서 DL이 필요시 Provider 사용
3.1. 스프링 컨테이너 전체 주입 받은 후 DL 수행
- 스프링 컨테이너에 종속적인 코드 -> 단위 테스트 어려움
- 필요 이상의 data 받음
@Scope("singleton")
static class ClientBean {
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
3.2. ObjectProvider DL
- spring에서 제공하는 bean이다. = spring에 의존적
- 동작 원리
- 그동안 했던 방식은 실제 사용할 bean을 DI 했었다.
- 하지만 해당 bean 사용 목적은 필요할 때만 호출해서 사용하기 위함 == DI. 직접 DI 되면 안된다.
- 그래서 Provider를 대신 DI한다.
- Provider는 실제로 DI 해야될 bean이 호출 되는 code에서 getObject()를 통해서 해당 빈을 호출해주기 때문
- 결론적으로 provider가 대신 di에 위치하면서 실제 di 할 bean이 호출되는 code에 getObject()통해서 실제 bean을 호출해준다.
== DI를 늦춰준다.
- 참고
- ObjectProvider도 bean으로 등록
- DI로 실제 주입되어야하는 Bean 대신에 임시로 Provider 빈이 주입 되고 실제 사용시 Provider가 진짜 DI 해야하는 bean을 찾아서 바꿔치기 해준다.
package hello.core.scope;
public class SingletonWithPrototype2 {
@Test
@DisplayName("singleton-prototype 연결 시, ObjectProvider 사용")
void singletonClientUsePrototype() {
AnnotationConfigApplicationContext ac =
new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int logic = clientBean1.logic();
assertThat(logic).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int logic1 = clientBean2.logic();
assertThat(logic1).isEqualTo(1); // prototype이 제기능을 함. DL 작동 함.
}
@Scope("singleton")
static class ClientBean{
/* singleton-prototype 연결 시, ObjectProvider로 해결 */
// private ObjectFactory<PrototypeBean> prototypeBeanProvider; // 옛날 version
private ObjectProvider<PrototypeBean> prototypeBeanProvider; // 요즘 version
@Autowired
public ClientBean(ObjectProvider<PrototypeBean> prototypeBeanProvider) {
this.prototypeBeanProvider = prototypeBeanProvider;
}
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject(); // DL : 이 때 prototypeBean을 찾아서 반환.
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
@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"); }
}
}
3.3. java 표준 종류 : JSR-330 Provider
- 라이브러리 설치 필요
# gradle
dependencies {
# 스프링 부트 3.0 이상
jakarta.inject:jakarta.inject-api:2.0.1
# 스프링 부트 3.0 미만
javax.inject:javax.inject:1
}
- 작동 원리 동일
- ObjectProvider -> Provider
- getObject -> get();
- 장점
- 자바 표준이고, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.
- 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
- 순환참조 문제 해결, 지연 추가 시 사용
- 단점
- library 설치 필요
- get() 기능 하나만 존재
- 참고
- Provider도 bean으로 등록
- DI로 실제 주입되어야하는 Bean 대신에 임시로 Provider 빈이 주입 되고 실제 사용시 Provider가 진짜 DI 해야하는 bean을 찾아서 바꿔치기 해준다.
- 코드
package hello.core.scope;
public class SingletonWithPrototype3 {
@Test
@DisplayName("singleton-prototype 연결 시, ObjectProvider 사용")
void singletonClientUsePrototype() {
AnnotationConfigApplicationContext ac =
new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int logic = clientBean1.logic();
assertThat(logic).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int logic1 = clientBean2.logic();
assertThat(logic1).isEqualTo(1); // prototype이 제기능을 함. DL 작동 함.
}
@Scope("singleton")
static class ClientBean{
/* singleton-prototype 연결 시, Provider로 해결 */
private Provider<PrototypeBean> prototypeBeanProvider; // 요즘 version
@Autowired
public ClientBean(Provider<PrototypeBean> prototypeBeanProvider) {
this.prototypeBeanProvider = prototypeBeanProvider;
}
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.get(); // DL : 이 때 prototypeBean을 찾아서 반환.
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
@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");
}
}
}
4. 웹 스코프
- 웹 스코프 특징
- 웹 환경에서만 동작
- 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리
- 종료 메서드가 호출
- 웹 관련 스코프는 범위만 다르지 동작 원리는 동일
- 종류
- request (각각 따로 관리)
- HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프
- 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성, 관리
- session
- HTTP Session과 동일한 생명주기를 가지는 스코프
- application
- 서블릿 컨텍스트( ServletContext )와 동일한 생명주기를 가지는 스코프
- websocket
- 웹 소켓과 동일한 생명주기를 가지는 스코프
- request (각각 따로 관리)
4.1. Web library 설정
- web 관련 library 등록
# build.gradle
dependencies {
// web library
implementation 'org.springframework.boot:spring-boot-starter-web'
}
- 웹 환경 추가 : spring boot 환경 설치해서 내장 톰켓 서버 이용
- spring server 수행시 해당 log 나타남
2023-10-29T21:49:45.518+09:00 INFO 45731 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
- 참고
- 웹 라이브러리 없을 경우 springContainer 구현체: AnnotationConfigApplicationContext
- 웹 라이브러리 있을 경우 springContainer 구현체: AnnotationConfigServletWebServerApplicationContext - 웹 관련 기능 필요하기 때문
- port 중복 문제 해결
- application.properties : server.port=9090
- terminal
- ps-ef | grep tomcat - 좌상단 2번째 숫자
- kill -9 [tomcat 번호]
4.2. 직접 만든 Logger - request 요청 확인용
- My Logger: controller로부터 받은 request 값으로부터 log를 출력하는 source code
- scope를 request으로 했으므로 request 요청이 올 때부터 My Logger 객체를 생성 가능 - 수정자 주입 사용
package hello.core.common;
@Component
@Scope(value = "request") // controller 요청이 와야지만 bean을 생성할꺼임
public class MyLogger {
private String uuid;
private String requestURL;
// requestURL은 MyLogger 빈의 생성 시점을 알수 없어서 setter로 받아야한다.
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)
}
}
4.3. Controller, MyLogger 문제점
- LogDemoController
- controller가 springboot 가 run 될때 먼저 bean으로 등록 된다.
- 하지만 MyLogger는 scope가 request여서 요청이 없으면 bean으로 등록되지 않는다.
- request가 오면, MyLogger가 bean으로 등록되고 setter를 통해서 Mylogger는 의존관계 주입도 완료한다.
- 그리고 controller의 method를 실행한다.(이때 mylogger bean을 이용해서 log를 기록 할 수 있다.)
- 문제점
- springboot가 run 할 때, controller가 bean으로 등록, 의존관계 주입 실행
- 의존관계 주입시 존재해야하는 Mylogger이 존재하지 않는다. - Mylogger은 request가 있어야 생성되는 scope 범위에 존재하기 때문
- 해결 방안
- DL을 사용
- 우선은 provider로 DI 임시방편으로 이용
- client 요청이 오면 myLogger bean을 생성 -> bean에 등록 후, Provider가 Springboot container에서 해당 bean을 찾고 Controller에서 주입
- request 종료되면 해당 빈은 제거
package hello.core.web;
@RestController
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
myLogger.setRequestURL(requestURL); // MyLogger 의존관계 주입
myLogger.log("controller test");
logDemoService.logic("testId");
return "ok";
}
}
4.5. 해결 방법
4.5.1. ObjectProvider
package hello.core.web;
@RestController
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerProvider.getObject(); // DL 주입
System.out.println("myLogger = " + myLogger.getClass());
// myLogger = class hello.core.common.MyLogger
myLogger.setRequestURL(requestURL); // MyLogger 의존관계 주입
myLogger.log("controller test");
logDemoService.logic("testId");
return "ok";
}
}
package hello.core.web;
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final Provider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.get();
myLogger.log("service id = " + id);
}
}
# mylogger scope는 request기 때문에 controller-service는 동일한 log를 가지게 된다.
[23ead830-8ffa-475f-884e-74ce15416d2c] request scope bean create:hello.core.common.MyLogger@20d4145c
[23ead830-8ffa-475f-884e-74ce15416d2c][http://localhost:8080/log-demo] controller test
[23ead830-8ffa-475f-884e-74ce15416d2c][http://localhost:8080/log-demo] service id = testId
[23ead830-8ffa-475f-884e-74ce15416d2c] request scope bean close:hello.core.common.MyLogger@20d4145c
summary
- prototype 빈 : client 요청과 관계 없이 호출만 하면 생성된다.
- request 빈 : client 요청이 올 때 bean이 생성된다.
- prototype은 호출을 임의로 늦추고 싶은 것이고 request bean은 규칙상 늦게 생성되는 빈을 오류 없이 인정 받고 싶다.
-> provider가 도움을 준다.
- 작동원리
- provider 라는 bean을 대신 주입한다. provider도 빈으로 등록된다. spring에서 제공하는 bean
- 그리고 실제 DI하려는 Bean이 생성되거나 사용되는 시점에 Provider method를 이용해서 해당 Bean을 찾아서 준다.
- provider 라는 bean을 대신 주입한다. provider도 빈으로 등록된다. spring에서 제공하는 bean
- 참고
- Provider도 bean으로 등록
- DI로 실제 주입되어야하는 Bean 대신에 임시로 Provider 빈이 주입 되고 실제 사용시 Provider가 진짜 DI 해야하는 bean을 찾아서 바꿔치기 해준다.
4.5.2. scope와 proxy
1. 사용법
- provider 쓰는 것도 귀찮아져서 새로운 걸로 발전함
- proxyMode = ScopedProxyMode.XXXX
- 적용 대상이 클래스면 TARGET_CLASS 를 선택 - public class MyLogger{}
- 적용 대상이 인터페이스면 INTERFACES 를 선택 - public Interface MyLogger{}
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) //target class
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_INTERFACE) //target interface
- 원리: MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시에 미리 주입해 둘 수 있다.
2. 코드
package hello.core.common;
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLoggerProxy {
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(); // 유니크 id 부여
System.out.println("[" + uuid + "] request scope bean create:" + this);
}
@PreDestroy
public void destroy() {
System.out.println("[" + uuid + "] request scope bean close:" + this);
}
}
package hello.core.web;
@RestController
@RequiredArgsConstructor
public class ProxyLogDemoController {
private final ProxyLogDemoService logDemoService;
private final MyLoggerProxy myLogger;
@RequestMapping("proxy-log-demo")
public String logDemo(HttpServletRequest request) throws InterruptedException {
String requestURL = request.getRequestURL().toString();
System.out.println("myLogger = " + myLogger.getClass());
// myLogger = class hello.core.common.MyLoggerProxy$$SpringCGLIB$$0
myLogger.setRequestURL(requestURL); // MyLogger 의존관계 주입
myLogger.log("controller test");
Thread.sleep(1000); // log 섞이게 하려고 넣음
logDemoService.logic("testId");
return "ok";
}
}
package hello.core.web;
@Service
@RequiredArgsConstructor
public class ProxyLogDemoService {
private final MyLoggerProxy myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
3. 동작원리
- 웹 스코프와 프록시 동작 원리
- 스프링 컨테이너는 CGLIB 바이트코드를 조작하는 라이브러리를 사용해서, MyLogger를 상속받은 가짜 프록시 객체를 생성한다.
log : class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d - CGLIB로 내 클래스(=MyLogger)를 상속 받은 가짜 프록시 객체(MyLoggerCGLIB$$)를 만들어서 의존관계 주입(=DI)한다.
- 스프링 컨테이너는 CGLIB 바이트코드를 조작하는 라이브러리를 사용해서, MyLogger를 상속받은 가짜 프록시 객체를 생성한다.
- 가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
- 가짜 프록시 객체는 내부에 진짜 myLogger를 찾는 방법을 알고 있다.
- 가짜 프록시 객체는 request 스코프(MyLogger object)의 진짜 myLogger.logic() 를 호출한다.
4. 정리
- 가짜 프록시 객체는 실제 request scope와는 관계가 없다. 그냥 가짜이고, 내부에 단순한 위임 로직만 있고, 싱글톤 처럼 동작한다.
그래서 scope같은걸 고려할 필요가 없다. - Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다.
- 주의점
- Provider 덕분에 prototype, requestType의 Bean이 마치 싱글톤을 사용하는 것 같지만 각각의 scope에 맞춰서 동작하기 때문에 결국 주의해서 사용해야 한다.
- 메서드를 요청시 각각의 object가 생성된다.
- 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자, 무분별하게 사용하면 유지보수하기 어려워진다.
- test가 까다롭다.
이전 발행글 : 스프링 핵심 원리 이해 7 - bean 생명주기 callback
출처: 인프런 스프링 핵심 원리 - 기본편