빈 스코프란?
스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어 스프링 컨테이너가 종료될때 까지 유지된다.
이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다.
스프링은 다음과 같은 다양한 스코프를 지원한다.
- 싱글톤 : 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
- 프로토타입 : 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.
- 웹 관련 스코프
- request : 웹 요청이 들어오고 나갈때 까지 유지되는 스코프이다.
- session : 웹 세선이 생성되고 종료될때까지 유지되는 스코프이다.
- application : 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프이다.
싱글톤 스코프
- 싱글톤 스코프의 빈을 스프링컨테이너에 요청한다.
- 스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환한다.
- 이후에 스프링 컨테이넝에 같은 요청이와도 같은 객체 인스턴스 빈을 반환한다.
▶ 기존에 알고있던 싱글톤 방식이다.
Test
public class SingletonTest {
@Test
void singletonBeanTest() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonTest.class);
SingletonTest singletonBean1 = ac.getBean(SingletonTest.class);
SingletonTest singletonBean2 = ac.getBean(SingletonTest.class);
System.out.println("singletonBean1 = " + singletonBean1);
System.out.println("singletonBean2 = " + singletonBean2);
Assertions.assertThat(singletonBean1).isSameAs(singletonBean2);
}
@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@5884a914
singletonBean2 = hello.core.scope.SingletonTest$SingletonBean@5884a914
20:29:08.921 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@6c80d78a, started on Thu Mar 30 20:29:08 KST 2023
SingletonBean.destroy
▶ 초기화 실행 및 싱글톤 빈생성, 종료 까지 실행이된다!
프로토타입 빈 스코프
1. 프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.
2. 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.
▶ 요청시 마다 새로운 빈을 생성한다.
3. 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에게 반환한다.
4. 이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환한다.
▶ 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다. 클라이언트에게 빈을 반환하고, 이후 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않는다. 프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에게 있다. 그래서 '@PreDestory'같은 종료메서드가 호출되지 않는다.
Test
public class PrototypeTest {
@Test
void prototypeBeanTest() {
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);
Assertions.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");
}
}
}
결과
PrototypeBean.init
PrototypeBean.init
PrototypeBean1 = hello.core.scope.PrototypeTest$PrototypeBean@5884a914
PrototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@50378a4
20:33:50.557 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@6c80d78a, started on Thu Mar 30 20:33:50 KST 2023
▶ 개별 다른 빈이 생성, destory 종료 되지않는다.
종료를 해야할 경우
prototypeBean1.destroy();
prototypeBean2.destroy();
직접 수동 종료시켜야한다.
웹스코프
웹 스코프의 특징
- 웹 스코프는 웹 환경에서만 동작한다.
- 웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다. 따라서 종료메서드가 호출된다.
웹 스코프의 종류
- request : HTTP 요청 하나가 들어오고 나갈때까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다.
- session : HTTP Session과 동일한 생명주기를 가지는 스코프
- application : 서블릿 컨텍스트(ServletContext)와 동일한 생명주기를 가지는 스코프
- websocket : 웹 소켓과 동일한 생명주기를 가지는 스코프
request 스코프 예제 만들기
웹 환경추가
- 웹스코프는 웹 환경에서만 동작하므로 web 환경이 동작하도록 라이브러리를 추가해야한다.
implementation 'org.springframework.boot:spring-boot-starter-web'
▶ spring-boot-starter-web 라이브러리를 추가하면 스프링 부트는 내장 톰캣 서버를 활용해서 웹 서버와 스프링을 함께 실행시킨다.
2023-04-01 20:27:48.032 INFO 4488 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2023-04-01 20:27:48.041 INFO 4488 --- [ main] hello.core.CoreApplication : Started CoreApplication in 2.977 seconds (JVM running for 3.74)
request 스코프 예제 개발
동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다.
이럴때 사용하기 딱 좋은것이 바로 request 스코프이다.
ex)
공통포맷 : [UUID][requestURL]{message}
UUID를 사용해서 HTTP 요청을 구분하자.
requestURL 정보도 추가로 넣어서 어떤 URL을 요청해서 남은 로그인지 확인하자.
MyLogger
@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 클래스이다.
- @Scope(value = "request")를 사용해서 request 스코프로 지정했다. 이제 이 빈은 HTTP 요청 당 하나씩 생성되고 HTTP요청이 끝나는 시점에 소멸된다.
- 이빈이 생성되는 시점에 자동으로 @PostConstruct 초기화 메서드를 사용해서 uuid를 생성해서 저장한다.
- 이 빈은 HTTP요청 당 하나씩 생성되므로, uuid를 저장해두면 다른 HTTP요청과 구분할 수 있다.
- 이빈이 소멸되는 시점에 @PreDestroy를 사용해서 종료 메서드를 남긴다.
- requestURL은 이빈이 생성되는 시점에는 알 수 없으므로, 외부에서 setter로 입력 받는다.
LogDemoController
@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 값 : http://localhost:8080/log-demo - 이렇게 받은 requestURL 값을 myLogger에 저장해둔다. myLogger는 HTTP 요청당 각각 구분되므로 다른 HTTP 요청때문에 갑싱 섞이는 걱정은 하지 않아도 된다.
- 컨트롤러에서 controller test라는 로그를 남긴다.
LogDemoService
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
- 비즈니스 로직이 있는 서비스 계층에서도 로그를 출력해보자.
- 여기서 중요한점이 있다. request scope를 사용하지 않고 파라미터로 이 모든 정보를 서비스 계층에 넘긴다면, 파라미터가 많아서 지저분해진다. 더 문제는 requestURL같은 웹과 관련된 정보가 웹과 관련이 없는 서비스 계층까지 넘어가게 된다. 웹과 관련된 부분은 컨트롤러까지만 사용해야 한다. 서비스 계층은 웹 기술에 종속되지 않고, 가급적 순수하게 유지하는 것이 유지보수 관점에서 좋다.
- reqeust scope의 MyLogger 덕분에 이런 부분을 파라미터로 넘기지않고, MyLogger의 멤버변수에 저장해서 코드와
계층을깔끔히 유지 할 수 있다.
결과
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;
스프링 애플리케이션을 실행 시키면 오류가 발생한다. 스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, reqeust 스코프빈은 아직 생성되지않는다. 이 빈은 실제 고객의 요청이 와야 생성 할 수 있다.
ObjectProvider
- objectProvider : 지정된 빈을 컨테이너에 대신 찾아주는 DL 서비스를 제공
LogDemoController
@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";
}
}
LogDemoService
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " + id);
}
}
결과
2023-04-01 21:07:19.560 INFO 9328 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2023-04-01 21:07:19.572 INFO 9328 --- [ main] hello.core.CoreApplication : Started CoreApplication in 3.283 seconds (JVM running for 3.948)
URL요청
[9042288c-7f5d-4f15-8f64-823e23087d3e] request scope bean create : hello.core.common.MyLogger@31f135a
[9042288c-7f5d-4f15-8f64-823e23087d3e][http://localhost:8080/log-demo] controller test
[9042288c-7f5d-4f15-8f64-823e23087d3e][http://localhost:8080/log-demo] service id = testId
[9042288c-7f5d-4f15-8f64-823e23087d3e] request scope bean close : hello.core.common.MyLogger@31f135a
재요청
[ec91e362-6a53-41d8-ace1-01820edd9a61] request scope bean create : hello.core.common.MyLogger@6ef1316a
[ec91e362-6a53-41d8-ace1-01820edd9a61][http://localhost:8080/log-demo] controller test
[ec91e362-6a53-41d8-ace1-01820edd9a61][http://localhost:8080/log-demo] service id = testId
[ec91e362-6a53-41d8-ace1-01820edd9a61] request scope bean close : hello.core.common.MyLogger@6ef1316a
- ObjectProvider 덕분에 ObjectProvider.getObject()를 호출하는 시점까지 request scope 빈의 생성을 지연 할 수 있다.
- ObjectProvider.getObject()를 호출하는 시점에는 HTTP 요청이 진행중이므로 request scope 빈의 생성이 정상처리된다.
- ObjectProvider.getObject()를 LogDemoController, LogDemoService에서 각각 한번씩 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환된다.
스코프와 프록시
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
- 여기가 핵심이다. proxyMode = ScopedProxyMode.TARGET_CLASS)를 추가한다.
- 적용대상이 클래스면 TARGET_CLASS, 인터페이스라면 INTERFACES를선택
- 이렇게 하면 MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관없이 가짜 프록시 클래스를 다른 빈에 미리 주입 해 둘수있다.
LogDemoController
@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();
System.out.println("myLogger = " + myLogger.getClass());
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "Ok";
}
}
LogDemoService
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
결과
myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$d9406574
[94e7cfeb-3a75-489b-83a3-bc57914371c0] request scope bean create : hello.core.common.MyLogger@6baea9cf
[94e7cfeb-3a75-489b-83a3-bc57914371c0][http://localhost:8080/log-demo] controller test
[94e7cfeb-3a75-489b-83a3-bc57914371c0][http://localhost:8080/log-demo] service id = testId
[94e7cfeb-3a75-489b-83a3-bc57914371c0] request scope bean close : hello.core.common.MyLogger@6baea9cf
CGLIB이라는 라이브러리로 내클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.
- @Scope의 proxyMode = ScopedProxyMode.TARGET_CLASS)를 설정하면 스프링 컨테이너는 CGLIB라는 바이트코드를 조작하는 라이브러리를 사용해서 MyLogger를 상속받은 가짜 프록시 객체를 생성한다.
- 결과를 확인해보면 등록한 순수한 MyLogger 클래스가 아니라 MyLogger$$EnhancerBySpringCGLIB$$이라는 클래스로 만들어진 객체가 대신 등록된 것을 확인 할 수 있다.
- 그리고 스프링 컨테이너에 myLogger라는 이름으로 진짜 대신에 이 가짜 프록시 객체를 등록한다.
- ac.getBean(MyLogger.class)로 조회해도 프록시 객체가 조회되는 것을 확인 할 수 있다.
- 그래서 의존관계 주입도 이 가짜 프록시 객체가 주입된다.
가짜 프록시 객체는 요청이 오면 그떄 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
- 클라이언트가 myLogger.logic()을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이다.
- 가짜 프록시 객체는 request스코프의 진짜 myLogger.logic()을 호출한다.
- 가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 원본인지 아닌지 모르게 동일하게 사용할 수 있다.(다형성)
동작정리
- CGLIB라는 라이브러리로 내 클래스를 상속받은 가짜 프록시 객체를 만들어서 주입한다.
- 이 가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다.
- 가짜 프록시 객체는 실제 request scope와는 관꼐가 없다. 내부에는 단순한 위임 로직만 있고, 싱글톤 처럼 동작한다.
특정정리
- 프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용 할 수 있다.
- 사실 Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다.
- 단지 어노테이션 설정 변경만으로 우너본 객체를 프록시 객체로 대체 할 수 있다. 이것이 바로 다형성과 DI 컨테이너가 가진 가장 큰 강점이다.
- 꼭 웹 스코프가 아니어도 프록시는 사용 할 수 있다.
주의점
- 마치 싱글톤을 사용하는 것 같지만 다르게 동작하기 때문에 격국 주의해서 사용해야한다.
- 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자, 무분별하게 사용하면 유지보수가 어렵다.