해당 글은 김영한 스프링 핵심 원리 - 기본편을 보고 요약, 정리하며 개인적인 견해가 들어간 글입니다.
스코프와 Provider
저번에 배운 웹 스코프에서 request스코프에 대해 이해하고, 이를 코드로 짜보았다.
request 스코프를 코드로 짜면서 내가 원했던것은 아래와 같다.
- UUID를 사용해 HTTP 요청을 구분할것
- requestURL 정보를 넣어 어떤 URL을 요청해서 남은 로그인지 확인이 가능할것.
하지만, 코드를 짜고 실행하는 과정에서 오류가 발생했다.
그 이유는 스프링을 실행하는 시점에 싱글톤 빈은 미리 생성해서 주입이 가능하나, request 스코프 빈은 아직 생성되지 않았기 때문이다.
위 오류를 우리는 오늘 해결해볼것이다.
Provider 사용
ObjectProvider를 사용해 오류를 해결해보자.
LogDemoController
의 코드를 아래와 같이 수정해준다.
package hello.core.web;
import hello.core.common.MyLogger;
import hello.core.web.LogDemoService;
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) {
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
LogDemoService
의 코드를 아래와 같이 수정해준다.
package hello.core.web;
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);
}
}
이후 CoreApplication
을 실행후 http://localhost:8080/log-demo
를 인터넷에 치면 아래와 같은 결과가 나온다.
실행 결과
인터넷 창에서의 결과
인텔리제이 콘솔에서 출력 결과
이후 창을 새로고침 할 때마다 UUID가 바뀌어서 출력되는게 확인된다.
ObjectProvider
덕분에ObjectProvider.getObject()
를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다.ObjectProvider.getObject()
를 호출하시는 시점에는 HTTP 요청이 진행중이므로 request scope빈의 생성이 정상 처리된다.ObjectProvider.getObject()
를LogDemoController
,LogDemoService
에서 각각 한번씩 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환된다!- UUID사용하면 직접 이걸 구분하는데 들어가는 시간과 힘듦을 없앨 수 있다.
스코프와 프록시
이번에는 프록시 방식을 사용해보자
우선 LogDemoController
에서 MyLogger
로 들어가서 스코프를 아래와 같이 변경해준다.
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
proxyMode = ScopedProxyMode.TARGET_CLASS
를 추가해주자.- 적용 대상이 인터페이스가 아닌 클래스면
TARGET_CLASS
를 선택 - 적용 대상이 인터페이스면
INTERFACES
를 선택
- 적용 대상이 인터페이스가 아닌 클래스면
- 이렇게 하면 MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시클래스를 다른 빈에 미리 주입해 둘 수 있다.
그리고 나머지 코드를 Provider 사용 이전으로 돌려준다.
실행해보면 잘 동작하는게 확인된다.
LogDemoController
, LogDemoService
는 Provider 사용 전과 완전히 동일하다. 어떻게된 것일까?
웹 스코프와 프록시 동작 원리
주입된 myLogger를 확인하기 위해 LogDemoController
에 아래 코드를 추가해준다.
System.out.println("myLogger = " + myLogger.getClass());
실행하면 아래와 같은 결과가 나온다.
@Scope
어노테이션의proxyMode = ScopedProxyMode.TARGET_CLASS
를 설정하면 스프링 컨테이너는 CGLIB라는 바이트코드 조작 라이브러리를 사용하여 원본 클래스를 상속받은 가짜 프록시 객체를 생성한다.- 스프링은
MyLogger
클래스를 대신하여MyLogger$$EnhancerBySpringCGLIB
이라는 가짜 프록시 클래스를 생성하고, 이 클래스의 객체를 스프링 컨테이너에 등록합니다. 따라서ac.getBean("myLogger", MyLogger.class)
로 조회할 때도 실제로는 가짜 프록시 객체가 반환된다. - 의존관계 주입(Dependency Injection)도 이 가짜 프록시 객체가 주입된다.
다른 빈에서MyLogger
를 의존하고 있을 때, 실제로는 가짜 프록시 객체가 주입되어 요청 범위의MyLogger
빈을 사용할 수 있게 된다. - 이를 통해 스프링은 요청 범위(request scope)의 빈을 싱글톤 빈처럼 사용할 수 있도록 지원하고, 필요한 시점에 해당 빈의 인스턴스를 생성하고 주입할 수 있게 된다.
- 가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
- 가짜 프록시 객체는 내부에 진짜 myLogger를 찾는 방법을 알고 있다.
- 클라이언트가
myLogger.logic()
을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이다. - 가짜 프록시 객체는 request 스코프의 진짜
myLogger.logic()
를 호출한다. - 가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 원본인지 아닌지도 모르게, 동일하게 사용할 수 있다(다형성)
동작 정리
- CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.
- 이 가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다.
- 가짜 프록시 객체는 실제 request scope와는 관계가 없다. 그냥 가짜이고, 내부에 단순한 위임 로직만 있고, 싱글톤 처럼 동작한다.
특징 정리
- 프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 있다.
- 사실 Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다.단지 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 이것이 바로 다형성과 DI 컨테이너가 가진 큰 강점이다.
- 꼭 웹 스코프가 아니어도 프록시는 사용할 수 있다.
주의점
- 마치 싱글톤을 사용하는 것 같지만 다르게 동작하기 때문에 결국 주의해서 사용해야 한다.
- 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자, 무분별하게 사용하면 유지보수하기 어려워진다.
'개인 공부 > 인프런' 카테고리의 다른 글
[HTTP] URI & Web browser request flow (0) | 2023.06.01 |
---|---|
[HTTP] HTTP 기본 개념 정리 (0) | 2023.05.31 |
[Spring] Web Scope (0) | 2023.05.14 |
[Spring] Prototype Scope (0) | 2023.05.10 |
[Spring] Prototype Scope - Issues When Used with Singleton Beans (0) | 2023.05.03 |