티스토리 뷰
request 스코프는 웹 스코프 중 하나로 HTTP 요청이 들어오고 나갈 때까지 유지되는 스코프다. 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.
예제로 Logger를 하나 생각해보자.
HTTP 요청마다 id를 부여해 HTTP 요청을 구분하고 싶다. 이때 request 스코프를 이용하면 HTTP 요청을 쉽게 구분할 수 있다.
@Component
@Scope(value = "request") // 이 빈은 http 요청 당 하나씩 생성 & http 끝나는 시점에 소멸
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(); //생성 시점에 uuid 생성해서 저장. 다른 http 요청과 구분!
System.out.println("[" + uuid + "] request scope bean create : " + this );
}
@PreDestroy
public void close() {
//고객 요청이 끝날 때 호출
System.out.println("[" + uuid + "] request scope bean close : " + this);
}
}
Logger 객체를 만든다. @Scope(value="request")에 주목하자. request 스코프를 사용했다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody //String을 그대로 반환
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURI().toString();
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testID");
return "OK";
}
}
package hello.core.web;
import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("servide id = " + id);
}
}
HTTP 요청을 처리하기 위한 Controller, Service 객체도 하나씩 만들자.
이렇게 하고 실행해보면...
위와 같은 에러가 나온다. request가 active하지 않다...?
그렇다. 처음 Controller를 생성할 때 컨테이너에 MyLogger를 찾아서 주입해달라고 요청하는데, 아직 HTTP 요청이 들어오지 않은 상태다! request 스코프는 HTTP 요청이 들어왔을 때 생성된다고 했다. 따라서 컨테이너는 request 스코프 빈인 MyLogger를 줄 수 없고, 에러를 띄운 것이다.
그렇다면.. MyLogger 생성을 미루어두었다가(지연시켰다가) HTTP 요청이 들어왔을 때 컨테이너에게 MyLogger를 조회해달라고 요청(Dependency Lookup)할 순 없을까..?
가만, 비슷한 것을 프로토타입 스코프를 공부할 때 배웠었다! 컨테이너에게 특정 빈을 달라고 요청할 수 있는 방법..!
바로 Provider다!
Controller와 Service 객체를 다음과 같이 수정하자.
package hello.core.web;
import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
// private final MyLogger myLogger; //에러 발생. 컨테이너에 myLogger 요청하지만, 이때
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody //String을 그대로 반환
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURI().toString();
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testID");
return "OK";
}
}
package hello.core.web;
import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LogDemoService {
// private final MyLogger myLogger;
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("servide id = " + id);
}
}
이제 Controller와 Service가 생성될 때는 MyLogger를 찾을 수 있는 Provider까지만 생성된다. 그리고 MyLogger를 요청하는 것은 HTTP 요청이 들어오고 난 다음으로 미루어졌으므로 문제가 해결되었다. 실행해보면...
MyLogger가 제대로 동작한다!
(참고)
프록시를 사용해 Provider를 대체할 수 있다.
package hello.core.common;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.UUID;
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) // 이 빈은 http 요청 당 하나씩 생성 & http 끝나는 시점에 소멸
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(); //생성 시점에 uuid 생성해서 저장. 다른 http 요청과 구분!
System.out.println("[" + uuid + "] request scope bean create : " + this );
}
@PreDestroy
public void close() {
//고객 요청이 끝날 때 호출
System.out.println("[" + uuid + "] request scope bean close : " + this);
}
}
@Scope 어노테이션의 옵션을 바꾸어 주자.
proxyMode를 설정하는데, 아래 객체가 class면 TARGET_CLASS로, interface면 INTERFACES로 두자.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody //String을 그대로 반환
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURI().toString();
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testID");
return "OK";
}
}
package hello.core.web;
import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("servide id = " + id);
}
}
그리고 Controller와 Service는 Provider를 적용하기 전으로 돌리자.
그러면 정상 작동하는 걸 알 수 있다.
어떻게 된 걸까?
옵션에 프록시를 설정하면, 처음에 빈을 등록할 때 가짜 MyLogger가 등록된다. 그리고 MyLogger가 클라이언트에 의해 호출되면, 이 가짜 MyLogger가 진짜 MyLogger(request 스코프 빈)을 찾아서 필요한 로직을 수행하게 한다.
MyLogger.getClass()를 출력해보면, $$EnhancerBySpringCGLIB... 이 붙은 것을 볼 수 있는데, 스프링 컨테이너에 의해 가짜 myLogger 객체가 생성된 것이다.
이처럼 프록시를 이용해, Provider와 마찬가지로 request 스코프 빈의 생성을 지연시킬 수 있다. 프록시를 이용하면 마치 싱글톤 빈을 사용하는 것처럼 별도의 코드를 추가하지 않고 request 스코프 빈을 활용할 수 있다. 어노테이션만 바꿔서 기능을 달리 할 수 있다는 것이 스프링이 얼마나 다형성 있는 코드에 최적화되어 있는지를 보여준다.
정리하자면,
request 스코프 빈은 HTTP 요청이 들어왔을 때 생성되어 HTTP 요청 처리가 끝났을 때 소멸되는 빈이다.
이런 스코프 빈은 문제가 생겼을 때 디버깅하기 어렵고, 테스트 코드를 작성하기도 힘들다. 따라서 반드시 필요한 곳에만 활용하자.
사진, 내용 출처 : 인프런, 김영한 강사님의 '스프링 핵심 원리 - 기본편' 강좌, 강의자료
'SPRING' 카테고리의 다른 글
토비의 스프링 2. 테스트 (0) | 2022.05.27 |
---|---|
토비의 스프링 1. 오브젝트와 의존관계 (0) | 2022.05.27 |
프로토타입 스코프 (0) | 2021.05.20 |
빈 생명주기 콜백 (init, cleanup) (0) | 2021.05.20 |
Map, List를 활용해 모든 빈 조회하기 (0) | 2021.05.18 |
- Total
- Today
- Yesterday
- OOP
- 템플릿콜백
- 자바
- 백기선
- BOJ
- c++
- 코딩테스트
- 프록시패턴
- SOLID
- 토비의스프링
- AOP
- 김영한
- 객체지향
- 토비의봄TV
- java
- provider
- 메서드레퍼런스
- 디자인패턴
- 카카오
- ec2
- 코테
- 스프링
- gracefulshutdown
- 프록시
- 서비스추상화
- 프로그래머스
- 토비
- 예외처리
- 자바스터디
- 데코레이터패턴
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |