ObjectProvider, 프록시

김민우·2022년 8월 9일
0

잡동사니

목록 보기
2/22

빈 스코프 공부를 하면서 제일 어려웠던 부분을 다시 정리해보았다.

웹 스코프를 이용한 예제를 보자.

request 스코프 예제 개발

동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다.
이럴때 사용하기 딱 좋은것이 바로 request 스코프이다.

다음과 같이 로그가 남도록 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
  • 기대하는 공통 포멧: [UUID][requestURL] {message}
  • UUID를 사용해서 HTTP 요청을 구분하자.
  • requestURL 정보도 추가로 넣어서 어떤 URL을 요청해서 남은 로그인지 확인하자.

로그를 출력하기 위한 클래스 - MyLogger

import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.UUID;

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

로거가 잘 작동하는지 확인하기 위한 테스트용 컨트롤러 - LogDemoController

import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) throws Exception {
        String requestURL = request.getRequestURL().toString();

        myLogger.setRequestUrl(requestURL);
        myLogger.log("controller test");

        logDemoService.logic("testId");
        return "OK";
    }
}
  • 여기서 HttpServletRequest를 통해서 요청 URL을 받았다.
    • requestURL 값 http://localhost:8080/log-demo
  • 이렇게 받은 requestURL 값을 myLogger에 저장해둔다. myLogger는 HTTP 요청 당 각각 구분되므로 다른 HTTP 요청 때문에 값이 섞이는 걱정은 하지 않아도 된다.

LogDemoService 추가

import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;

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

이제 이를 실행해보자.

기대하는 출력

[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

실제 출력

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 스코프 빈인 MyLogger 객체는 요청이 와야 생성이 되므로 의존관계 주입이 안되기 때문이다.

이에 대한 해결책 2가지를 이제 알아보자.


1. ObjectProvider

ObjectProvider을 이용한 LogDemoController

import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

@Controller
@RequiredArgsConstructor
public class LogDemoController {

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

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

        myLogger.setRequestUrl(requestURL);
        myLogger.log("controller test");

        logDemoService.logic("testId");
        return "OK";
    }
}

ObjectProvider을 이용한 LogDemoService

import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final ObjectProvider<MyLogger> myLoggerProvider;

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

실행해보면 잘 작동하는 것을 알 수 있다. 이유가 뭘까?
기존 의존관계에서 LogDemoServiceLogDemoControllerMyLogger에 의존하는 것에서 ObjectProvider<MyLogger>에 의존하는 것으로 바뀌었기 때문이다.

그렇게 의존관계가 주입된 후 logic() 메소드가 실행되는 시점에는 비로소 MyLogger객체가 들어온 것이 확실하므로 (logic()은 요청이 와야 실행되니깐)
MyLogger myLogger = myLoggerProvider.getObject(); 를 통해 실제로 사용할 MyLogger 객체를 호출했다.

자, ObjectProvider 덕분에 ObjectProvider.getObject() 를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다.


2. 프록시

프록시를 이용한 MyLogger 추가

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
  	public class MyLogger {
    ...
}
  • 기존 코드의 @Scope 속성에 proxyMode = ScopedProxyMode.TARGET_CLASS) 추가

나머지 코드는 ObjectProvider 사용 전으로 돌리자

자 그러면 실행이 된다! 이유가 뭘까?

MyLogger객체는 스프링 애플리케이션이 시작하는 시점에 만들어지지 않는데 프록시 속성이 추가되어 CGLIB라는 라이브러리에서 해당 클래스를 상속 받은 가짜 프록시 객체를 만들어서 실제 MyLogger 역할을 대신한다.(의존관계에 주입 등)

중요한 건 이렇게 만들어진 가짜 객체요청이 들어왔을 때 내부의 진짜 빈을 요청하는 위임 로직이 들어있다. (즉, 내부의 실제 MyLogger를 찾는 방법을 알고 있다.)

요청이 오고나서야 실행되는 logic() 메소드가 실행되면 (요청이 들어왔으므로) 가짜 프록시 객체는 진짜 myLogger.logic()을 실행한다.

가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 원본인지 아닌지도 모르게, 동일하게 사용할 수 있다(다형성)


둘의 차이?

사실 Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다. 즉, 의존관계 주입시 실제 객체가 아닌 대체 객체(?)를 주입하고 의존관계를 스프링이 직접 찾도록 하는 방식(DL)이라는 것이다.

이제 둘의 차이를 간단히 알아보자.

코드의 간결성만 본다면 상대적으로 프록시가 매우 간결하다. 그러나 마치 싱글톤을 사용하는 것 같지만 다르게 동작하기 때문에 결국 주의해서 사용해야 한다.

이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자, 무분별하게 사용하면 유지보수하기 어려워진다. (개발은 혼자하는게 아니니깐)

0개의 댓글