포스트

빈 스코프

빈 스코프

빈 스코프란?

  • 빈이 존재할 수 있는 범위

스프링의 지원 스코프

  • singleton: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
  • prototype: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까짐나 관려하고 더는 관리하지 않는 매우 짧은 범위의 스코프
  • request: 웹 요청이 들어오고 나갈 때 까지 유지되는 스코프
  • session: 웹 세션이 생성되고 종료될 떄 까지 유지되는 스코프
  • application: 웹 서블릿 컨텍스트와 같은 범위로 유지되는 스코프

컴포넌트 스캔 자동 등록

1
2
3
4
5
@Scope("prototype")
@Component
public class Bean {
}

컴포넌트 스캔 수동 등록

1
2
3
4
5
6
@Scope("prototype")
@Bean
Bean bean() {
  return new Bean();
}

프로토타입 스코프

  • 빈 요청 시, 매번 새로운 객체를 생성하여 반환하고 스프링 컨테이너가 관리하지 않음
  • 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존 관계 주입, 초기화까지만 처리
  • 따라서, @Predestory 같은 종료 메서드가 호출되지 않음

싱글톤 빈과 함께 사용 시 문제점

싱글톤 빈에서 프로토타입을 의존 주입 받아 사용하게 되면 사용 할 때마다 새로운 객체이길 기대하겠지만, 의존 주입은 생성 시점에 한 번 주입되는 것이므로, 처음 주입된 객체가 유지됩니다.

싱글톤 빈과 함께 사용시 Provider로 문제 해결

가장 간단한 방법은 매 요청마다 ApplicationContext에서 빈을 조회하여 사용하는 것입니다.

1
2
3
4
5
6
7
8
public int logic() {
  // 매번 새로운 prototypeBean을 반환
  PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);

  prototypeBean.addCount();
  int count = prototypeBean.getCount();
  return count;
}
  • 의존 관계를 외부에서 주입(DI) 받는게 아니라 이렇게 직접 필욯란 의존 관계를 찾는 것을 Dependency Lookup(DL) 의존관계 조회(탐색)이라 함
  • 이렇게 스프링의 어플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고 단위 테스트도 어려워 짐
  • DL 정도의 기능만 제공하는 ObjectFactory, ObjectProvider를 사용하여 이 문제를 해결

ObjectFactory, ObjectProvider

1
2
3
4
5
6
7
8
9
10
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;

public int logic() {
  PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
  prototypeBean.addCount();
  int count = prototypeBean.getCount();
  return count;
}
  • ObjectProvidergetObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환(DL)
  • ObjectFactory: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존
  • ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리 등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존

JSR-330 Provider

  • build.gradle
1
2
3
4
dependencies {
  // ...
  implementation("jakarta.inject:jakarta.inject-api:2.0.1")
}
1
2
3
4
5
6
7
8
9
10
@Autowired
private Provider<PrototypeBean> provider;

public int logic() {
  PrototypeBean prototypeBean = provider.get();
  prototypeBean.addCount();
  int count = prototypeBean.getCount();
  return count;
}
  • get() 메서드 하나로 기능이 매우 단순
  • 별도의 라이브러리가 필요
  • 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있음

💡 ObjectProvider vs JSR-330 Provider

스프링이 아닌 다른 컨테이너를 사용할 상황이 있다면 JSR-330 Provider를 사용하면 되지만, 사실상 그럴 일은 거의 없기 때문에 스프링에서 제공하는 ObjectProvider를 사용하면 됩니다.

웹 스코프

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

request 스코프 예제 만들기

  • 매 요청 마다 사용자를 구분하기 위해 위와 같이 로그가 남도록 만들어 보겠습니다.
1
2
3
4
[d06b992f...] request scope bean create
[d06b992f...][http://localhost:8080/log-demo] controller test
[d06b992f...][http://localhost:8080/log-demo] service id = testId
[d06b992f...] request scope bean close
  • build.gradle
1
2
3
4
dependencies {
  // ...
  implementation("org.springframework.boot:spring-boot-starter-web")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@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);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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";
  }
}
1
2
3
4
5
6
7
8
9
10
11
@Service
@RequiredArgsConstructor
public class LogDemoService {

  private final MyLogger myLogger;

  public void logic(String id) {
    myLogger.log("service id = " + id);
  }
}
  1. log-demo로 요청
  2. 컨트롤러에서 로깅 찍음
  3. 서비스에서 로깅 찍음
  4. 이 둘은 같은 request 스코프를 가지므로 같은 UUID를 가짐

이렇게 예상하고 실행을 시켜보면 에러가 발생합니다.

그 이유는 MyLogger는 요청이 들어와야 생성되는 객체인데 스프링을 띄우는 시점에는 생성되어 있지 않으니 의존 주입을 할 수 없기 때문입니다.

스코프와 Provider

컨트롤러와 서비스에서 MyLogger 필드를 ObjectProvider<MyLogger>로 수정하고, ObjectProvider.getObject()를 통해 MyLogger 호출하도록 수정 후 실행 해보면 정상적으로 실행하면 정상 동작합니다.

1
2
3
4
5
6
7
8
9
10
11
12
@Service
@RequiredArgsConstructor
public class LogDemoService {

  private final ObjectProvider<MyLogger> myLoggerProvider;

  public void logic(String id) {
    MyLogger myLogger = myLoggerProvider.getObject();
    myLogger.log("service id = " + id);
  }
}
  • ObjectProvider.getObject()를 호출하는 시점까지 request.scope빈의 생성을 지연할 수 있음
  • ObjectProvider.getObject()를 호출하는 시점에는 HTTP 요청이 진행중이므로 request scope빈의 생성이 정상 처리 됨

스코프와 프록시

ObjectProvider를 사용하지 않고 바로 MyLogger를 사용하게 할 수도 있습니다.

1
2
3
4
5
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
  • proxyMode = ScopedProxyMode.TARGET_CLASS를 추가
    • 적용 대성이 클래스면 TARGET_CLASS
    • 적용 대상이 인터페이스면 INTERFACES
  • 이렇게 하면 MyLogger의 가짜 프록시 클래스를 만들어 두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있음
1
2
3
4
5
6
7
8
9
10
11
12
@Service
@RequiredArgsConstructor
public class LogDemoService {

  // 프록시 객체 주입
  private final MyLogger myLogger;

  public void logic(String id) {
    myLogger.log("service id = " + id);
  }
}
동작 원리
  • CGLIB 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입
  • 이 가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어 있음
  • 가짜 프록시 객체는 실제 request scope와는 관계가 없음. 그냥 가짜이고 내부에 단순한 위임 로직만 있고, 싱글톤 처럼 동작 함

참고

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.