Bean의 범위, 그리고 ObjectProvider와 ProxyMode

나르·2024년 2월 29일
0

Spring

목록 보기
25/25
post-thumbnail

스프링에서 지원하는 Bean Scope

Singleton

기본 스코프(Default Scope)로 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다. (컨테이너가 사라질 때 Bean도 제거된다.)

싱글턴인 경우 현재 스프링 컨테이너에서 같은 종류의 Bean 객체가 오직 1개의 인스턴스만 생성되는 것을 보장하는 디자인 패턴이기 때문에 매 요청마다 같은 객체를 반환한다.

대상 클래스에 @Scope("singleton")으로 설정할 수 있다.

Prototype

스프링 컨테이너가 프로토 타입 빈의 생성과 의존관계 주입 그리고 초기화까지만 관여하는 짧은 범위의 스코프이다.
따라서 소멸 메서드는 호출하지 않기 때문에 프로토타입 스코프의 객체는 사용자가 직접 자원 해제 코드를 실행해주거나 명시적으로 destroy 메서드를 호출해야 한다.

프로토타입의 경우 클래스의 인스턴스가 spring 컨테이너에 몇 개든 존재할 수 있는 디자인 패턴이다.
즉, 의존 주입이나 getBean() 호출처럼 매번 Bean 객체가 요청될 때마다 항상 새로운 인스턴스를 생성한 뒤 반환해준다.
그렇기 때문에 상태를 유지할 필요가 있는 객체는 Protortype 스코프를, 유지할 필요가 없는 객체는 Singleton 스코프를 적용하는 것이 일반적이다.

대상 클래스에 @Scope("prototype")으로 설정할 수 있다.


다음부터 나오는 request, session, global, session은 HTTP 통신과 관련된 스코프기 때문에 스프링으로 웹 기반 애플리케이션(Spring MVC Web Application)을 작성할 때만 적용할 수 있다.

웹 스코프는 웹 환경에서만 동작하므로 build.gradle에 web 환경, 라이브러리를 추가해야 한다.

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

만약 해당 라이브러리를 추가하지 않으면 AnnotationConfigApplicationContext를 기반으로 어플리케이션을 구동한다.

웹 라이브러리가 추가되면 웹 관련 기능 및 설정이 필요하기에 AnnotationConfigServletWebServerApplicationContext를 기반으로 애플리케이션을 구동한다.

Request

HTTP 요청이 들어오고 나갈 때 까지 유지되는 스코프이다.

web-aware 스프링 컨테이너에 전송된 HTTP 요청마다 객체가 생성된다. 동시에 여러 HTTP 요청이 들어왔을 때, 어떤 Request가 남긴 로그인지 판별할 때 사용한다.

주의점
Request scope는 HTTP 요청이 들어와야 생성되기 때문에 Singleton 빈 의존 관계 주입 시점에는 존재하지 않는다. (자주 ComplieError의 원인이 된다)

Session

웹 세션이 생성되고 종료될 때까지 유지되는 스코프이다.
해당 객체는 web-aware 스프링 컨테이너에서 맺어진 HTTP 세션마다 생성된다.
HTTP 요청이 아닌 서버와 클라이언트 간 세션 연결을 기반으로 객체를 생성한다.
마찬가지로 각 세션의 객체들은 서로 변경사항이 반영되지 않는 독립적인 객체들이며 사용이 끝난 뒤 폐기된다.

Global Session

해당 객체는 web-are 스프링 컨테이너에서 맺어진 global HTTP 세션마다 생성된다.

Application

서블릿 컨텍스트(Servlet Context)와 동일한 생명 주기를 가지는 스코프

Websocket

웹소켓과 동일한 생명주기를 가지는 스코프


Request 스코프 예제

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

형식: UUID + requestURL + message

UUID로 HTTP 요청을 구분하고 어떤 URL을 요청해서 남은 로그인지 확인한다.

// CustomLogger.java

@Component
@Scope(value = "request")
public class CustomLogger {
 
  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 created: " + this);
  }
 
  @PreDestroy
  public void close() {
    System.out.println("[" + uuid + "] request scope bean closed: " + this);
  }
}

해당 클래스는 request 스코프이기 때문에 HTTP 요청마다 빈이 생성되고 소멸될 것이다.
빈이 생성되는 시점에는 @PostConstruct로 UUID를 생성해 저장해둔다. 빈이 요청 당 하나씩 생성되므로 UUID를 저장해 두면 다른 요청과 구분할 수 있다.
requestURL은 빈이 생성되는 시점에는 알 수 없어 외부에서 setter로 입력받는다.
빈이 소멸되는 시점에는 @PreDestroy를 사용해 종료 메시지를 남긴다.

// LogController.java - CustomLogger의 동작을 확인하는 컨트롤러

@Controller
@RequiredArgsConstructor
public class LogController {
 
  private final LogService logService;
  private final CustomLogger logger;
 
  @RequestMapping("log")
  @ResponseBody
  public String log(HttpServletRequest request) {
   
    // request를 통해 요청 URL을 받아 setter로 저장했다.
    String requestURL = request.getRequestURL().toString();
    logger.setRequestURL(requestURL);
 
    //컨트롤러에 controller test 로그를 남기고 logService 의 로직을 호출한다.
    logger.log("controller test");
    logService.logic("testId");
 
    return "OK";
  }
}

reqeustURL을 logger 저장해두면 logger는 HTTP 요청마다 다른 Bean 객체가 생성되므로 고유한 값을 가지게 된다.

// LogService.java
@Service
@RequiredArgsConstructor
public class LogService {
 
  private final CustomLogger logger;
 
  public void logic(String id) {
    logger.log("service id = " + id);
  }
}

request 스코프를 사용하지 않고 모든 정보를 서비스 계층에 넘긴다면 파라미터가 많아서 지저분해진다.
requestURL처럼 웹과 관련된 정보가 웹과 관련없는 서비스 계층까지 넘어가는 문제도 있다.
웹과 관련된 부분은 컨트롤러까지만 사용하고 서비스 계층은 웹 기술에 종속되지 않아야 하므로 가급적 순수하게 유지하는 것이 유지 보수에 좋다.
reqeust 스코프의 CustomLogger 덕분에 웹과 관련된 부분을 파라미터로 넘기지 않고 CustomLogger 멤버 변수에 저장해 코드와 계층을 깔끔하게 유지할 수 있다.

애플리케이션 실행

애플리케이션을 실행하면 다음과 에러가 나는 것을 확인할 수 있다.

스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 아직 생성되지 않고 실제 클라이언트의 요청이 와야 생성할 수 있기 때문이다.

자세히 말하자면, 프로젝트가 구동될 때 스프링 빈들이 컴포넌트 스캔이 되며 등록 및 의존관계 주입이 되는데, 여기서 웹스코프인 CustomLogger 빈의 경우 HttpRequest 요청이 올때 생성되는 빈이기 때문에 스프링 구동단계에서는 아직 생성을 할 수 없다. 때문에 빈을 찾을 수 없기에 에러가 발생하는 것이다.

스코프와 ObjectProvider

위에서 언급했던 문제를 해결하기 위해 빈 객체를 DL(동적으로 로딩) 할 수 있는 ObjectProvider를 사용해보자.

// LogController.java
@Controller
@RequiredArgsConstructor
public class LogController {
 
    private final LogService logService;
    private final ObjectProvider<CustomLogger> loggerProvider;
 
    @RequestMapping("log")
    @ResponseBody
    public String log(HttpServletRequest request) {
        // DL로 원하는 객체를 가져온다
        CustomLogger logger = loggerProvider.getObject();
        logService.logic("testId");
 
        String requestURL = request.getRequestURL().toString();
        logger.setRequestURL(requestURL);
 
        logger.log("controller test");
        logService.logic("testId");
 
        return "OK";
    }
}
// LogService.java
@Service
@RequiredArgsConstructor
public class LogService {
    private final ObjectProvider<CustomLogger> loggerProvider;
 
    public void logic(String id) {
        CustomLogger logger = loggerProvider.getObject();
        logger.log("service id = " + id);
    }
}

ObjectProvider 덕분에 ObjectProvider.getObject()를 호출할 시점까지 request scope 빈을 생성해달라고 스프린 컨테이너에 요청하는 시점을 지연할 수 있다.
따라서 ObjectProvider.getObject()를 호출하는 시점에는 HTTP 요청이 진행 중이므로 request 스코프 빈이 생성된다.

스코프와 프록시

스코프 속성을 이용해 스프링 빈을 프록시 객체로 만들어 줄 수 있다.

// CustomLogger.java
@Component
// 프록시 모드 추가
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class CustomLogger {
  ...
}

proxyMode를 추가하면 CustomLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입한다.

해당 빈이 실제 사용될 때 프록시 빈에서 실제 빈을 가져와 사용할 수 있도록 한다.

적용 대상이 클래스면 TARGET_CLASS, 인터페이스면 INTERFACES를 선택한다.

// LogController.java
@Controller
@RequiredArgsConstructor
public class LogController {
 
    private final LogService logService;
    private final ObjectProvider<CustomLogger> loggerProvider;
 
    @RequestMapping("log")
    @ResponseBody
    public String log(HttpServletRequest request) {
        // DL로 원하는 객체를 가져온다
        CustomLogger logger = loggerProvider.getObject();
        logService.logic("testId");
 
        String requestURL = request.getRequestURL().toString();
        logger.setRequestURL(requestURL);
 
        logger.log("controller test");
        logService.logic("testId");
 
        return "OK";
    }
}
// LogService.java
@Service
@RequiredArgsConstructor
public class LogService {
 
  private final CustomLogger logger;
 
  public void logic(String id) {
    logger.log("service id = " + id);
  }
}

웹 스코프와 프록시의 동작 원리

proxyMode = ScopedProxyMode.TARGET_CLASS를 설정하면 스프링 컨테이너는 CGLIB이라는 바이트 코드 조작 라이브러리로 CustomLogger를 상속받은 가짜 프록시 객체를 생성한다.
스프링 컨테이너에 logger라는 이름의 가짜 프록시 객체를 넣어두고 실제 필요한 시점에 가져와서 동작하는 것이다.
ac.getBean("logger", CustomLogger.class)로 찍어봐도 가짜 프록시 객체가 주입된다. 그래서 의존관게 주입도 이 가짜 프록시 객체로 주입된다.


정리

Provider와 프록시의 핵심은 진짜 객체 조회를 곡 필요한 시점까지 지연 처리한다는 점이다!

profile
💻 + ☕ = </>

0개의 댓글