[Spring] 웹 스코프
웹 스코프의 특징
- 웹 스코프는 웹 환경에서만 동작한다.
- 웹 스코프는 프로토타입과 다르게 스프링이 종료시점 까지 관리한다. (종료 메서드 호출됨)
종류
- request : ****HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 요청마다 별도 빈 인스턴스가 생성되고 관리됨
- session : http session과 동일한 생명주기를 가지는 스코프
- application : 서블릿 컨텍스트(
ServletContext
)와 동일한 생명주기를 가지는 스코프 - websocket : 웹 소켓과 동일한 생명주기를 가지는 스코프
설명보다 예제를 만들어보는 것이 이해가 더 잘될 것이다.
request 스코프 예제 만들기
웹 환경 추가하기
웹 스코프는 웹 환경에서만 동작하므로 web 환경이 동작하도록 라이브러리를 추가하자.
build.gradle에 추가
// web 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
탐색폴더에서 tocmat, web관련 라이브러리가 추가된 걸 확인하자.
CoreApplication을 실행시키고 주소창에 localhost:8080을 입력하면 서버의 동작을 확인할 수 있다.
spring-boot-starter-web
라이브러리는 톰캣 서버를 내장하여 애플리케이션 구동 시 웹 서버와 스프링을 함께 실행시킨다.- 스프링 부트는 웹 라이브러리가 없으면
AnnotationConfigApplicationContext
을 기반으로 애플리케이션을 구동하지만, 웹 라이브러리를 추가되면 웹 추가 설정과 환경들이 필요하므로AnnotationConfigServletWebServerApplicationContext
를 기반으로 애플리케이션을 구동한다. - 만약 포트가 충돌날 시
main/resources/application.properties
에서 server.port=9090 설정을 추가하자.
request 스코프 예제 개발
동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다.
이럴 때 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
왼쪽 d06b992f...
가 request 스코프를 이용해 띄운 식별로그이다. (UUID)
- 기대하는 공통 포맷 : [UUID][requestURL]{message}
- UUID를 사용해서 HTTP 요청을 구분하자.
- requestURL 정보도 넣어 어떤 URL을 요청한 로그인지 확인하자.
MyLogger
@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 );
}
}
- 로그를 출력하기 위한
MyLogger
클래스 @Scope(value = "request")
를 이용해 request 스코프로 지정했다. HTTP 요청 당 하나씩 생성되고, 요청이 끝나는 시점 소멸된다.- 빈이 생성되는 시점에
@PostConstruct
메서드로 uuid를 생성하여 저장한다. 이 빈은 HTTP 요청당 하나씩 생성되어 고유한 값을 가지게 된다. - 소멸되는 시점 종료 메서드를 호출하여 메시지를 남긴다.
requestURL
은 빈이 생성되는 시점에는 알 수 없으므로 외부에서 setter로 입력 받는다.
LogDemoController
@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();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
- 로거가 잘 동작하는지 확인하는 테스트용 컨트롤러
- HttpServletRequest를 통해 요청 URL을 받음 (
http://localhost:8080/log-demo
) - 이렇게 받은 URL을 myLogger에 저장한다.
- 컨트롤러에서 controller Test라는 로그를 남긴다.
(requestURL을 MyLogger에 저장하는 부분은 컨트롤러가 아닌 인터셉터나 필터에 넣는 것이 더 좋음)
위 예제를 그대로 실행시키면 myLogger가 생성되지 않아서 오류가 뜬다.
(myLogger 는 http request가 올 때 호출된다.)
이 문제는 앞서 배웠던 provider를 사용하여 해결할 수 있다.
스코프와 Provider
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
MyLogger myLogger = myLoggerProvider.getObject();
String requestURL = request.getRequestURI().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
a
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " + id);
}
}
myLogger를 직접 의존성 주입하지 않고 의존성 조회(DL)를 통해 필요할 때 인스턴스를 생성할 수 있도록 수정한다.
출력
[1cfcec3f-8a44-4af8-8881-8decdd087d4a] request scope bean create:hello.core.common.MyLogger@23ea483f
[1cfcec3f-8a44-4af8-8881-8decdd087d4a][/log-demo]controller test
[1cfcec3f-8a44-4af8-8881-8decdd087d4a][/log-demo]service id = testId
[1cfcec3f-8a44-4af8-8881-8decdd087d4a] request scope bean close:hello.core.common.MyLogger@23ea483f
이렇게 요청에 대한 고유 ID를 지닌 로그를 볼 수 있다.
ObjectProvider
를 통해ObjectProvider.getObject()
를 호출할 때 빈이 생성된다.- 만약 이렇게 하지 않으면 request scope는 서버 가동 때 호출되므로 null값이 반환된다.
즉, 요청이 들어올 때만 request scope를 지닌 빈을 생성하도록 만드는 것이다.
스코프와 프록시
스코프의 생명관리를 더 간단하게 작성하기 위한 방법으로 프록시를 사용하는 기법이 있다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
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);
}
기존의 provider 관련된 코드를 전부 지우고 Scope에 proxyMode
를 추가해주면 된다.
proxyMode = ScopedProxyMode.TARGET_CLASS
를 추가해주자.- 적용 대상이 인터페이스가 아닌 클래스면
TARGET_CLASS
를 - 적용 대상이 인터페이스인 경우엔
TARGET_INTERFACE
를 - 이렇게 하면 서버 실행시점에 스프링 컨테이너에서 MyLogger의 가짜 프로시 클래스를 주입해둔다.
- 이 프록시 객체는 MyLogger를 호출하는 참조값을 갖고 있고, 클라이언트에서 myLogger를 호출할 때 원본 객체인
myLogger.logic()
를 호출한다. - 가짜 프록시 개체는 원본 클래스를 상속 받아서 만들어졌기 때문에 원본과 동일한것 처럼 대입되어 서버가 이상없이 실행되는 것이다. (다형성)
@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 = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$13e6c3a9
myLogger의 Class정보를 출력해보면 위와 같은 결과가 나온다.
동작, 특징 정리
- CGLIB 라이브러리로 클래스를 상속받은 프록시 객체를 주입해놓는다.
- 프록시 객체는 실제 요청이 오면 실제 빈을 요청하는 위임 로직이 들어있다.
- 프록시 객체는 request scope와는 관계없고 위임로직만 있으며, 싱글톤 처럼 동작한다.
- Provider나 프록시나 객체 조회를 필요한 시점 까지 지연처리 한다는 점에서 같다.
- 단순 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체하여 사용할 수 있다. (다형성과 DI 컨테이너의 강점)
- 꼭 웹 스코프가 아니어도 프록시는 사용할 수 있다.
스코프는 필요한 곳에만 사용하지 않으면 테스트나 유지보수가 까다로워진다.