티스토리 뷰

Spring

[Spring] Web Scope(웹 스코프)

MAENCO 2021. 8. 17. 23:00
반응형
Web Scope

웹 스코프는 웹 환경에서만 동작하는 특징을 가지고 있으며 프로토타입 스코프와 다르게

종료 시점까지 스프링 컨테이너가 관리하기 때문에 종료 메서드를 호출해준다

 

웹 스코프를 사용하는 이유는 클라이언트에게 서비스를 제공하다 문제가 생겼을 때

보통 로그를 확인하여 디버깅을 진행한다

하지만 이때 동시에 접속한 여러 고객들의 요청이 뒤죽박죽 섞여 있으면 아주 힘든 상황이 될 것이다

 

그래서 공통적인 포맷으로 깔끔하게 로그들이 정리되어서 출력되도록 해주는 것이 웹 스코프의 역할이다

 

웹 스코프의 종류는 아래와 같이 다양하다

1. request

HTTP 요청에 하나가 들어오고 나갈 때까지 유지되는 스코프이다

각각의 HTTP요청 마다 별도의 빈 인스턴스가 생성되고 관리된다

 

2. session

HTTP Sessionr과 동일한 생명주기를 가지는 스코프

 

3. application

서블릿 컨텍스트와 동일한 생명주기를 가지는 스코프

 

4. websocket

웹 소켓과 동일한 생명주기를 가지는 스코프

 

스코프들이 가지는 생명주기 범위만 다르지 이 스코프들이 작동하는 방식은 다 비슷하다고 한다

Request Scope의 예제로 이 웹 스코프가 어떻게 작동하는지 알아보자

 

Request Scope

동시에 여러 HTTP 요청이 오게 되면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다 (뒤죽박죽)

이럴 때 Reqeust Scope를 사용하면 로그들을 깔끔하게 만들 수 있다

 

공통적으로 만들 포맷 : [UUID] [requestURL] {message}

 

UUID로 HTTP 요청을 구분하고 requestURL로 어떤 URL을 요청하였는지 로그를 만들어보자

@Component
@Scope(value = "request") //request 스코프로 지정 (HTTP 요청마다 새로운 빈 생성 및 종료)
public class MyLogger {

    private String uuid;
    private String requestURL;
	
    // requestURL을 setter로 만든 이유는 빈이 생성되는 시점에서는 URL을 알수 없기 때문에
    // Controller단에서 요청할때 URL값을 setter로 넣는다
    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    public void log(String message) {
        System.out.println("[" + uuid + "]" + "[" + requestURL + "]" + "{" + message + "}");
    }

    @PostConstruct
    public void init() {
    	// @PostConstruct를 사용하여 빈이 초기화 될때 uuid를 생성하여 필드에 저장
        uuid = UUID.randomUUID().toString();
        System.out.println("[" + uuid + "] request scope bean create" + this);
    }

    @PreDestroy
    public void close() {
    	//@PreDestroy를 사용하여 빈이 소멸될때 문구를 출력한다
        System.out.println("[" + uuid + "] request cope bean close" + this);
    }
}

우선 scope를 request scope로 지정하였다 이렇게 지정하게 되면 이 빈(MyLogger)은

HTTP 요청당 하나씩 생성되고 HTTP 요청이 끝나는 시점에 소멸된다

또한 이 빈이 생성되는 시점에 @PostConstruct초기화 메서드를 사용해 uuid를 생성하고 저장한다

마지막으로 빈이 소멸되는 시점에 @PreDestroy를 사용해서 종료 메시지를 남긴다

 

정리하자면 받은 HTTP Request를 MyLogger에 저장하는 것이다 이때 핵심은 Requset Scope로 지정하였기 때문에

각각 요청이 새로운 빈으로 등록되어 값이 뒤죽박죽 섞이지 않는 걸 보장한다

 

컨트롤러를 만들어 실제로 실행을 시켜보자

@Controller
@RequiredArgsConstructor
public class LogDemoController {

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

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
    	//provider를 사용하여 새로 생기는 객체에 대한 의존성을 찾아 주입할 수 있도록 하였다
        String requestURL = request.getRequestURL().toString();
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.setRequestURL(requestURL);

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

log-demo라는 URL로 접속을 하면 아래와 같이 각기 다른 로그들이 남은걸 확인할 수 있었다

[dbf70530-8829-44b1-aaa9-606a8612900f] request scope bean createhello.core.common.MyLogger@6514fb53
[dbf70530-8829-44b1-aaa9-606a8612900f][http://localhost:8080/log-demo]{controller test}
[dbf70530-8829-44b1-aaa9-606a8612900f][http://localhost:8080/log-demo]{service id =testId}
[dbf70530-8829-44b1-aaa9-606a8612900f] request scope bean closehello.core.common.MyLogger@6514fb53

[04c72a9b-bb23-4add-8200-f59a947ff897] request scope bean createhello.core.common.MyLogger@38bbc63a
[04c72a9b-bb23-4add-8200-f59a947ff897][http://localhost:8080/log-demo]{controller test}
[04c72a9b-bb23-4add-8200-f59a947ff897][http://localhost:8080/log-demo]{service id =testId}
[04c72a9b-bb23-4add-8200-f59a947ff897] request scope bean closehello.core.common.MyLogger@38bbc63a

[00a44ffc-f0a2-45f1-98b5-7467b6db95f4] request scope bean createhello.core.common.MyLogger@2e4b890f
[00a44ffc-f0a2-45f1-98b5-7467b6db95f4][http://localhost:8080/log-demo]{controller test}
[00a44ffc-f0a2-45f1-98b5-7467b6db95f4][http://localhost:8080/log-demo]{service id =testId}
[00a44ffc-f0a2-45f1-98b5-7467b6db95f4] request scope bean closehello.core.common.MyLogger@2e4b890f

 

즉 클라이언트가 동시에 요청을 한다고 해도 아래와 같이 각기 다른 빈으로 다른 로그를 남기게 하는 것이다

위의 예시에서는 provider를 사용하였는데 이보다 더 나은 방법인 proxy를 알아보자

 

Proxy

아래와 같이 프록시 모드를 설정하면 프록시를 만들어 동작하게 되며

기존에 provider를 사용했을 때처럼 정상적으로 동작한다

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

이렇게 프록시로 선언한 객체를 출력해보면 아래와 같이 나온다

myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$2e9b720f

CGLIB 즉 스프링 컨테이너가 나의 클래스를 상속 받아 가짜 프록시 객체를 만든 것인데

이렇게 가짜 객체를 스프링 컨테이너에 등록하여 의존관계를 주입할 때도 이 가짜 프록시 객체를 주입하게 된다

가짜 프록시는 그 객체의 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직을 가지고 있다는 것이다

클라이언트가 요청하는 myLogger는 Proxy 즉 가짜를 호출한 것이고, 이를 다형성을 통해 진짜 같이 사용하는 것이다

그리고 실제 요청이 들어왔을 때 실제 빈을 호출한다

또한 이렇게 생성된 Proxy 객체는 reqeust scope와도 연관이 없다 그저 싱글톤처럼 동작하는 가짜 객체일 뿐이다

프록시(provider도 마찬가지)의 핵심은 실제 요청이 올 때까지 진짜 객체의 조회를 지연시킨다는 것이다

(프록시는 웹 스코프뿐만 아니라 AOP같은 경우에도 활용하여 사용할 수 있다)

 

핵심정리

웹 스코프를 사용하여 클라이언트가 요청한 로그들을 뒤죽박죽 섞이지 않게 정리할 수 있으며

이를 통해 문제가 발생하였을 때 더욱 용이하게 디버깅을 할 수가 있다

 

웹 스코프의 동작 방식은 비슷해서 request scope로 예시를 만들었을 때

HTTP의 요청부터 종료까지 새로운 객체를 생성하여 관리하기 때문에

동시에 요청이 들어온다 하더라도 각기 다른 객체로 인식하여 로그를 남길 수 있다

 

하지만 이러한 웹 스코프는 꼭 필요한 시점에서만 사용하는 것이 좋다고 한다

너무 남발하게 되면 무수히 많은 로그산에 깔려 유지보수 용이성을 떨어트린다

 

더보기

개인 학습을 위해 작성되는 글입니다.

제가 잘못 알고 있는 점에 대한 지적 / 더 나은 방향에 대한 댓글을 환영합니다.

 

참조 링크:
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

https://kth990303.tistory.com/26

 

 

반응형
댓글