request
: HTTP 요청이 들어오고 나갈 때까지 유지되는 스코프로 각각의 요청마다, 별도의 빈 인스턴스가 생성 및 관리된다.
session
: HTTP Session과 동일한 생명주기를 가진다.
application
: 서블릿 컨텍스트(ServletContext)와 동일한 생명주기를 가지는 스코프
websocket
: 웹소켓과 동일한 생명주기를 가지는 스코프
네 종류 모두 범위는 다르지만 동작 방식은 비슷하기에 대표적으로 Request
웹 스코프를 가지고 학습하자.
예제의 웹 스코프 범위의 빈은 로깅 빈이다.
// web 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
해당 라이브러리를 추가하면 스프링 부트는 내장 톰캣 서버를 활용해 웹 서버와 스프링을 함께 실행한다.
해당 라이브러리가 없으면 지금까지처럼 AnnotationConfigApplicationContext
를 기반으로 애플리케이션을 구동한다.
웹 라이브러리가 추가되면 웹 관련 기능 및 설정이 필요하기에 AnnotationConfigServletApplicationContext
를 기반으로 애플리케이션을 구동한다.
만약 실행했을 때 기본 포트인 8080 포트가 사용 중이라면 main/resources/application.yml
에 다음 내용을 추가해 포트를 변경하자.
server.port=9000
@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) {
String format = String.format("[%s][%s]%s", uuid, requestURL, message);
System.out.println(format);
}
@PostConstruct
public void init() {
uuid = UUID.randomUUID().toString();
System.out.println(String.format("[%s]request scope bean create:", uuid) + this);
}
@PreDestroy
public void destroy() {
System.out.println(String.format("[%s]request scope bean close:", uuid) + this);
}
}
로그를 출력하기 위한 MyLogger
클래스
@Scope(value = "request")
를 사용해서 request 스코프로 지정했다. 이제 이 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸된다.
빈이 생성되는 시점에 자동으로 @PostConstruct
초기화 메서드를 사용해서 uuid를 생성해서 저장해둔다. 해당 빈은 HTTP 요청 당 하나씩 생성되므로, uuid를 저장해두면 다른 HTTP 요청과 구분할 수 있다.
이 빈이 소멸되는 시점에 @PreDestroy
를 사용해서 종료 메시지를 남긴다.
requestURL
은 이 빈이 생성되는 시점에는 알 수 없으므로, 외부에서 setter로 입력받는다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final MyLogger myLogger;
private final LogDemoService logDemoService;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
로거가 잘 작동하는지 확인하는 테스트용 컨트롤러다.
여기서 HttpServletRequest를 통해서 요청 URL을 받았다.
http://localhost:8080/log-demo
이렇게 받은 requestURL 값을 myLogger에 저장해둔다.
컨트롤러에서 controller test라는 로그를 남긴다
여기서는 컨트롤러에 URL을 저장했지만 인터셉터나 서블릿 필터에서 저장하는 것이 좋다. (AOP)
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String testId) {
myLogger.log("service id=" + testId);
}
}
MyLogger가 없는데 서비스 계층에서 로그를 남기려면 파라미터로 모든 정보를 넘겨야 하는데, 그럴 경우 요구 파라미터가 너무 많아진다.
웹과 관련 없는 서비스 계층에 웹과 관련된 정보가 넘어간다.(requestURL)
웹과 관련된 부분은 컨트롤러까지만 사용되고 서비스 계층에서는 웹 기술에 종속되지 않는 게 좋다.
CoreApplication을 실행시켜보면, 예외가 발생할 것이다.
스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 아직 생성되지 않고 실제 고객의 요청이 와야 생성할 수 있기 때문이다.
다시 말해, 해당 프로젝트가 구동될 때 스프링 빈들이 컴포넌트 스캔이 되며 등록 및 의존관계 주입이 되는데, 여기서 웹스코프인 MyLogger 빈의 경우 HTTP request 요청이 올 때 생성되는 빈이기 때문에 스프링 구동 단계에서는 아직 생성을 할 수 없다. 그렇기에 해당 에러가 발생하는 것이다.
마지막으로 정리하면, 스프링 구동시 MyLogger 스프링 빈을 등록을 요구하는데 해당 빈은 자신이 생성돼야 할 스코프 범위에 해당되지 않았기 때문에 에러가 발생한다.
그럼 해당 스프링 빈은 스프링 구동 시점이 아닌 사용자의 HTTP request 요청 시점에 생성될 수 있다는 말인데, 이를 해결하기 위한 방법들에 대해 알아보자.
스프링 빈의 생성시점이 구동 시점이 아닌 지연된 시점인 경우 해결책에 대해 알아보자.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final ObjectProvider<MyLogger> myLoggerProvider;
private final LogDemoService logDemoService;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerProvider.getObject();
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 testId) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id=" + testId);
}
}
log-demo 경로로 접근을 시도하면 콘솔 창에도 의도한 포맷 형식으로 데이터가 잘 나온다.
ObjectProvider
를 사용해 getObject()
를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다. getObject()
를 호출하는 시점에는 HTTP 요청이 진행 중이기에 빈 생성이 정상적으로 동작한다.getObject()
를 컨트롤러, 서비스에서 각각 호출하는데도 동일한 HTTP 요청일 경우 같은 스프링 빈이 반환된다. @Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
...
}
proxyMode = ScopedProxyMode.TARGET_CLASS
속성 활용
TARGET_CLASS
사용INTERFACES
사용 이제 이 클래스는 가짜 프록시를 만들어두고 이 가짜 프록시 빈을 의존관계 주입을 한다.
해당 빈이 실제 사용될 때 프록시 빈에서 실제 빈을 가져와 사용할 수 있도록 한다.
어떻게 가능한 것일까 ❓
myLogger의 클래스를 출력해 보면 우리가 만든 hello.core.common.MyLogger가 출력되지 않고 이를 상속한 임의의 객체가 생성되는 것을 볼 수 있다.
System.out.println("myLogger: "+myLogger.getClass());
//myLogger: class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$ac52ff68
@Scope
의 proxyMode = ScopedProxyMode.TARGET_CLASS)
를 설정하면 스프링 컨테이너는 CGLIB 라는 바이트코드를 조작하는 라이브러리를 사용해서, MyLogger를 상속받은 가짜 프록시 객체를 생성한다.
결과를 확인해 보면 우리가 등록한 순수한 MyLogger 클래스가 아니라 MyLogger$ $EnhancerBySpringCGLIB
이라는 클래스로 만들어진 객체가 대신 등록된 것을 확인할 수 있다.
그리고 스프링 컨테이너에 "myLogger"라는 이름으로 진짜 대신에 이 가짜 프록시 객체를 등록한다.
ac.getBean("myLogger", MyLogger.class)
로 조회해도 프록시 객체가 조회되는 것을 확인할 수 있다.
그래서 의존관계 주입도 이 가짜 프록시 객체가 주입된다.
myLogger.logic()
을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이다.myLogger.logic()
를 호출한다.