빈 스코프란 빈이 존재할 수 있는 범위를 말한다.
기본적으로 빈은 싱글톤 스코프로 생성되며 스프링 컨테이너가 시작할 때 부터 종료할 때까지 유지된다.
싱글톤 : 스프링 컨테이너가 시작되고 종료될 때까지 빈을 유지
프로토타입 : 빈 생성 및 의존성 주입까지만 스프링 컨테이너가 관리를 해준다.
웹 관련 스코프
- request : 웹 요청을 받는 순간부터 로직을 처리후 응답할때까지 빈을 유지
- session : 웹 세션이 생성되고 종료될 때까지 빈을 유지
- application : 서블릿 컨텍스트와 같은 범위로 빈을 유지
Request 스코프
request 스코프를 가진 빈은 HTTP 요청이 들어올 때 생성(@PostConstruct 실행) 되며 로직을 처리한 후 HTTP 응답을 줄 때 소멸(@PreDestory)된다.
@Component
@Scope(value = "request")
public class Test {
private String uuid;
public void log(String message) {
System.out.println("[" + uuid + "] message : "+message);
}
// HTTP 요청이 들어오는 순간 빈이 생성되며 실행됨
@PostConstruct
public void init() {
uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid + "] request scope bean created : " + this);
}
// HTTP 응답이 나가는 순간 빈이 소멸되며 실행됨
@PreDestroy
public void close() {
System.out.println("[" + uuid + "] request scope bean closed : " + this);
}
}
즉, Controller에 ReqeuestMapping이 되는 순간 빈이 생성되고 반환될 때 소멸된다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final Test test;
@RequestMapping("test")
@ResponseBody
public String test() {
// /test로 요청이 들어온 시점에 Request 스코프 빈이 생성됨 -> Test 빈이 해당 시점에 생성되길 기대
test.log("test");
// 응답 시점에 Request 스코프 빈 소멸
return "OK";
}
}
하지만, 여기서 문제점이 발생한다.
위의 코드에서 개발자가 Request 스코프로 빈을 설정한 의도는 “/test”로 요청이 들어오는 시점에 빈이 생성되고, 각각의 요청마다 빈을 따로 관리하고 싶어서 해당 스코프로 빈을 설정했을 것이다.
하지만, Request 스코프로 설정한 빈은 어찌되었든 스프링 컨테이너가 최초로 실행될 때에는 @Component를 보고 빈을 등록을 시도하지만, 실제로는 HTTP 요청이 들어오지 않은 상태이기 때문에 런타임 에러가 발생한다.
이를 해결하기 위해서 Provider를 이용해 빈의 생성을 지연시키거나, Proxy를 이용해 가짜 객체를 주입한 상태로 스프링 컨테이너를 실행하는 방법이 있다.
Provider
문제를 해결할 수 있는 첫 번째 방법은 의존관계 주입을 생성 시점이 아닌 필요한 시점에 ObjectProvider를 이용해 직접 조회한 후 호출 시점에 빈을 생성하는 지연 방식이다.
실제 HTTP 요청이 들어오는 시점에 getObject()를 이용해 request 스코프 빈을 생성할 수 있다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
// ObjectProvider를 이용해 빈의 생성을 지연한다.
private final ObjectProvider<Test> testProvider;
@RequestMapping("test")
@ResponseBody
public String test() {
// .getObject()를 이용해 빈을 조회하는 시점은 HTTP 요청이 일어난 이후이므로 빈이 정상적으로 생성된다.
Test test = testProvider.getObject();
test.log("test");
return "OK";
}
}
동일한 요청 내에서 여러 로직을 타고 들어갔을 때, 해당 로직에서 똑같이 Test 빈을 getObject()로 조회하는 경우가 존재한다면, 동일한 HTTP 요청이기 때문에 동일한 스프링 빈이 반환된다.
Proxy
CGLIB라는 바이트코드 조작 라이브러리를 이용해 가짜 객체를 생성시점에 만들어 주입한 후 실제 호출 시점에 가짜 객체가 실제 객체를 호출하는 방식이다.
가짜 객체는 내부에 실제 객체를요청하는 위임로직을 갖고 있기 때문에, 클라이언트 입장에서는 가짜 객체를 호출하더라도 Java의 다형성 특징에 의해 동일하게 사용이 가능하다.
Proxy는 @Scope 내에 proxyMode를 설정해 사용할 수 있다.
TARGET_CLASS는 클래스 인 경우 해당 객체를 상속받은 가짜 객체를 생성해 스프링 컨테이너가 동작 시 주입한다.
인터페이스인 경우 ScopedProxyMode.INTERFACES를 이용한다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class Test {
private String uuid;
public void log(String message) {
System.out.println("[" + uuid + "] message : "+message);
}
// HTTP 요청이 들어오는 순간 빈이 생성되며 실행됨
@PostConstruct
public void init() {
uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid + "] request scope bean created : " + this);
}
// HTTP 응답이 나가는 순간 빈이 소멸되며 실행됨
@PreDestroy
public void close() {
System.out.println("[" + uuid + "] request scope bean closed : " + this);
}
}
Controller의 로직은 Provider를 이용하기 전과 동일하다.
가짜 객체로 주입된 Test는 HTTP 요청이 들어오면서 실제 객체로 위임되기 때문이다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final Test test;
@RequestMapping("test")
@ResponseBody
public String test() {
System.out.println("test.getClass() = " + test.getClass());
test.log("test");
// 응답 시점에 Request 스코프 빈 소멸
return "OK";
}
}
실제로 /test로 요청을 보내보면 CGLIB라는 이름으로 생성된 가짜 객체가 생성되어있는 것을 확인할 수 있고, 실제 빈이 생성될 때는 실제 객체로 생성되는 것을 확인할 수 있다.
📄 References
스프링 핵심원리 기본편 : https://www.inflearn.com/course/스프링-핵심-원리-기본편/dashboard
'Backend > Spring Boot' 카테고리의 다른 글
Spring Cloud Gateway를 이용한 서비스 라우팅 및 JWT 토큰 검증 (2) | 2022.05.24 |
---|---|
Custom Exception을 이용한 예외 처리 (0) | 2022.05.22 |
Spring Cloud Bus와 RabbitMQ를 이용해 설정 정보 한번에 최신화하기! (0) | 2022.05.15 |
싱글톤 스코프, 프로토타입 스코프 (0) | 2022.05.03 |
Spring Data JPA의 Page와 Slice (0) | 2022.05.02 |