개발자로 후회없는 삶 살기

spring PART.request scope 본문

[백엔드]/[spring | 학습기록]

spring PART.request scope

몽이장쥰 2023. 4. 2. 23:25

서론

※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.

https://www.inflearn.com/roadmaps/373

 

우아한형제들 최연소 기술이사 김영한의 스프링 완전 정복 - 인프런 | 로드맵

Spring, MVC 스킬을 학습할 수 있는 개발 · 프로그래밍 로드맵을 인프런에서 만나보세요.

www.inflearn.com

 

 

본론

 

- 웹 스코프

=> 종류

 

1) request : 서버에 사용자가 접속을 하는 등 http 요청이 들어오면 서버를 타고 응답이 오는데 들어오고 나가기까지가 request 스코프의 범위입니다. HTTP 요청마다 각각 스코프를 가지고 각각의 HTTP 요청마다 별도의 인스턴스가 생성되고 관리됩니다.

그림을 보면 만약 클라 A, B가 동시에 요청을 했더라도 다른 스프링 빈이 생성이됩니다.

 

-> 예시

클라 A가 요청을 하면 컨트롤러에서 request scope와 관련된 객체를 조회를 합니다.(여기서 조회가 요청입니다.) 로그를 찍는 거라면 A 클라 전용의 객체가 만들어집니다. 그리고 서비스 객체에서 또 request scope를 조회하면 http request가 같으면 같은 객체를 바라봅니다. 클라 B가 동시에 들어오면 다른 http request면 완전 다른 별도의 request scope를 생성합니다.

 

2) session : HTTP 세선과 동일한 생명주기

3) application : 서블릿 컨텍스트와 동일한 생명주기

4) websocket : 웹 소켓과 동일한 생명주기

 

 

- request 스코프 예제

웹 스코프는 웹 환경에서 동작하므로 웹 환경 라이브러리를 gradle에 추가합니다. 메인 메서드인 core app을 실행하면 예전에는 없던 톰캣이 생겼습니다. 8080에서 오류 페이지가 나오면 된 것입니다. 웹 라이브러리가 없으면 지금까지 AnnotationConfigApplicationContext를 기반으로 컨테이너를 구성했는데 웹 라이브러리가 있으면 AnnotationConfigServletWebServerApplicationContext를 기반으로 구동합니다.

 

- 예제 개발

만약에 동시에 여러 http 요청이 온다면 로그를 남길 건데 어떤 요청이 남긴 로그인지 구분하기 어렵습니다. 이럴때 사용하기 좋은 것이 request 스코프입니다.

@Component
@Scope("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 + "] create" + this);
    }

    @PreDestroy
    public void close() {
        System.out.println("[" + uuid + "] close " + this);
    }
}

 

UUID를 붙인 로그가 남도록 하여 같은 request랑 다른 request를 구분하도록 할 것입니다. URL도 같이 남겨서 어떤 URL로 요청을 한 건지도 보겠습니다. (UUID로 어떤 http 요청인지 구분하고 어떤 URL로 요청한 건지 구분)

 

core 하위에 패키지를 만듭니다. MyLogger 클래스를 만들고 request 스코프를 따르게 합니다. 필드로 uuid와 URL을 가진다고 했고 URL은 나중에 별도로 넣기 위해 세터를 만듭니다. 그리고 로그를 남길 때 log 메서드로 로그를 남기도록합니다.

> 초기화 메서드로 uuid를 random으로 만들어서 빈 생성과 등록, DI, 초기화 중 초기화를 하여 무거운 로직이 실행될 준비를 합니다. 지금 배우려고 하는 것이 uuid가 반드시 있어야 하는 것이라서 초기화를 하는 것입니다. uuid를 저장해두면 같은 HTTP 요청의 request scope 빈의 uuid는 컨트롤러에서 호출하든 서비스에서 호출하든 동일하다.

> request 스코프는 요청이 나가면 destroy가 됩니다. 고객 요청이 들어올 때 필요한 것을 초기화에 작성하여 호출하고 스프링이 관리하다가 고객 요청이 나가면서 close를 호출하여 소멸됩니다.

얘가 로그를 출력하기 위한 클래스로 스코프를 req를 사용해서 이 빈은 HTTP 요청하는 시점에 HTTP 요청 당 하나씩 생성되고 HTTP 요청이 끝나는 시점에 소멸됩니다. URL은 생성되는 시점을 알 수 없고 외부에서 들어올 것이므로 수정자를 만듭니다.

 

-> 컨트롤러

@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("testI d");
        return "OK";
    }
}

@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final MyLogger myLogger;
    public void logic(String testId) {
        myLogger.log("service id = " + testId);
    }
}

web 패키지를 만들고 컨트롤러를 만듭니다. @Controller를 하고 서비스와 로거를 RequiredArgsCons로 생성자 주입을 합니다. 서비스에서 레포를 썼던 거처럼 컨트롤러에서 서비스를 사용한다는, 의존한다는 목적으로 가져온 것입니다.

request mapping을 만들고 화면이 없는 view가 없는 것으로 할 거라서 resposebody를 만듭니다. request의 getRequestURL을 하면 고객이 어떤 URL로 요청했는지 알 수 있습니다. 이걸로 로거의 URL을 set합니다. set했으니 log를 남깁니다.

서비스에서도 로직이 호출할 거라고 했습니다. testId를 파라미터로 넘기는 로직을 작성합니다. return은 responsebody니깐 보낼 문자인 OK를 씁니다.

서비스의 로직을 작성합니다. 의존관계를 로거를 받고 로직에서 로거의 로그를 실행합니다. > 또한 이렇게 하면 서비스에서의 의존관계 주입도 너무 이해가 잘 됩니다. 서비스에서 로거를 실행할 것이라서 로거를 주입받습니다.

 

-> 실행

실행해보면 서버가 뜨지도 않고 오류가 납니다. 오류가 나는게 정상입니다. 오류가 어디서 나는지 찾아보면 마이 로거의 request 스코프가 active되지 않았습니다. 뭐냐면 스프링 컨테이너가 뜹니다. 컨트롤러에서 두개를 의존관계 주입을 받습니다. 스프링 컨테이너가 뜰 때 컨트롤러를 등록을 할 것이고 그 때 의존관계 주입이 일어납니다. 주입이 일어나면 컨테이너한테 로거를 내놓으라고 하는데 문제가 있습니다. 로거의 스코프가 request라서 내놓으려고 하는데 request가 없는 것입니다. 얘의 생존 범위는 고객 요청이 들어와서 나갈 때까지인데 지금 들어오지도 않았으니 주입이 안되서 난 오류입니다. scope is not active입니다.

 

-> 정리

서버에 요청을 하면 localhost:8080/log-demo라고 요청을 할 것입니다. 이렇게 받은 URL은 로거에 저장이 됩니다. 로거는 http 요청당 각각 구분되므로 다른 http 요청 때문에 값이 섞이는 걱정을 하지 않아도 됩니다.

> 비즈니스 로직이 있는 서비스 계층에서도 로그를 출력할 것입니다. 여기서 중요한 점이 request scope인 로거를 사용하지 않고 파라미터로 이 모든 정보를 서비스 계층에 넘길 수 있습니다. 근데 파라미터가 너무 많아서 지저분해지고 서비스 계층은 웹 기술에 종속되지 않고 순수하게 유지하는 것이 유지보수(단일 책임)  관점에서 좋으니 웹과 관련된 부분은 컨트롤러까지만 하는게 좋습니다. 로거 덕분에 이 부분을 파라미터로 넘기지 않고 주입해서 깔끔하게 유지할 수 있습니다.

 

또한 기대하던 출력은 이러한데 오류가 발생했습니다. 웹 다 빼고 볼 때 스프링 컨테이너가 뜰 때 컨트롤러를 보고 로거를 주입해야 하는데 http 요청 자체가 안와서 로거가 스프링 컨테이너에 등록이 안 되어있습니다. 이 빈은 실제 고객의 요청이 와야 실행할 수 있습니다.

> 결국 ★ 스프링 컨테이너한테 "주입할 빈을 주세요"하는 단계를 의존관계 주입 단계가 아니라 실제 고객 요청이 왔을 때로 미뤄야합니다. 그것을 앞에서 배운 provider를 쓰면 해결이 됩니다.(프로토타입에서는 provider가 호출해야 그제서야 생기는 프로토 빈을 getObject로 직접 생성하였습니다. provider 덕분에 getObject를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있습니다.)

 

- 스코프와 provider

//프로바이더 사용 후
public class LogDemoController {
    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> myLoggerObjectProvider;

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

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

첫번째 해결 방법은 앞서 배운 Provider를 쓰는 것입니다. 로거에 ObjectProvider를 넣습니다. 이러면 로거를 주입하는게 아니라 로거를 찾을 수 있는 DL할 수 있는 애가 주입이 됩니다.(아까는 프로토를 찾을 수 있는 애를 주입했고 프로토는 get으로 호출할 때 생성이 되니 getObject로 호출했었다.)

> ★ 얘는 주입 시점에 주입을 받을 수 있습니다. 로거 프로바이더의 getObject를 해서 로거를 받습니다. 돌려보면 또 똑같은 오류가 발생합니다. 서비스에서도 주입 시점에 오류가 안나게 프로바이더로 고칩니다. 이렇게 하면 컨트롤러에 http가 살아있는 상태에서 고객의 요청을 받을 수 있는 상황이 됩니다.

 

 

 

 

-> 출력

로그 4개가 한 고객의 요청입니다. 한 고객의 create, close가 다 나옵니다.

 

 

-> 정리

프로바이더가 get을 최초로 하는 시점에 로거가 처음 만들어지고 그때 빈이 생성되고 등록되고 초기화되면서 로거의 init 메서드에서 uuid를 만들고 로거에 set을 해서 url을 담고 로그를 찍습니다. 동시에 여러 요청이 오더라도 요청마다 각각 다르게 처리하는게 핵심입니다.

프로바이더 덕분에 getobject를 호출하는 시점까지 request scope 스프링 컨테이너에게 주입을 요청하는 것을 지연할 수 있었습니다.

> 또한 컨트롤러에서 log를 찍던 서비스에서 log를 찍던 같은 http 요청이면 같은 uuid를 가지는 것을 알 수 있었습니다. 쓰레드로 직접해야하는 것을 request scope로 굉장히 쉽게 할 수 있게 된 것입니다.

 

 

- 스코프와 프록시

// 전
@Scope("request")

// 후
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)

프로바이더를 적는 것도 귀찮습니다. 처음에 하던 오류나던 코드처럼 할 수 있는 방법이 있습니다. 로거의 scope에 proxyMode를 줍니다. scopedProxyMode를 target_class로 줍니다. 그리고 오류나던 코드로 바꿉니다.

> 원래는 이렇게 하면 오류가 났었습니다. 근데 잘 동작합니다. 마치 프로바이더를 쓰는 것과 똑같이 동작합니다.

 

- 프록시 모드

이렇게 하면 가짜 프록시 클래스를 만들어서 주입을 해줍니다. 마치 프로바이더를 만들어 주입하듯이 진짜가 아닌 로거를 상속받은 가짜를 주입합니다. 이것을 sout로 로거를 찍어보면 @configuration 때처럼 cglib이 보입니다. 스프링이 조작해서 만든애가 스프링 빈으로 등록이 되어있습니다. 얘가 로거처럼 동작하는게 아니고 프로바이더로 등록되어 동작하는 것입니다.

 

> 실제 로거를 호출하는 시점에서 진짜를 그 때 찾아서 호출합니다. 이 가짜는 내부에 진짜를 찾는 방법을 알고 있고 실행 호출을 받으면 진짜의 메서드를 호출합니다. 꼭 웹 스코프가 아니더라도 프록시를 사용할 수 있습니다. (프로토타입 빈의 프로바이더처럼)

Comments