지난시간 :
MyLogger는 request scope 빈이다.
∴ Controller 생성시점에 MyLogger는 존재하지 않기 때문에 단순히 private final MyLogger myLogger 처럼만 멤버로 두면 문제가 생긴다.
∴ Provider나 Proxy를 사용할 수 있다.
● Provider 적용
MyLogger를 주입하는게 아니라,
MyLogger 찾을 수 있는 (Dependency Lookup할 수 있는) Provider를 주입하게 된다.
∴ MyLogger가 생성되는 시점(HTTP요청 들어올 때)에 주입받을 수 있게 된다!
∨ Controller의 멤변 myLogger는 request scope 빈이기 때문에 Provider를 적용하자 (ObjectProvider)
그럼 Controller에 실제 HTTP 요청이 들어왔을 때에 맞춰 주입할 수 있다!
∨ Service의 로직에서도 MyLogger를 사용하고 있으므로
Service에도 단순 MyLogger를 쓰지 말고 Provider를 적용해주자.
private final MyLogger myLogger;
↓
private final ObjectProvider<MyLogger> myLoggerProvider;
▽ 변경 이후
∨ Controller
@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";
}
}
사용자의 http request가 들어오는 logDemo() 시점에 맞춰
Provider를 통해 myLogger를 꺼내 쓸 수 있게 된다!
완존 짱!!
∨ 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);
}
}
여기서도 myLogger 말고 Provider를 적용해주자.
일단 myLogger는 request scope 빈이니까 기본적으로 Provider 같은 걸로 DL해주는 게 맞다고 떠올리면 되겠다.
이전에는 Controller의 멤버로 request scope 빈이 들어가 있어서 DI 관련 오류가 났기 때문에 스프링부트가 꺼졌다.
이번에는 request scope 빈을 Provider를 통해 적절시기에 생성해 썼기 때문에 스프링부트가 정상적으로 실행되었다.
매핑한 url인 http://localhost:8080/log-demo로 들어가보면 return "OK"; 결과를 확인가능하다.
로그도 잘 찍힌다.
내가 http 요청 한번 할 때마다 아래와 같은 4줄이 뜬다.
MyLogger 생성되어 초기화콜백 실행되고,
컨트롤러에서 요청처리되고,
(컨트롤러가 호출한) 서비스로직 실행되고,
MyLogger 소멸직전 종료콜백 실행된다.
[edd3ca8b-55f6-4711-b9c9-fa8f25f270a2] request scope bean create: hello.core.common.MyLogger@4d22713e
[edd3ca8b-55f6-4711-b9c9-fa8f25f270a2][http://localhost:8080/log-demo]controller test
[edd3ca8b-55f6-4711-b9c9-fa8f25f270a2][http://localhost:8080/log-demo]service id = testId
[edd3ca8b-55f6-4711-b9c9-fa8f25f270a2] request scope bean close: hello.core.common.MyLogger@4d22713e
※ uuid는 요청마다 다르고, request scope 빈도 요청마다 각각 관리된다.
이 예제에서는 저 4줄이 하나의 http 요청이다.
계속 요청해보면 uuid가 요청때마다 다르게 찍히는 것을 확인가능!
※ 당연히 동시에 요청이 들어오면 서로다른 uuid를 가진 요청이 섞여서 로그에 표시되겠쥐
● Proxy 방식 사용
MyLogger는 request 스코프를 가지기 때문에 싱글톤빈마냥 컨테이너 생성시점에 만들어지지 않아서 DI 관련 문제가 있었다는 것을 다시 한번 기억하자! 그래서 Provider로 시기맞춰 지연생성 해준 것이다. 이번에 볼 Proxy 방식도 마찬가지로 지연생성 해주어 request scope bean의 DI문제를 해결한다!
Proxy 방식은 Provider 적용 방식보다 코드를 좀 줄일 수 있다.
Provider는 적용하기 위해서는 Provider를 선언하고 Provider.getObject()로 꺼내 쓰는 코드를 써야 했다.
private final MyLogger myLogger;
↓
private final ObjectProvider<MyLogger> myLoggerProvider;
MyLogger에 '가짜 프록시 객체'를 넣어주는 방식이다.
그러면 실제 MyLogger 객체가 존재하지 않아도 작동이 되겠지!!
∨ 프록시 방식 사용하기
기존처럼 private final MyLogger myLogger; 로 그냥 선언하고도 정상적으로 작동하도록 할 수 있다.
@Scope 지정할 때 다음 설정을 넣으면 된다.
// 스코프 적용 대상이 class MyLogger기 때문에 TARGET_CLASS를 선택한 것이다.
// 적용 대상이 인터페이스면 INTERFACES 선택하면 된다.
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger { ... }
.
.
실제 MyLogger는 여전히 요청 전에는 존재하지 않을텐데..? MyLogger에 뭔가 주입되는 걸까..?
∨ 원리 : 가짜 프록시 클래스를 만들어 주입시켜준다!
MyLogger의 가짜 프록시 클래스를 만들어두고, HTTP request와 상관 없이 가짜 프록시 클래스를 미리 주입해둔다.
.
.
∨ 가짜 프록시 클래스 확인해보기
myLogger가 실제 create되기 전에 myLogger.class를 찍어보면 이렇게 나온다.
myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$6ed2debc
[c2cf0758-8949-4657-a4f3-cee94f227bf1] request scope bean create: hello.core.common.MyLogger@468217fc
[c2cf0758-8949-4657-a4f3-cee94f227bf1][http://localhost:8080/log-demo]controller test
[c2cf0758-8949-4657-a4f3-cee94f227bf1][http://localhost:8080/log-demo]service id = testId
[c2cf0758-8949-4657-a4f3-cee94f227bf1] request scope bean close: hello.core.common.MyLogger@468217fc
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
// myLogger 사용 전에 myLogger를 확인해보면?
System.out.println("myLogger = " + myLogger.getClass());
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
저번에 공부했던 AppConfig와 결이 비슷하다.
↓↓↓ recall ↓↓↓
@Configuration이 싱글톤을 보장한다는 내용을 공부하면서
다음과 같은 정리를 대충 했었다 ↓
- AppConfig <- AppConfig@CGLIB
(CGLIB, 바이트코드를 조작하는 라이브러리, 스프링빈으로등록되는건새로만들어진상속받은클래스)
- 그래서 @Bean 메서드마다 : 빈존재하면리턴, 빈존재안하면생성
=> 그래서 repository print문이 1번만 출력되는 것을 볼 수 있었음
- @Configuration 안쓰면 그냥 클래스 AppConfig를 사용 & 싱글톤보장X
- @Bean을 빈으로 등록하기는 하지만 싱글톤X
- 고민할 것 없이 설정정보는 항상 @Configuration
우리가 만든 MyLogger를 그대로 사용하는 것이 아니다.
스프링은 CGLIB 라이브러리를 통해 내가작성한 클래스를 상속받은 '가짜 프록시 객체'를 만들어서 컨테이너에 등록했고, Controller에 그 myLogger 프록시 빈을 DI한 것이다.
이렇게 주입된 가짜 프록시 객체는 실제요청이 오면 그제서야 진짜 빈을 요청한다.
가짜 프록시 객체 내부적으로 진짜 myLogger를 찾는 로직이 존재한다.
가짜 프록시 객체가 주입된 상태에서,
MyLogger에 대한 요청, 예를들면 setRequestURL() 메서드가 호출되었다고 하자.
이때 진짜 MyLogger의 메서드를 호출한 것이 아니라, 그것을 상속한 (가짜 객체)프록시의 setRequestURL()를 호출한 것이다. 그 후에 프록시 객체는 단순한 위임 로직을 통하여 실제로 request 스코프를 가지는 myLogger 객체를 찾아와 setRequestURL()을 호출해준다.
→ 이렇게 프록시를 사용하면 진짜 객체 조회를 딱 필요한 시점에 할 수 있도록 지연처리할 수 있다.
→ 우리는 프록시를 사용하면 진짜객체든 프록시객체든 동일한 로직으로 개발할 수 있다 (다형성 굿굿)
cf. 프록시 객체는 singleton 빈과 같이 편리하고 자유롭게 사용할 수 있다. 하지만 프록시도 요청마다 각각 생성되기 때문에 싱글톤은 아니라는 것을 주의하자.
'web +a' 카테고리의 다른 글
스프링부트 ~04 | 뷰템플릿 (0) | 2022.07.17 |
---|---|
메모) 구상/기획 단계라는 것 (0) | 2022.07.16 |
웹 스코프 | request scope (0) | 2022.07.15 |
테스트 주도 개발 (TDD) (0) | 2022.07.14 |
Bean Scope 개념 & 프로토타입 스코프 (0) | 2022.07.14 |