웹 스코프

namkun·2022년 3월 31일
0

Spring

목록 보기
17/18

해당 내용은 '스프링 입문을 위한 자바 객체 지향의 원리와 이해'와 인프런 김영한님의 '스프링 핵심 원리 - 기본편' 강의를 참고하였습니다.


웹 스코프

이전에는 싱글톤, 프로토타입에 대해 알아보았으니 이제는 웹 스코프에 대해서도 알아보자.

웹 스코프의 특징은 다음과 같다.

  • 웹 환경에서만 동작
  • 스프링이 해당 스코프의 종료시점까지 관리, 즉 종료 메서드 호출.

웹 스코프는 다음과 같은 종류가 있다.

  • request
    • http 요청 하나가 들어오고 나갈때까지 유지되는 스코프.
    • 각각의 http 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.
  • session
    • http session과 동일한 생명주기를 갖는 스코프
  • application
    • 서블릿 컨텍스트(servletContext) 와 동일한 생명 주기를 갖는 스코프
  • websocket
    • 웹 소켓과 동일한 생명주기를 갖는 스코프

이러한 웹 스코프는 요청이 들어올 때마다, 해당 요청 전용으로 만들어진다.

한 번 만들어 보자.

request 스코프 예제

웹 스코프의 종류는 많지만, 그 중 하나만 해보자.

시작하기 전에..

웹 스코프는 웹 환경에서만 동작하기에 web 환경이 동작하도록 build.gradle에 라이브러리를 추가한다.

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

이제 main 메서드를 실행하면 내장 톰캣 서버를 활용해서 웹 애플리케이션을 실행 시킬 수 있다.

request 스코프

동시에 여러 http 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 힘들다.

이럴때 사용하기 좋은 것이 request 스코프이다.

우선 로그를 남기기 위한 클래스를 만들어보자.

MyLogger.java

@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 msg){
        System.out.println("[" + uuid + "]" + "[" + requestURL + "] " + msg);
    }

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

위의 코드를 보면 @Scope(value = "request") 를 사용해서 request 스코프로 지정했다.

이렇게 되면 위 클래스 빈은 HTTP 요청 하나당 하나가 생성되며, 요청이 끝나는 시점에 소멸된다.

또한, requestURL은 이 빈이 생성되는 시점에는 알 수 없기에 외부에서 setter로 주입받도록 하였다.

그 다음에는 데모용 컨트롤러와 서비스 클래스를 생성한다.

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";
    }
}
  • 위 클래스는 로거가 잘 작동하는지 확인하기 위한 데모용 컨트롤러다.
  • 여기서 HttpServletRequest를 통해서 요청 URL을 받는다.
    • ~~/log-demo
  • 이렇게 받은 요청 URL 값을 MyLogger에 저장한다.
    • MyLogger는 HTTP 요청마다 각각 구분되므로 다른 HTTP 요청에 의해 값이 섞이거나 하지 않는다.

LogDemoService.java

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;

    public void logic(String id) {
        myLogger.log("service id = " + id);
    }
}
  • 서비스 계층에서도 로그를 남긴다.

사실...굳이 request 스코프를 사용하지 않고 파라미터로 모든 정보를 서비스 계층에 넘긴 뒤에 로그를 작성해도 동일한 결과 값이 나올 것이다.

그러나 그렇게 하지 않는 이유는, request URL과 같은 정보가 웹과 관련없는 서비스 계층까지 넘어가게 되기때문이다.

웹과 관련된 부분은 컨트롤러까지만 사용해야 한다. 서비스 계층은 웹 기술에 종속되지 않고, 가급적 순수하게 유지하는 것이 유지보수 관점에서 좋다.

자 이제 실행해보자.

는 에러

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'가 활성화되지 않았습니다.싱글톤에서 참조할 경우 이 빈에 대해 스코프 프록시를 정의하십시오.

request 스코프 빈의 특성때문에 에러가 발생할 수 밖에 없다.

스프링 애플리케이션을 실행함과 동시에 우리가 짜놓은 코드때문에 MyLogger에 자동 주입이 발생하는데, request 스코프 빈은 실제 외부의 요청이 와야만 생성되기 때문에 이런 에러가 발생한 것이다.

이런걸 이제 어떻게 해결할까?

이 MyLogger이라는 빈의 생성을 일단 늦춰야한다. 왜냐면 생성될때 주입 받으면 저렇게 서버가 뜨지도 않으니까.

이전에 배웠던 ObjectProvider를 사용해보자.

LogDemoController.java

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
//    private final MyLogger myLogger;
    private final ObjectProvider<MyLogger> myLoggersProvider;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request){
        MyLogger myLogger = myLoggersProvider.getObject();
        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;
    private final ObjectProvider<MyLogger> myLoggerProvider;

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

MyLogger를 주입받는게 아니라, ObjectProvider<MyLogger> 를 주입받는다.

이 덕분에 ObjectProvidergetObject()를 최초로 호출하는 시점까지 request scope 빈은 생성되지 않는다.

getObject()을 호출하는 시점에서는 HTTP 요청이 진행중이기에 request 스코프 빈이 정상적으로 생성된다.

LogDemoControllerLogDemoService에서 각각 호출했지만, 결국 호출되는 HTTP Request가 같은 Request면 같은 스프링 빈이 반환된다.

스코프와 프록시

프록시라는 걸 사용해보자.

코드는 provider를 사용하기 전의 코드로 돌려놓은 뒤, 다음과 같이 코드에 몇 글자 를 추가해주자.

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
    ...
  • 참고로 적용 대상이 클래스이면 TARGET_CLASS

    인터페이스면 INTERFACES로 선택하면 된다.

  • 위와 같이 적용하면 MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다.

이게 무슨소리냐고?

직접 sout 을 찍어서 확인해보자.

System.out.println("myLogger = " + myLogger.getClass());

결과는?

myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$13aed967

$$EnhancerBySpringCGLIB$$...오랜만에 보지 않는가?

스프링이 CGLIB라는 라이브러리를 통해서 내 클래스를 상속받은 가짜 프록시 객체를 만든 것이다.

프록시를 사용한 원리는 다음과 같다.

우선 스프링 컨테이너가 생성될때 스프링 컨테이너가 비어있는 MyLogger 껍데기(가짜 프록시 객체)를 생성해서 의존성을 주입한다.

이렇게 생성된 가짜 프록시 객체는 해당 객체에 대한 요청이 오면 그때 provider가 한 것처럼 내부에서 진짜 빈을 찾아서 요청한다.

진짜 우리가 아는 프록시 마냥 작동을 한 것이다.

클라이언트 요청 -> 프록시 -> 진짜 객체의 메서드

추가로, 가짜프록시 객체는 내부의 request 스코프랑은 상관없다.

그냥 단순하게 프록시 객체 내부에 위임 로직이 있고, 싱글톤 처럼 동작한다. ( 그렇다고 진짜 싱글톤은 아니니...주의해서 사용하자.)

꼭 웹 스코프가 아니더라도 프록시는 언제든 사용할 수 있다.

정리

사실 중요한건 뭘 사용하였는가? 가 아니라

진짜 객체 조회를 필요한 시점까지 지연 처리할 수 있다는 것이다.

profile
개발하는 중국학과 사람

0개의 댓글