본문 바로가기
WEB/Spring

[Spring] 스프링 빈 웹 스코프, request 타입과 프록시 모드 - 2/2

by 정권이 내 2022. 6. 19.

File:Spring Framework Logo 2018.svg - Wikimedia Commons

 

 

[Spring] 스프링 웹 스코프, request Scope

웹 스코프는 웹 환경에서 동작하는 스코프이며 프로토 타입과 다르게 스프링 컨테이너가 생성시점부터 종료시점까지 관리합니다.

 

웹 스코프의 종류

  • request : HTTP 요청이 들어와서 나갈때까지 유지되는 스코프, 각 클라이언트의 HTTP 요청마다 별도의 인스턴스 생성
  • session : HTTP Session과 동일한 생명주기를 가지는 스코프
  • application : 서블릿 컨텍스트(ServletContext)와 동일한 생명주기를 가지는 스코프
  • websocket : 웹 소켓과 동일한 생명주기를 가지는 스코프

서블릿 컨텍스트 (ServletConext)

하나의 서블릿이 서블릿 컨테이너와 통신하기 위해 사용되는 메서드들을 가지고 있는 클래스, 서블릿은 웹서버에서 동적인 페이지를 생성하기 위해 클라이언트에서 웹서버로 요청이 들어오면 웹서버로부터 요청을 전달받아 처리하고 다시 웹서버에 응답을 하는 자바 프로그램입니다.

 

request 스코프 테스트

HTTP 요청이 동시에 여러개가 발생할때 request 스코프를 사용하면 각 HTTP 요청에 대한 로그를 구분할수 있는데 테스트 코드를 통해 확인 해보겠습니다. 먼저 웹 환경을 만들기 위해 build.gradle에 라이브러리를 추가합니다.

implementation 'org.springframeork.boot:spring-boot-starter-web'

 

gradle 정보를 업데이트하여 라이브러리가 추가되면 @SpringBootApplication 애노테이션이 설정된 클래스에서 main 메서드를 실행하면 웹 애플리케이션이 실행되는것을 확인할수 있습니다.

@SpringBootApplication

public class SpringDemoStudyApplication {
public static void main(String[] args) {
SpringApplication.run(SpringDemoStudyApplication.class, args);
}
}
2022-06-15 22:47:37.416  INFO 25416 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-06-15 22:47:37.422  INFO 25416 --- [           main] c.e.s.SpringDemoStudyApplication         : Started SpringDemoStudyApplication in 1.804 seconds (JVM running for 2.212)

 

MyLogger.java

MyLogger는 http 요청 로그를 출력하기 위한 기능이 있는 클래스 입니다. request 스코프로 설정 되있기 때문에 http 요청이 들어올때 스프링 컨테이너에 등록됩니다.

@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);
}
}

 

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.getRequestURL().toString();
myLogger.setRequestURL(requestURL);

myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}

 

LogDemoService.java

@Service
@RequiredArgsConstructor
public class LogDemoService {

private final MyLogger myLogger;

public void logic(String id) {
myLogger.log("service id = " + id);
}
}

 

실행시 오류발생

Caused by: org.springframework.beans.factory.support.ScopeNotActiveException: Error creating bean with name 'myLogger': Scope 'request' is not active for the current thread; ....

 

실행하면 오류가 발생하는데 이유는 request 스코프 때문입니다. 위에서 설명했듯이 request 스코프의 생명주기는 http 요청이 들어올때부터 반환할때 까지인데 스프링 애플리케이션을 실행하는 시점에는 request 요청이 없기 때문에 MyLogger 클래스 객체가 스프링 컨테이너에 등록되지 않았는데 LogDemoController 클래스의 logDemo 함수에서 접근하기 때문에 오류가 발생하는것입니다.

 

이 오류를 해결하려면 스프링 컨테이너에서 MyLogger 객체에 대한 요청을 스프링 애플리케이션이 실행하는 시점이 아닌 HTTP 요청이 들어올때로 지연시키는 방법이 있는데 Provider, 프록시 두가지 방법이 있습니다.

 

스코프와 ObjectProvider

이전 포스팅에서 설명한 ObjectProvider을 사용하여 스프링 컨테이너에서 MyLogger 객체를 받아오는 시점을 HTTP 요청이 들어올때로 사용하는 방법입니다.

 

LogDemoController.java

@Controller
@RequiredArgsConstructor
public class LogDemoController {

private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerObjectProvider;

@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerObjectProvider.getObject();
myLogger.setRequestURL(requestURL);

myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}

 

LogDemoService.java

@Service
@RequiredArgsConstructor
public class LogDemoService {

private final ObjectProvider<MyLogger> myLoggerObjectProvider;

public void logic(String id) {
MyLogger myLogger = myLoggerObjectProvider.getObject();
myLogger.log("service id = " + id);
}
}

 

웹브라우저에서 http://localhost:8080/log-demo 입력후 로그 확인

[c983340c-7929-4130-ae78-3dcff374db1f] request scope bean create:com.example.springdemostudy.common.MyLogger@6a8afcbf
[c983340c-7929-4130-ae78-3dcff374db1f][http://localhost:8080/log-demo][controller test]
[c983340c-7929-4130-ae78-3dcff374db1f][http://localhost:8080/log-demo][service id = testId]
[c983340c-7929-4130-ae78-3dcff374db1f] request scope bean close:com.example.springdemostudy.common.MyLogger@6a8afcbf

 

ObjectProvider클래스의 getObject() 메서드를 호출하는 시점에는 http 요청이 활성화된 상태이므로 MyLogger 클래스의 request 스코프 빈 생성이 정상적으로 동작하는것입니다.

 

컨트롤러 서비스에서 외부 http 요청이 들어오면 MyLogger 객체를 생성하면서 MyLogger 클래스에서 @PostConstruct로 지정된 init() 메서드를 수행하게되고 uuid를 생성하게 됩니다. 또한 LogDemoController, LogDemoService 클래스에서 각각 MyLogger 객체를 생성하지만 동일한 http 요청에 대해서는 동일한 스프링 빈 객체가 반환됩니다.

 

스코프와 프록시

request 스코프를 설정한 클래스에 프록시 모드 설정을 추가하여 가짜 프록시 클래스를 만들어서 가짜 프록시 클래스가 스프링 컨테이너에 들어가게되고 http 요청에 상관없이 다른 클래스에서 주입받을수 있도록 하는 방법입니다.

ObjectProvider 방법보다 훨씬 간편합니다!!

 

MyLogger.java

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
...
}

 

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.getRequestURL().toString();
myLogger.setRequestURL(requestURL);

myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}

 

LogDemoService.java

@Service
@RequiredArgsConstructor
public class LogDemoService {

private final MyLogger myLogger;

public void logic(String id) {
myLogger.log("service id = " + id);
}
}

 

proxyMode = ScopedProxyMode.TARGET_CLASS 이 설정이 중요한 부분인데 예제에서는 적용대상인 MyLogger가 클래스이기 때문에 TARGET_CLASS 로 설정하였고 인터페이스라면 INTERFACE로 설정하면 됩니다.

  • 적용대상이 클래스 : ScopedProxyMode.TARGET_CLASS
  • 적용대상이 인터페이스 : ScopedProxyMode.INTERFACE

 

Provider 방식과 다른점은 스프링 빈에 등록된 MyLogger 객체는 스프링에서 CGLIB 라이브러리로 만들어진 MyLogger 클래스를 상속받은 가짜 프록시입니다. 가짜 프록시 객체에는 http 요청이 오면 내부에서 원본 스프링 빈을 찾아서 요청하는 위임 로직이 들어있는데 가짜 프록시는 원본 클래스를 상속받아 만들어졌기 때문에 클라이언트 입장에서는 원본인지 가짜인지 구분할수 없지만 동일한 기능으로 사용할수 있으므로 다형성의 특징이 있습니다.

 

요점 정리

Provider, 프록시 사용의 핵심은 객체 조회를 필요한 시점까지 지연처리 할수있다는 것이고 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할수 있는데 이것이 다형성과 DI 컨테이너가 가진 장점입니다.

 

주의점

request 스코프는 싱글톤 스코프를 사용하는것 같지만 다르게 동작하기 때문에 사용에 주의해야 하고 request 스코프 같은 특별한 스코프는 반드시 필요한곳에서만 최소화로 사용하며 무분별한 사용은 유지보수가 어려워집니다.

 

 

 

 

테스트 소스(GitHub)

 

GitHub - rlatjd1f/spring-demo-study: spring study

spring study. Contribute to rlatjd1f/spring-demo-study development by creating an account on GitHub.

github.com

출처 : 인프런 - 우아한 형제들 기술이사 김영한의 스프링 완전 정복 (스프링 핵심원리 - 기본 편)

반응형

댓글