[기본기] 9-4. 웹 스코프, ObjectProvider

khyojun·2022년 10월 14일
0
post-thumbnail

본 게시글은 김영한님의 스프링 핵심 원리 기본편을 정리한 글입니다.


이전 시간까지는 싱글톤, 프로토타입 빈에 대해서 계속 알아보았는데 이제는 웹 스코프에 대해서 한 번 알아보자.

📌 웹 스코프

특징

  • 웹 스코프는 웹 환경에서만 동작을 한다.
  • 웹 스코프는 프로토타입과는 달리 스프링이 해당 스코프의 종료시점까지 관리를 하게 된다. 따라서 그냥 없어지는 프로토타입에 반해 종료메서드가 호출이 된다.

종류

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

request와 나머지 3개의 스코프는 범위만 다르지 동작 방식은 비슷하여 예제에서는 Request 스코프를 중심으로 다루기로 한다.

우리가 위에서 말했던 별도의 빈 인스턴스가 생성이되고 관리가 되어진다는 말이 뭘까?

위 그림을 보면 이제 A,B라는 사람이 각각의 요청을 보내는데 뭐 myLogger라는 요청을 보낼때 A는 빨강색, B는 파랑색의 선으로 표시가 되어있다. 이 말인 즉슨 이제 A가 요청한 request scope빈은 A의 것만 이제 B의 것은 B의 것만 따로 보게 된다는 것이다.
(선의 색깔따라 나뉘는 Request마다 나뉘는 것으로 생각하면 될 거 같다.)

예시 코드

이제 코드를 적어나가기전에 먼저 dependency를 추가해야되는 것이 하나 있다.

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

를 추가하여서 SpringBoot의 내부 톰캣 서버를 활용해야 한다.

우리가 이번에 작성할 코드는 request 스코프 예제 개발을 위한 코드이다. 예상하는 결과는 다음과 같다.

[d0239ef..] request scope bean create
[d0239ef..] [http://localhost:8080/log-demo] controller test
[d0239ef..] [http://localhost:8080/log-demo] service id = testId
[d0239ef..] [http://localhost:8080/log-demo] request scope bean close

여기서 앞의 숫자들은 UUID라고 한다. Universally unique identifier 의 약자라고 하는데 간단히 번역하자면 아주 독특한 id 즉 하나밖에 없는 id라는 것이다. 그래서 이를 활용하여서 HTTP요청을 구분할 것이다.

📂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 destroy(){
        System.out.println("[ " + uuid + "] request scope bean close:" + this);
    }

}

MyLogger를 통하여서는 로그를 출력한다. 여기서 uuid,url들을 받아와 로그를 출력할 것이다. 그리고 이 친구는 @Scope(value = "request")이다. 잘 기억하자. 이 친구의 스코프는? 요청이 들어왔을때부터 요청이 끝날때까지다. 잘~~ 기억하자! 그리고 여기서 requestURL은 빈이 생성되는 시점에 알 수 없으니까 setter로 받아오기로 한다.

📂LogDemoService

@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final MyLogger myLogger;

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


}

그리고 이 친구는 비즈니스 로직을 담당하는 Service 친구이다. 너무 간단해서 다른 곳에 끼워도 되지 않을까? 싶지만 실제로 비즈니스 로직이 이렇게 간단할 일도 없고 다른거랑 섞이면 되게 유지 보수하기 복잡해진다. 그러니까 이렇게 역할을 각각 분리하여서 놔둬놓도록 하자.

📂LogDemoController

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;


    @RequestMapping("log-demo")
    @ResponseBody
    public void logDemo(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String requestURL = request.getRequestURL().toString();
  myLogger.setRequestURL(requestURL);

  myLogger.log("controller test");
  logDemoService.logic("testId");
        response.getWriter().write("ok");
    }
}

로거가 잘 작동하는지 확인하기 위한 컨트롤러인데 여기서 HttpServletRequest를 통하여 요청 URL을 받게 된다. http://localhost:8080/log-demo 이 친구가 오게 된다. 그리고 myLogger에 URL을 저장하고 요청 당 각각 구분이 될것이다. 위에서 말했던 대로 각각의 요청마다 인스턴스가 다르기 때문이다. 여기에도 controller 로그를 남긴다.

그래서 이렇게 다 만들고 이제 실행을 하면 아~~~주 잘 될 거 같다. 그런데 막상 실행하면

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;

이런 오류가 뜨게 되는데 이 말을 간단히 해석해보자면 myLogger가 request스코프라서 실제로 쓰레드에 작동되고 있지 않는다고 한다. 위에서 언급한대로 요청이 들어오기 전에 미리 의존관계 다 넣고 빈 다 생성하고 해야하는데 myLogger는 실제로 생성되지 않았기에 다른 곳에 @Autowired로 주입을 할려는데 "어! scope가 request라 아직 생성이 안됬네?" 하고 컴파일 조차 되지 않게 막아버린 것이다.

해결을 위한 ObjectProvider

이전 빈 스코프때에도 나타났던 친구였다. DL을 하기 위하여서 사용을 하였는데 이번에도 이 친구가 도움을 줄 예정이다. 이 ObjectProvider를 그러면 MyLogger라는 타입의 친구에게 다 씌워주자.

📂LogDemoService

@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final ObjectProvider<MyLogger> myLoggerProvider;

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


}

📂LogDemoController

@Controller
@RequiredArgsConstructor
public class LogDemoController {

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

    @RequestMapping("log-demo")
    @ResponseBody
    public void logDemo(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String requestURL = request.getRequestURL().toString();
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        logDemoService.logic("testId");
        response.getWriter().write("ok");
    }
}

이제 이렇게 코드를 진행하게 되면?

정상적으로 작동이 되었다. 그러면 이 ObjectProvider가 무슨 역할을 하였을까?
스프링 래퍼런스를 확인하게 되면 ObjectProvider에 대해서 다음과 같이 말을 한다.

주입 지점을 위해 특별히 설계된 변형으로 ObjectFactory프로그래밍 방식의 선택 사항과 관대하지 않은 취급이 가능합니다.

ObjectProvider는 ObjectFactory를 상속받은 친구이다. 이 말을 통해 본 바 ObjectProvider는 어찌보면 빈을 생성하는데 어떤 의미에서는 주입시점을 바꿔버렸다는 것을 알 수 있다. 그리고 생성시점조차도. 코드를 통해 보게 되면 .getObject()를 하게 된 시점부터 빈을 생성을 하고 주입을 하게 된다. 즉, 빈 생성을 지연시킨 셈이다!

정리

  • ObjectProvider를 통하여 빈 생성을 지연시켰다.
  • 그로 인해 request가 오고 난 뒤에 MyLogger빈이 생성되었다.
  • 이제 정상적으로 코드들이 돌아가게 된다.

근데 실은 이렇게도 해도 되지만 개발자들은 이보다 더 코드를 줄여보기 위한 방법들을 고안하게 되었다고 한다. 그건 다음시간에...

오늘의 결론

Scope("request")의 범위를 잘 알고 각각의 인스턴스가 요청마다 다르다는 것을 잘 인지하자.

ObjectProvider를 통하여서 빈 생성을 지연시켜 웹 스코프에 알맞도록 생성 지점을 특정시키자.

출처

  1. 김영한님의 스프링 핵심 원리 기본편(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)
  2. https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/ObjectProvider.html
profile
코드를 씹고 뜯고 맛보고 즐기는 것을 지향하는 개발자가 되고 싶습니다

0개의 댓글