본문 바로가기

JAVA/Spring

[Spring] 웹 스코프

728x90

웹 스코프의 특징

  • 웹 스코프는 웹 환경에서만 동작한다.
  • 웹 스코프는 프로토타입과 다르게 스프링이 종료시점 까지 관리한다. (종료 메서드 호출됨)

종류

  • 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 컨테이너의 강점)
  • 꼭 웹 스코프가 아니어도 프록시는 사용할 수 있다.

스코프는 필요한 곳에만 사용하지 않으면 테스트나 유지보수가 까다로워진다.