2023.06.09 - [SpringBoot] - Spring Basic- Bean Scope(프로토타입 스코프)
앞서 말한 프로토타입 스코프에 이어서 이번에는 web scope 에 대해서 알아보자.
웹 스코프가 뭘까?
- 웹 스코프는 웹 환경에서만 동작하는 스코프이다.
- 웹 스코프는 스프링 컨테이너가 해당 스코프의 종료 시점까지 관리한다. → 종료 메서드 호출된다.
웹 스코프의 종류에는 request, session, application, websocket 이 있는데 해당 글에서는 request에 대해서 알아보자.
request : Http 요청 하나가 들어오고 나갈 때까지 유지되는 스코프, 각각의 요청마다 별도의 빈 인스턴스가 생성,관리됨.
[나머지도 범위만 다르고 동작은 비슷하게 돌아간다.]
우선, request 스코프를 포함한 web 스코프를 사용하려면, web 환경이 동작하도록 라이브러리를 추가해야 한다.
build.gradle에 해당 코드를 추가하자.
//web 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
이를 추가하고 main 메서드를 실행하면 웹 어플리케이션이 작동한다.
request 스코프는 언제 사용될까?
동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다.
이럴때 사용하기 딱 좋은것이 바로 request 스코프이다.
그렇다면 이 로그를 남기고 구분하는데 효과적인 request 스코프의 예제를 통해서 알아보자.
[d06b992f...] request scope bean create
[d06b992f...][http://localhost:8080/log-demo] controller test
[d06b992f...][http://localhost:8080/log-demo] service id = testId
[d06b992f...] request scope bean close
우리는 이와 같은 출력 결과를 기대하고 예제를 작성할 것이다.
기대하는 공통 포멧: [UUID][requestURL] {message}
UUID를 사용해서 HTTP 요청을 구분하자. ( uuid는 전 세계에 단 하나의 id라고 생각하면 된다. )
우선, 로그를 출력하는 클래스인 MyLogger를 만들어보자.
@Component
@Scope(value = "request") //@Scope("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 // 빈이 생성되는 시점에 초기화 메서드를 통해 uuid를 생성
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);
}
}
- 해당 빈은 http 요청 당 하나씩 생성되므로, uuid를 저장해두면 다른 요청들과 구분이 가능하다.
- requestURL은 이 빈이 생성되는 시점에는 알 수 없으므로, 외부에서 setter로 입력받는다.
로그를 남기는 MyLogger 클래스를 만들었으니, 이번에는 테스트용 컨트롤러 LogDemoController를 만들어보자.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) { //java에서 제공
String requestURL = request.getRequestURL().toString(); //getRequestURL()- HttpServletRequest 내부 메서드
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
- HttpServletRequest를 통해서 요청 URL을 받았다. → requestURL : http://localhost:8080/log-demo
- 이를 myLogger에 저장
- 컨트롤러에서 controller test라는 로그도 남긴다.
※ requestURL을 MyLogger에 저장하는 부분과 같은 공통 처리가 가능한 것들은 되도록이면 스프링 인터셉터나 서블릿 필터와 같은 걸 활용하는 걸 추천한다. 현재는 예제를 단순화했기 때문에 넘어가겠다.
비지니스 로직이 있는 서비스 클래스 LogDemoService 를 만들어보자.
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
이제 로직을 실행하는 서비스 클래스와 컨트롤 클래스, 로그를 출력하는 request 스코프의 로그 클래스까지 다 완성했다.
Main 메서드를 통해서 실행 결과가 우리가 예상한 것과 같은지 확인해보자.
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 스코프 빈이 아직 생성되지 않았기 때문이다.
이 빈은 실제 고객의 요청이 와야 생성할 수 있다.
이를 해결하려면 어떻게 해야할까?
컨테이너에게 빈을 달라고 하는 단계를 실제 고객 요청이 있을 때로 미루면 된다.
이를 하기 위해선, 앞서 말했었던 DL 기능을 제공하는 Provider를 사용하면 된다.
ObjectProvider를 사용해보자!
MyLogger 클래스는 건드리지 않고, 컨트롤러 클래스와 서비스 클래스의 코드를 수정해보자.
@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(); // Provider 내부 메서드, 초기화 메서드 init()으로 uuid 생성
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject(); //이를 호출할 때까지 request scope 빈의 생성을 지연
myLogger.log("service id = " + id);
}
}
- Controller, Service 에서 각각 한번씩 getObject()를 호출해도 같은 HTTP 요청이라면 같은 빈이 반환된다.
이제 다시 실행해보면 우리가 원하는 결과를 얻을 수 있다.
위에서 말한 Provider를 활용한 방법을 제외하고 또 다른 방법이 있다.
이 방법을 쓰면 오히려 코드가 더 간단해진다.
그러나, 이 방법은 특별한 Scope이기 때문에 무분별하게 사용하면 유지보수가 어려워지므로 꼭 필요할 때만 사용하자.
Proxy 방법을 사용해보자!
애노테이션에 proxyMode = ScopedProxyMode.TARGET_CLASS 만 추가해주면 끝이다.
[ 적용 대상이 클래스가 아니라 인터페이스라면, CLASS → INTERFACES로 바꾸면 된다. ]
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
이렇게 변경해주고 모든 코드를 Provider를 사용하기 전 코드로 복구해주면 정상적으로 작동하는 것을 확인할 수 있다.
무슨 원리로 되는걸까?
CGLIB이라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입하기 때문에 가능하다!
- 무슨 말이냐면, 스프링 컨테이너는 proxyMode로 설정을 하면 CGLIB이라는 라이브러리를 통해서, MyLogger를 상속받은 가짜 프록시 객체를 생성한다.
- 그리고 스프링 컨테이너에 "myLogger"라는 이름으로 진짜 대신에 생성한 가짜 프록시 객체를 등록한다.
- 그래서 DI 주입도 이 가짜 프록시 객체가 주입된다.
→ 가짜 프록시 객체는 HTTP 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
총 정리
- Provider를 사용하든 Proxy를 사용하든 중요한 것은 진짜 객체 조회를 꼭 필요한 시점까지 지연처리한다는 점이다.
- 프록시는 웹 스코프가 아니더라도 사용이 가능하다.( Provider 역시 다른 곳에서도 사용 가능 )
- 프록시는 마치 싱글톤처럼 사용하는 것 같지만 결국은 다르기에 주의해서 사용해야 한다.
'Spring' 카테고리의 다른 글
싱글톤 패턴, 싱글톤 컨테이너 (Singleton) (0) | 2023.08.02 |
---|---|
스프링 컨테이너, 스프링 빈(Spring Container, Spring Bean) (0) | 2023.08.02 |
Spring Basic- Bean Scope(프로토타입 스코프) (0) | 2023.06.09 |
Spring Basic- Bean LifeCycle CallBack (0) | 2023.05.24 |
Spring Basic- DI(의존 관계) 자동 주입 3 (0) | 2023.05.23 |