[spring 핵심 기본] 빈 스코프

채원·2024년 7월 25일

스프링

목록 보기
17/18
post-thumbnail

출처) 인프런 스프링 핵심 원리 기본편 강의

빈 스코프

빈이 존재할 수 있는 범위

스프링이 지원하는 스코프

  • 싱글톤(디폴트): 스프링 컨테이너 시작 ~ 종료, 가장 넓은 범위
  • 프로토타입: 빈: 스프링 컨테이너는 프로토타입 빈 생성 ~ 의존관계 주입까지만 관여, 매우 짧음

웹 관련 스코프

  • request: 웹 요청 들어옴 ~ 나갈때까지 유지
  • session: 웹 세션 생성 ~ 종료까지 유지
  • application: 웹 서블릿 컨텍스트와 같은 범위로 유지

프로토타입 스코프

싱글톤 스코프 빈 조회시 항상 같은 빈 ↔️ 프로토타입 스코프 조회시 항상 새로운 인스턴스 생성하여 반환

싱글톤 스코프

클라이언트 A, B, C는 같은 빈을 공유한다.

프로토타입 스코프

  • 클라이언트 요청이 들어올 때마다 빈을 생성, 의존 관계 주입, 초기화 후 반환
  • 컨테이너는 반환한 빈을 관리하지 않음, 반환하면 끝
  • 빈 관리 책임은 빈을 조회한 클라이언트에 있음
  • @PreDestory 같은 종료 메서드 호출 X
    빈을 조회한클라이언트가 종료해야함, prototypeBean1.destory(); 이런식으로 직접 호출

프로토타입 스코프 - 1) 싱글톤 빈과 함께 사용시 문제점

싱글톤 빈 clientBean은 컨테이너 생성 시점에 생성되고, 의존관계 주입 발생
1. 의존 관계 주입에 사용할 프로토타입 빈을 컨테이너에 요청
2. 스프링 컨테이너에서 프로토타입 빈 생성하여 반환, clientBean은 내부에 프로토타입 빈 저장하여 보관함, count = 0

  1. 클라이언트 A가 로직 호출 -> 항상 같은 clientBean 반환 받음

  2. clientBean은 count+1 함, count = 1

  3. 클라이언트 B가 로직 호출 -> 항상 같은 clientBean 반환 받음

  4. clientBean은 count+1 함, count = 2

count값이 다른 클라이언트에 의해 변경된 상황
클라이언트 A, B는 같은 빈을 사용하고 (싱글톤이므로), 프로토타입 빈은 이미 과거에 주입된 빈으로 계속 같은 것을 사용함(로직 호출마다 프로토타입빈 새로 생성 X)

싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 프로토타입 빈이 새로 생성되긴 하지만 주입되지 않는다.
⭐️ 즉 제일 처음에 만들어진 프로토타입 빈이 싱글톤 빈과 함께 계속 유지된다.

프로토타입 스코프 - 2) 싱글톤 빈과 함께 사용시 문제 해결

1. 싱글톤 빈이 사용할 때마다 프로토타입 빈을 새로 요청하기
로직을 호출할 때마다, ac.getBean()을 통해서 새로운 프로토타입 빈이 생성됨

문제점) 스프링 컨테이너에 종속적인 코드가 되고, 단위별 테스트가 어려움

@Autowired
         private ApplicationContext ac;
         public int logic() {
             PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
             prototypeBean.addCount();
             int count = prototypeBean.getCount();
             return count;
}

2. ObjectProvider 사용
지정한 프로토타입 빈만 컨테이너에서 대신 찾아주는 DL 기능을 제공함
Dependency Lookup (DL) 의존 관계
의존 관계를 외부 주입이 아니라, 직접 필요한 의존 관계를 찾는 방식

ObjectProvider의 getObject() == 스프링 컨테이너에서 해당 빈을 찾아 반환

  static class ClientBean {
    @Autowired private ObjectProvider<PrototypeBean> prototypeBeanProvider;

    public int logic() {
      PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
      prototypeBean.addCount();
      int count = prototypeBean.getCount();
      return count;
    }
  }

3. JSR-330 Provider 사용
자바 표준을 사용, 사용하기 위해서는 build.gradle에 라이브러리를 추가해야함
dependencies에 추가하기 (spring 3.0 이상)

implementation 'jakarta.inject:jakarta.inject-api:2.0.1'
  • java security provider를 import하지 않도록 주의하기
  • provider.get()으로 항상 새로운 프로토타입 빈이 생성됨
  • DL 정도의 기능 제공
  static class ClientBean {
    @Autowired private Provider<PrototypeBean> provider;

    public int logic() {
      PrototypeBean prototypeBean = provider.get();
      prototypeBean.addCount();
      int count = prototypeBean.getCount();
      return count;
    }
  }

프로토타입 빈을 사용하는 경우

매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요한 경우 사용 (매우 드물다)
실무에서는 싱글톤으로 거의 해결이 되기 때문에, 거의 사용하지 않음

spring 환경에서 사용 → ObjectProvider
spring 외 컨테이너 → JSR-330 Provider

웹 스코프

웹 환경에서만 동작, 스코프의 종료시점까지 관리하여 종료 메서드 호출

웹 스코프 종류

  • request: HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프, 각 요청마다 별도의 빈 인스턴스 생성 및 관리
  • session: HTTP session과 동일한 생명주기 가지는 스코프
  • application: 서블릿 컨텍스트와 동일한 생명주기를 가지는 스코프
  • Websocket: 웹소켓과 동일한 생명주기를 가지는 스코프


프로토타입과 다르게, 각 클라이언트 전용 스코프가 제공이 됨

웹 request 스코프 예제

웹 스코프는 웹에서만 동작하므로 웹 라이브러리 추가 필요

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

동시에 여러 HTTP 요청이 오면 어떤 요청이 남긴 로그인지 구분하기 어려움
→ 이럴 때 사용하기 좋은 것 == request 스코프

  • [UUID][requestURL][message] 포맷 형태로 로그 확인하기

로그 출력을 위한 MyLogger

package hello.core.common;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

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.randomUUID().toString();
    System.out.println("[" + uuid + "] request scope bean create:" + this);
  }

  @PreDestroy
  public void close() {
    System.out.println("[" + uuid + "] request scope bean close:" + this);
  }
}
  • @Scope 종류를 request로 지정
  • requestURL은 요청이 들어왔을 때 알 수 있으므로 setter로 입력
  • @PostConstruct로 생성 시점에 uuid 저장, 메시지 출력
  • @PreDestor로 종료 시점에 메시지 출력

log 확인용 컨트롤러
아래 코드에서는 requestURL을 MyLogger에 저장하는데,
로직 공통처리가 가능한 스프링 인터셉터를 통해 구현하는것이 좋음

package hello.core.web;

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

@Controller
@RequiredArgsConstructor
public class LogDemoController {
  private final LogDemoService logDemoService;
  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 서비스 계층에서 출력
⭐️서비스 계층에서는 웹 기술에 종속되지 않고 순수하게 유지하는 것이 좋음
웹 관련 정보는 컨트롤러까지 사용해야함
MyLogger의 멤버 접근을 통해서, 파라미터로 모든 정보를 서비스 계층에 넘기지 않고도 서비스 아이디 출력 가능

package hello.core.web;

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

지금의 코드는 실제 요청이 들어와야 빈이 생성되기 때문에, 컨테이너에 빈 요청을 하는 것에서 오류가 발생
즉, 스프링 컨테이너에 빈 요청을 의존관계 주입 시점이 아닌 고객 요청 이후에 해야한다.
→ Provider로 이 문제를 해결할 수 있음

웹 스코프와 Provider

  • ObjectProvider 덕분에 ObjectProvider.getObject() 를 호출하는 시점까지 request scope 빈의 생성을 지연
    - ObjectProvider.getObject() 를 호출하시는 시점에는 HTTP 요청이 진행중이므로 request scope 빈 생성이 정상처리 됨
  • getObject()를 controller, service 에서 각각 호출해도 같은 HTTP 요청인 경우에는 같은 빈 반환

service 코드 수정

public class LogDemoService {
  private final ObjectProvider<MyLogger> myLoggerProvider;

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

controller 코드 수정
위와 비슷하게 ObjectProvider로 myLogger 만들고 getObject()

웹 스코프와 프록시

위 Provider처럼 컨테이너에 빈 요청을, request 이후로 할 수도 있지만
HTTP request 여부와 별도로 가짜 프록시를 미리 주입해서 해결 가능

  • 클라이언트가 myLogger.log() 호출 == 가짜 프록시 객체의 메서드 호출
  • 가짜 프록시 객체는 이후 진짜 myLogger.log() 를 호출함
  • 클라이언트 입장에서는 원본 여부를 알 필요 없이 동일하게 사용 가능 (다형성)

MyLogger 코드 수정
프록시 방식을 적용하기 위해 수정
클라이언트 코드 수정 없이, 애노테이션 설정 변경만으로 프록시 객체로 대체 가능
== 스프링 컨테이너가 가진 장점

  • prooxyMode = ScopedProxyMode.TARGETCLASS
    - 적용 대상이 클래스면 TARGETCLASS 적기
    • 적용 대상이 인터페이스면 INTERFACES 적기
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {

controller, service 코드는 이전 것으로 돌리기

⭐️ Provider, 프록시 사용의 핵심 아이디어 = 진짜 객체 조회를 필요한 시점까지 지연처리

0개의 댓글