Spring/스프링 핵심 원리 - 기본편

[빈 스코프] 웹 스코프에 ObjectProvider 사용 시 발생하는 문제, CGLIB 가짜 프록시로 해결하기

alsruds 2024. 1. 8. 21:57
🧐강의🧐
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/dashboard

 

↓ ↓ 이어지는 강의 ↓ ↓

2024.01.03 - [Spring/스프링 핵심 원리 - 기본편] - [빈 스코프] 웹 스코프 request 예제 만들기

 

[빈 스코프] 웹 스코프 request 예제 만들기

🧐강의🧐 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/dashboard 웹 스코프는 웹 환경에서만 동작하기 때문에, 라이브러리를 추가해야 한다 💡

alsrudalsrudalsrud.tistory.com

 

문제 상황

  • 같은 HTTP 요청 시 같은 스프링 빈 반환 → 새로운 객체가 생성되지 않는 문제

· LogDemoController

➡️ ObjectProvider 코드 추가하기

@Controller
@RequiredArgsConstructor
public class LobDemoController {

    ...
    private final ObjectProvider<MyLogger> myLoggerProvider;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        MyLogger myLogger = myLoggerProvider.getObject();
        ...
    }
}

 

· LogDemoService

➡️ ObjectProvider 코드 추가하기

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final ObjectProvider<MyLogger> myLoggerProvider;

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

 

· hello.core.CoreApplication 실행 후 웹 브라우저 접속하기

 

· 로그 확인하기

✔️ ObjectProvider 사용으로 ObjectProvider.getObject() 호출 시점까지 request scope 빈 생성 지연

✔️ ObjectProvider.getObject() 호출 시점 : HTTP 요청 진행 중이므로 request scope 빈 생성 정상 처리

 

➡️ ObjectProvider.getObject() 를 Controller 와 Service 에서 각각 호출해도, 같은 HTTP 요청이면 같은 스프링 빈 반환!

 


 

해결 방법

  • CGLIB 라이브러리로 내 클래스를 상속받은 가짜 프록시 객체를 만들어 주입하기

· myLogger

✅ MyLogger 의 가짜 프록시 클래스 생성 후, HTTP request 와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입 가능

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger { ... }

➡️ 적용 대상이 인터페이스일 경우 : proxyMode = ScopedProxyMode.INTERFACES

➡️ 적용 대상이 인터페이스가 아닌 클래스일 경우 : proxyMode = ScopedProxyMode.TARGET_CLASS

 

· LogDemoController

@Controller
@RequiredArgsConstructor
public class LobDemoController {

    ...
    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("testId");
        return "OK";
    }
}

 

· LogDemoService

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;

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

 

· 동작 원리

  1. @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) 설정 시, 스프링 컨테이너는 CGLIB 라이브러리를 사용하여 MyLogger 를 상속받은 가짜 프록시 객체 생성 (바이트코드 조작)
    → 확인 시 MyLogger$$EnhancerBySpringCGLIB 클래스로 만들어진 객체가 대신 등록되어 있음 + 의존 관계 주입
    (MyLogger 대신 등록된 가짜 프록시 객체)
  2. 클라이언트가 myLogger.log() 호출 시, 가짜 프록시 객체 메서드 호출
  3. 가짜 프록시 객체가 request scope 의 진짜 myLogger.log() 호출
    → 가짜 프록시 객체는 원본 클래스를 상속받았기 때문에, 진짜 빈을 요청하는 위임 로직이 들어있음
    → 클라이언트 입장에서는 동일하게 사용 가능 (다형성)

 

동작 정리
- CGLIB 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체 생성 후 의존 관계 주입
- 실제 요청이 들어오면, 가짜 프록시 객체는 내부에서 실제 빈을 요청하는 위임 로직 실행
- 가짜 프록시 객체는 싱글톤처럼 동작 (실제 request scope 과는 상관X)

특징 정리
- 클라이언트는 싱글톤 빈을 사용하듯 request scope 사용 가능
- ⭐ 진짜 조회 객체를 필요한 시점까지 지연 처리
- 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체 가능 (다형성 & DI 컨테이너의 강점)
- 웹 스코프가 아니더라도 프록시 사용 가능

주의점
- 마치 싱글톤을 사용하는 것 같지만, 실제로는 동작 방식이 다르기 때문에 주의해서 사용
- 무분별한 사용 시, 유지보수가 어려워져 필요한 곳에 최소화하여 사용