김영한님의 Spring 핵심원리 강의를 듣고 정리하는 글입니다.
1. 스코프(Web Scope)란?
- 웹 환경에서만 동작하는 스코프이다.
- 스프링이 해당 스코프의 종료시점까지 관리한다. 따라서 종료 메서드가 호출된다.
2. 웹 스코프의 종류
- requst: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.
- session: HTTP Session과 동일한 생명주기를 가지는 스코프
- application: 서블릿 컨텍스트와 동일한 생명주기를 가지는 스코프
- websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프
해당 스코프들은 범위만 다르고 동작 방식은 비슷하기 때문에 request 스코프로 설명하겠다.
3. request 스코프 동작 방식
Controllre에서 request 스코프의 빈으로 지정된 MyLogger을 주입받는다고 가정하자.
1. 클라이언트가 Controller에 요청한다.
2. 스프링 DI 컨테이너에 MyLogger를 요청하고 주입받는다.
3. MyLogger의 스코프는 HTTP 요청 하나가 들어오고 나갈 때 까지 유지된다.
즉, request 스코프는 HTTP의 요청마다 각각 할당된다.
4. request 스코프 잘못된 예제 코드
각각의 HTTP 요청을 파악하기 쉽도록 UUID 고유값을 할당하고, 로그를 출력해보자.
웹 환경 설정
- request 스코프는 웹 환경에서만 동작하므로 web 환경이 동작하도록 라이브러리를 추가해야 한다.
- build.gradle에 web 라이브러리를 추가 implementation 'org.springframework.boot:spring-boot-starter-web'
- 해당 라이브러리를 추가하게 되면 스프링 부트는 내장 톰켓 서버를 활용해서 서버와 스프링을 함께 실행시킨다.
MyLogger.java
@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);
}
}
- 로그를 출력하기 위한 클래스이다.
- request 스코프로 지정했기 때문에 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸된다.
- 초기화 메서드에 uuid를 생성해서 저장한다.
- requestURL은 외부에서 setter로 입력 받는다.
LogDemoController.java
@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.getRequestURI().toString();
System.out.println("myLogger = " + myLogger.getClass());
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
- MyLogger가 잘 동작하는지 테스트 하기 위한 Controller이다.
SeLogDemoService.java
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
- 서비스 계층에서도 로그를 출력해보자.
하지만 이대로 스프링 부트를 실행시키면 오류가 발생한다.
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 스코프가 현재 시점에 활성화 되지 않았다라는 내용이다. 원인은 간단하다. 실행시점에는 HTTP 요청이 일어나지 않기 때문에 LogDemoService, LogDemoController 는 MyLogger 빈을 주입 받을 수 없기 때문이다.
5. request 스코프를 정상 동작하도록 하는 방법
a. Provider 해결
Provider를 사용하여 해결할 수 있다.
MyLogger는 수정할 코드는 없다.
SeLogDemoService.java
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " + id);
}
}
LogDemoController.java
@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.getRequestURI().toString();
MyLogger myLogger = myLoggerProvider.getObject();
System.out.println("myLogger = " + myLogger.getClass());
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
- 실행결과를 보면 LogDemoController, SeLogDemoService 각각 따로 호출해도 같은 HTTP 요청이기 때문에 같은 빈이 반환되어 사용된다.
- ObjectProvider 덕분에 ObjectProvider.getObject() 를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다.
- getObject() 호출 시점에는 HTTP 요청이 진행중이므로 request 스코프 빈이 생성되어 있어 정상적으로 동작한다.
하지만 이 코드는 service 계층, Controller 계층에서 중복된 코드가 있기 때문에 번거롭다. 코드를 단순화하기 위해 프록시를 통해 해결하는 방법을 보자.
b. 프록시를 통해 해결
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {...}
- 스코프 설정 시 proxyMode를 설정해주면 된다.
- 적용 대상이 클래스면 TARGET_CLASS를 선택
- 적용 대상이 인터페이스면 INTERFACES를 선택
- 이렇게 하면 MyLogger의 가짜 프록시 객체를 만들어두고, 다른 빈에 미리 주입하게 된다.
- 실제 HTTP 요청이 들어오면 MyLogger의 새로운 빈을 만들어 프록시 객체와 갈아끼우는 작업을 하게 된다.
동작과정
- CGLIB 라이브러리를 통해 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.
- 가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다.
- 프록시 객체는 그냥 가짜 객체로, 내부에는 단순한 위임로직만 있고, 싱글톤처럼 동작한다.
6. 정리
- requets 스코프는 웹 환경 위해서 동작한다.
- request 스코프는 HTTP 요청이 있을 때 빈을 생성하기 때문에 스프링 부트 실행시점에서 오류가 발생할 수 있다는 걸 주의하자.
- Provider, 프록시 객체를 통해 해결할 수 있다.
- 프록시 객체를 사용하면 코드를 단순화할 수 있고, 싱글톤 빈을 사용하는 것처럼 request 스코프 빈을 사용할 수 있다.
- provider나 프록시로 해결하든 진짜 객체 조회를 필요한 시점까지 지연처리 한다는 점이 핵심이다.
- request 스코프와 같은 특별한 스코프는 무분별하게 사용하면 유지보수가 어려워진다.
'Programming > Spring' 카테고리의 다른 글
[Spring] 스프링 MVC 1편(서블릿) (0) | 2024.02.02 |
---|---|
[Spring] 스프링 MVC 1편(웹 애플리케이션 이해) (0) | 2024.01.22 |
[Spring] 싱글톤과 프로토타입 스코프 (1) | 2024.01.10 |
[Spring] 스프링 컨테이너가 빈을 등록하는 방법 (1) | 2024.01.10 |
[Spring] @Autowired에서 조회 빈이 2개 이상인 경우 (1) | 2024.01.10 |