빈 스코프란?
기본적으로 스프링 빈은 스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료될때 까지 유지되는데, 이것은 스프링 빈이 싱글톤 스코프로 생성되기 때문입니다. 그렇다면 다른 생명주기를 가진 스코프는 어떤게 있을까요?
- 싱글톤: 기본스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
- 프로토타입 : 스프링 컨테이너가 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 짧은 범위의 스코프
- request : 웹 요청이 들어오고 나갈때 까지 유지되는 스코프
- session : 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
- application : 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
싱글톤으로 생성된 빈은 스프링 DI 컨테이너에서 여러 클라이언트의 요청이 와도 동일한 객체를 반환합니다.
반면 프로토타입으로 생성된 빈은 스프링 DI 컨테이너에서 여러 클라이언트의 요청이 와면 각각 다른 객체를 반환합니다. 또 다른 특징으로는 @PreDestory가 적용되지 않습니다. 그 이유는 아래 그림과 같이 스프링 DI 컨테이너는 프로토타입 빈 요청이 들어오면 빈을 생성하고 DI를 해준 뒤, 생성된 빈을 반환하고 관리하지 않기 때문입니다.
package mntdev.core.scope;
public class ProtorypeTest {
@Test
void protorypeBeanFind() {
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);
Assertions.assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
prototypeBean1.destory();
prototypeBean2.destory();
ac.close();
}
@Scope("prototype")
static class PrototypeBean {
@PostConstruct
private void init() {
System.out.println("PrototypeBean.init");
}
@PreDestroy
public void destory()
{
System.out.println("PrototypeBean.destory");
}
}
}
[결과]
find prototypeBean1
PrototypeBean.init
find prototypeBean2
PrototypeBean.init
prototypeBean1 = mntdev.core.scope.ProtorypeTest$PrototypeBean@545b995e
prototypeBean2 = mntdev.core.scope.ProtorypeTest$PrototypeBean@76a2ddf3
PrototypeBean.destory
PrototypeBean.destory
프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점
위 그림과 같이 addCount를 했을때 각각 1의 값을 가지려고 했지만 싱글톤 빈과 함께 사용하면 아래와 같은 문제가 발생한다.
즉 위 그림과 같이 클라이언트 B는 addCount를 했을 때, count가 2가 되는 문제점이 발생한다. 그 이유는 DI 컨테이너에서 싱글톤 빈을 생성하는 과정에서 PrototypeBean이 같이 생성되어 생명주기를 공유하기 때문이다. 그래서 다른 클라이언트에서 logic을 호출할 경우에도, prototype으로 스코프가 잡히는게 아닌, 마치 싱글톤처럼 잡혀버리는 문제가 생긴다. 이런 문제를 해결하기 위해 Provider가 있다.
@Scope("singleton")
static class ClientBean {
@Autowired
private Provider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.get();
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.destory" + this);
}
}
웹 스코프
웹 스코프는 웹 환경에서만 동작합니다. 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리합니다. 따라서 종료 메서드가 호출됩니다.
웹 스코프 종류
- request : HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 인스턴스가 생성되고 관리된다.
- session : HTTP Session과 동일한 생명주기를 가지는 스코프
- application : 서블릿 컨텍스트와 동일한 생명주기를 가지는 스코프
- websocket 웹 소켓과 동일한 생명주기를 가지는 스코프
request 스코프를 통해서 어떻게 개발을 할 수 있을지 예제를 통해서 살펴보았습니다. 바로 동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하는 예제로, request를 활용했습니다.
[예제 컨트롤러]
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
private String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURI().toString();
System.out.println("myLogger = " + myLogger.getClass());
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
[예제 서비스]
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
[빈으로 등록된 MyLogger 클래스]
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
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
private void init() {
uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid+"] request scope bean create:"+this);
}
@PreDestroy
private void close() {
System.out.println("[" + uuid+"] request scope bean close:"+this);
}
}
살펴봐야 할 부분은 빈으로 등록된 MyLogger 클래스에 추가된 어노테이션으로 다소 생소할 수 있는 부분입니다. @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) 은 scope의 범위를 request로 설정하고, proxyMode를 통해서 웹에서 요청이 안온다면 에러가 발생하게 됩니다.
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 스코프는 해당 빈이 실제 고객(클라이언트)의 요청이 와야만 생성할 수 있지만 스프링 애플리케이션이 실행되는 시점에 생성되지 않기 때문입니다.
그래서 proxyMode = ScopedProxyMode.TARGET_CLASS 를 사용하며, 이것을 사용한다면 CGLIB을 통해서 내 클래스를 상속받은 가짜 프록시 개체를 만들어서 주입하게 됩니다.
2022.03.18 김영한 스프링 핵심 원리 - 기본편 끝
저는 이미 스프링을 배웠고, 실무에서 프로젝트를 해본 경험이 있지만 조금 부족하다는 생각이 들었습니다. '내가 잘 알고 있는게 맞을까?' 그래서 김영한님의 스프링 입문 강의를 듣고 스프링의 핵심 원리 기본편을 통해서 기본기를 다지고, 다시 리마인드 하려는 측면에서 시작한 강의인데 생각보다 너무 재밌게 들었고 몰랐던 스프링의 핵심 기능들을 파악할 수 있었습니다. 다양한 예제를 통해서 실제 어떻게 적용해야 좋을지를 많이 고민하게 되는 강의였던 것 같습니다.
'Languege > Java & Spring' 카테고리의 다른 글
[Spring Framework OPEN API서비스 교육] 1.API KEY란? (0) | 2022.10.04 |
---|---|
[스프링 에러] 카카오 로그인 시 발생 오류 및 해결방법 Provider ID must be specified for client registration 'kakao' (0) | 2022.08.24 |
[김영한 스프링] 빈 생명주기 콜백 (0) | 2022.03.17 |
[김영한 스프링] 의존관계 자동주입 (0) | 2022.03.16 |