빈 스코프란 ?
빈 스코프는 빈의 생존 기간을 의미한다. 보통의 경우는 스프링 컨테이너의 시작과 함께 싱글톤으로 생성되어 종료될 때 까지 유지된다. 다른 설정이 없다면 이 경우가 기본 스코프이며 '싱글톤 스코프' 라고 한다. 스프링은 이 외에도 다음과 같은 스코프를 지원한다.
빈 스코프 종류
- 싱글톤 (singleton)
- 기본 스코프. 별도 설정이 없다면 싱글톤 스코프로 간주한다.
- 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프.
- 프로토타입 (prototype)
- 스프링 컨테이너가 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 스코프.
- 관리 책임은 프로토타입을 받는 클라이언트에 있다. 그렇기에 @PreDestroy와 같은 콜백메서드는 호출 불가
- 스프링 컨테이너에 빈을 요청할 때마다 새로운 인스턴스가 생성된다.
- 멀티 스레드 환경에서 문제가 없어야 하는 경우라면(thread-safe), singleton이 아닌 prototype을 사용해야 한다.
- 스프링 컨테이너가 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 스코프.
- 웹 스코프
- 웹 환경에서만 동작하며 프로토타입과 다르게 스프링 컨테이너가 해당 스코프의 종료시점까지 관리한다.
- request : HTTP 요청이 들어올 때마다 새로운 빈 인스턴스가 만들어지고 요청이 나갈때 까지 유지되는 스코프.
- 동시에 여러 요청이 오기 때문에, 요청마다 로그를 구분하기 위해 사용하기 좋다.
- session : HTTP 세션이 만들어질 때마다 새로운 빈 인스턴스가 만들어지고 세션 종료시 까지 유지되는 스코프.
- application : 서블릿 컨텍스트(Servlet Context)와 같은 범위로 유지되는 스코프.
- websocket : 웹 소켓과 동일한 생명주기를 가지는 스코프.
빈 스코프는 아래와 같이 지정할 수 있다.
//컴포넌트 스캔 자동 등록의 경우
@Scope("prototype")
@Component
public class TestBean{}
//수동 등록의 경우
@Scope("prototype")
@Bean
PrototypeBean TestBean() {
return new TestBean();
}
아래는 이해를 돕기 위한 싱글톤 스코프와 프로토타입 스코프 사용 예시.
스코프가 다른 빈 주입
스코프 종류가 여러가지이니 이런 생각을 할 수 있을 것이다. '스코프가 서로 다른 빈 간의 의존관계가 형성된 경우에는 빈의 스코프는 어떻게 될까?'
결론만 이야기 하자면 스프링 컨테이너에 의해 주입된 빈은 자신의 스코프와 상관없이 주입받는 빈의 스코프를 따른다.
예시를 들자면, 프로토타입 스코프의 빈을 싱글톤 스코프의 빈에 주입한 경우는 싱글톤 스코프의 빈이 살아 있는 동안에는 스프링 컨테이너에서 다시 생성할 필요가 없기 때문에 싱글톤 인스턴스와 같은 수명을 가지게 된다. (생성하고 주입하고 관리를 안하기 때문에)
매번 새로운 인스턴스를 생성하려고 프로토타입 스코프로 지정했는데 그렇지 않으니 자칫하면 문제가 발생할 수 있다.
아래 예시와 같이 멀티 스레드 환경에서 안전하지 않은 PasswordEncoder 클래스가 있다고 하자. 싱글톤 스코프로 설정하면 thread-safe하지 않으므로 prototype으로 설정 하였다.
@Bean
@Scope("prototype")
PasswordEncoder passwordEncoder() {
// 멀티 스레드 환경에서 안전하지 않다.
return new ThreadUnsafePasswordEncoder();
}
다음은 이 빈을 service layer에서 사용하는 경우이다. (UserService의 스코프는 singleton)
@Component
public class UserServiceImpl implements UserService {
//편의상 필드 주입
@Autowired
private final PasswordEncoder passwordEncoder;
public void register(User user, String rawPassword) {
String encoderPassword = passwordEncoder.encode(rawPassword);
...
}
}
자세한 코드는 없지만 PasswordEncoder는 멀티 스레드 환경에서 안전하지 않은 경우이기 때문에, 매번 다른 인스턴스를 가지고 로직을 수행해야 한다. 하지만 PasswordEncoder를 주입받은 UserService의 스코프가 싱글톤이므로 (PasswordEncoder가 프로토타입 스코프로 정의되어 있어도 스프링 컨테이너에서는 다시 생성할 필요를 느끼지 못해) 이미 생성되어 주입받았던 PasswordEncoder빈을 재사용하게 된다.
이런 경우에는 어떻게 해야 할까?
싱글톤 스코프 빈인 UserService에서 의존관계 주입을 이미 받은 채로 빈의 소멸 없이 계속 동작해서 일어난 일이기 때문에 의존관계 주입을 받지 않고, 필요할 때마다 스프링 컨테이너에 등록된 프로토타입 스코프 빈을 직접 찾아다 쓰면 되지 않을까?
의존관계 검색(Dependency Lookup, DL)
이렇게 의존관계를 외부에서 주입(Dependency Injection, DI)받는 것이 아니라 직접 의존관계를 찾는 것을
의존관계 검색(Dependency Lookup, DL)라고 한다.
프로토타입의 경우는 스프링 컨테이너에 요청할 때마다 새로운 인스턴스가 생성된다고 맨 처음에 정리했었다. 그래서 빈을 사용하는 시점에 직접 찾아 사용하겠다는 것이다.
의존관계 검색에는 여러가지가 있는데 여기에는 자주 쓰이는 방법 2가지를 정리하도록 하겠다.
DL : ObjectProvider
@Component
public class UserServiceImpl implements UserService {
//편의상 필드 주입
@Autowired
private ObjectProvider<PasswordEncoder> passwordEncoderProvider;
public void register(User user, String rawPassword) {
PasswordEncoder passwordEncoder = passwordEncoderProvider.getObject();
String encoderPassword = passwordEncoder.encode(rawPassword);
...
}
}
ObjectProvider의 getObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)
기능이 단순하고 별도 라이브러리 없이 스프링에 의존하기만 한다.
DL : JSR-330 Provider
JSR-330 자바 표준을 사용하는 방법이다.
스프링 부트 3.0 미만 : <code>javax.inject:javax.inject:1</code> 라이브러리
스프링 부트 3.0 이상 : <code>jakarta.inject:jakarta.inject-api:2.0.1</code> 라이브러리
import jakarta.inject.Provider;
@Component
public class UserServiceImpl implements UserService {
//편의상 필드 주입
@Autowired
private Provider<PasswordEncoder> provider;
public void register(User user, String rawPassword) {
PasswordEncoder passwordEncoder = provider.get();
String encoderPassword = passwordEncoder.encode(rawPassword);
...
}
}
provider의 get()을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)
자바 표준이기 때문에 스프링이 아닌 다른 컨테이너에서도 사용 가능하며, 기능이 단순해 단위테스트를 만들거나 mock코드를 만들기 쉬워진다.
위 방법은 꼭 프로토타입 스코프를 위해 사용하는 것은 아니라 경우에 따라 Dependency Lookup(DL)을 해야하는 상황에 언제든지 사용할 수 있다.
Proxy (프록시)
스코프가 다른 빈을 주입할 때 발생하는 문제를 해결하기 위한 또다른 방법이 있다.
기존의 빈을 프록시(Proxy)로 감싸 가짜를 만들어 다른 빈에 주입하고 주입받은 빈에서 이 프록시 메서드를 호출하게 되면 프록시 내부적으로 실제 빈을 요청해 메서드를 실행하는 방식이다.
이 방식은 보통 request 스코프나 session 스코프와 같이 수명이 짧은 빈을 singleton 스코프와 같이 수명이 긴 빈에 주입할 때 쓰이므로 예시를 request 스코프로 들겠다.
request 스코프 빈은 웹 스코프로써 웹 상에 요청이 와야 생성되기 때문에 비즈니스 로직이 있는 Service 싱글톤 빈에 아무 설정 없이 request빈을 주입한다면 빈을 생성할 수 없다는 메세지와 함께 에러가 발생 할 것이다.
이를 해결하기 위한 Proxy 방법을 정리해 보겠다.
사용 방법은 해당 빈에 @Scope 애너테이션을 붙인 후 proxyMode 속성에 프록시 만드는 방법을 설정하면 된다.
- 적용 대상이 클래스면 <code>TARGET_CLASS</code>
- 스프링 내장 라이브러리인 CGLIB을 사용해 서브클래스 기반의 프록시를 생성.
- 적용 대상이 인터페이스면 <code>INTERFACES</code>
- JDK의 동적 프록시(java.lang.reflect.Proxy)를 사용해 인터페이스 기반의 프록시를 생성.
@Bean
@Scope(value = "request", proxyMode = ScopeProxyMode.INTERFACES)
PasswordEncoder passwordEncoder() {
return new ThreadUnsafePasswordEncoder();
}
@Component
public class UserServiceImpl implements UserService {
//편의상 필드 주입
@Autowired
private PasswordEncoder passwordEncoder;
public void register(User user, String rawPassword) {
String encoderPassword = passwordEncoder.encode(rawPassword);
...
}
}
passwordEncoder의 encode 메서드가 호출될 때 마다 request 스코프의 PasswordEncoder인스턴스가 생성된다.
가짜 프록시 객체는 단순 위임 로직만 있으며, 싱글톤 처럼 동작한다. 그렇기 때문에 스프링 애플리케이션을 실행하는 시점에 PasswordEncoder 빈을 주입할 수 있는 것이다.
하지만 실제로 사용하는 빈은 싱글톤이 아니므로 결국엔 싱글톤 스코프와는 다르게 동작하기 때문에 주의해야 한다.
결론
스코프가 다른 빈을 주입받는 경우에는 스프링이 제공하는 간편한 방법인 ObjectProvider, 자바 표준 방법인 Provier,
Proxy 방법을 사용할 수 있다.
간편하게 스프링이 제공하는 기능을 사용하겠다면 ObjectProvider,
스프링이 아닌 다른 컨테이너에서도 돌리거나 테스트를 해야 한다면 자바 표준 Provider,
웹 스코프를 사용하거나 특별한 코드 수정없이 해결하고 싶다면 Proxy
'Backend > Spring' 카테고리의 다른 글
스프링부트3.2.3 HTTP메세지 로그레벨 변경 (debug->trace) (0) | 2024.03.10 |
---|---|
빈 생명 주기(Bean Life Cycle)와 콜백(callback) (0) | 2023.12.24 |
@Autowired 빈 설정 방식 (+ @Qualifier, @Primary) (0) | 2023.12.22 |
다양한 의존관계 주입 방법 (생성자 주입을 선택해야 하는 이유) (0) | 2023.12.18 |
스프링 빈 설정하는 방법 (XML, @Configuration, @Component) (1) | 2023.12.17 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!