스프링이 제공하는 빈이 존재할 수 있는 범위에 대한 섹션!
1. 빈 스코프란?
빈 스코프는 빈이 존재할 수 있는 범위를 말함
- 스프링 빈은 기본적으로 싱글톤 스코프로 생성되기 때문에 스프링 컨테이너의 시작과 함께 생성돼서 스프링 컨테이너가 종료될 때까지 유지됨
- 아래와 같이 지정할 수 있음
// 컴포넌트 스캔 자동 등록
@Scope("prototype")
@Component
public class HelloBean() {}
// 수동 등록
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}
스프링이 지원하는 다양한 스코프
a. 싱글톤 스코프(default)
- 기본값으로, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
b. 프로토타입 스코프
- 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고, 더는 관리하지 않는 매우 짧은 범위의 스코프
c. 웹 관련 스코프
- request: 웹 요청이 들어오고 나갈 때까지 유지되는 스코프
- session: 웹 세션이 생성되고 종료될 때까지 유지되는 스코프
- application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
2. 프로토타입 스코프
싱글톤과 프로토타입의 빈 요청 차이
a. 싱글톤 빈 요청
빈 조회시 스프링 컨테이너가 항상 같은 인스턴스의 스프링 빈을 반환함
- 싱글톤 스코프의 빈을 스프링 컨테이너에 요청
- 스프링 컨테이너는 자기가 관리하는 스프링 빈을 반환
- 이후 스프링 컨테이너에 같은 요청이 와도 동일한 객체 인스턴스의 스프링 빈을 반환
싱글톤 스코프 빈 코드
- 생성자를 호출하기 전 빈 초기화 메서드를 통해 "SingletonBean.init"이 출력되고, 종료되기 전 종료 메서드를 통해 "SingletonBean.destroy"가 출력됨
@Scope("singleton")
static class SingletonBean {
@PostConstruct
public void init() {
System.out.println("SingletonBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("SingletonBean.destroy");
}
}
b. 프로토타입 빈 요청
빈 조회시 스프링 컨테이너가 항상 새로운 인스턴스를 생성해 반환함
- 프로토타입 스코프의 빈을 스프링 컨테이너에 요청
- 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입, 초기화
- 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환
- 이후 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해 반환
- 기존에 생성된 프로토타입 빈을 관리하지 않음
- 관리는 프로토타입 빈을 받은 클라이언트의 책임
- 따라서 @PreDestroy 같은 종료 메서드가 자동으로 호출되지 않음
프로토타입 스코프 빈 코드
- 스프링 컨테이너에서 빈을 조회할 때 빈이 생성되고, 초기화 메서드도 실행됨 ("PrototypeBean.init" 출력)
- 스프링 컨테이너가 빈의 생성과 의존관계 주입, 초기화까지만 관여하기 때문에 스프링 컨테이너가 종료될 때 @PreDestroy 같은 종료 메서드는 실행되지 않음
- 프로토타입 빈을 조회한 클라이언트가 프로토타입 빈을 관리해야 함 (종료 메서드 직접 호출)
@Scope("prototype")
static class PrototypeBean {
@PostConstruct
public void init() {
System.out.println("PrototypeBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
싱글톤 빈과 함께 사용 시 문제점
싱글톤 빈과 함께 사용하면 의도한 대로(빈 요청 시 새로운 객체 인스턴스 생성 및 반환) 동작하지 않을 때가 있음
a. 프로토타입 빈 직접 요청
스프링 컨테이너에 프로토타입 빈을 직접 요청할 수 있음
- 클라이언트A가 스프링 컨테이너에 프로토타입 빈 요청
- 스프링 컨테이너가 프로토타입 빈을 새로 생성해 반환(x01)
- 해당 빈(x01)의 count 필드 값은 0
- 클라이언트는 조회한 프로토타입 빈에 addCount()를 호출하면서 count 필드에 1을 더함
- 해당 빈(x01)의 count 필드 값은 1
- 클라이언트B가 스프링 컨테이너에 프로토타입 빈 요청
- 스프링 컨테이너가 프로토타입 빈을 새로 생성해 반환(x02)
- 해당 빈(x02)의 count 필드 값은 0
- 클라이언트는 조회한 프로토타입 빈에 addCount()를 호출하면서 count 필드에 1을 더함
- 해당 빈(x02)의 count 필드 값은 1
b. 싱글톤 빈에서 프로토타입 빈 사용
clientBean이라는 싱글톤 빈이 의존관계 주입을 통해 프로토타입 빈을 주입받아 사용하는 예시
clientBean은 싱글톤이므로, 보통 스프링 컨테이너의 생성 시점에 함께 생성되고, 의존관계 주입도 발생함
- clientBean은 의존관계 자동 주입을 사용 → 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청
- 스프링 컨테이너는 프로토타입 빈(x01)을 생성해 clientBean에 반환
- 프로토타입 빈(x01)의 count 필드 값은 0
- clientBean은 프로토타입 빈(참조값)을 내부 필드에 보관함
- 클라이언트A가 clientBean.logic() 호출 → 싱글톤이므로 항상 같은 clientBean이 반환됨
- clienBean은 prototypeBean의 addCount()를 호출해 프로토타입 빈의 count를 증가시킴
- 프로토타입 빈(x01)의 count 필드 값은 1
- 클라이언트B가 clientBean.logic() 호출 → 싱글톤이므로 항상 같은 clientBean이 반환됨
- clientBean은 prototypeBean의 addCount()를 호출해 프로토타입 빈의 count를 증가시킴
- 프로토타입 빈(x01)의 count 필드 값은 2
주의: 여기서 clientBean이 내부에 갖고 있는 프로토타입 빈은 이미 과거(싱글톤 의존관계 주입 시점)에 주입이 끝난 빈이므로 사용할 때마다 새로 생성되지 않음
- 스프링은 일반적으로 싱글톤 빈을 사용하므로 싱글톤 빈이 프로토타입 빈을 사용하게 됨
- 이때, 싱글톤 빈은 생성 시점에만 의존관계를 주입받기 때문에 프로토타입 빈이 생성되긴 하지만, 싱글톤 빈과 함께 계속 유지되는 것이 문제가 됨
- 사용할 때마다 새로 생성해서 사용하는 것을 원하는 경우 의도한 대로 동작하지 않음
참고: 여러 빈에서 같은 프로토타입 빈을 주입받으면, 주입받는 시점에 각각 새로운 프로토타입 빈이 생성됨
- 사용할 때마다 새로 생성되는 것은 아님
싱글톤 빈과 함께 사용 시 Provider로 문제 해결
사용할 때마다 항상 새로운 프로토타입 빈을 생성하기 위한 방법
a. 스프링 컨테이너에 요청
가장 간단한 방법으로, 싱글톤 빈이 프로토타입을 사용할 때마다 스프링 컨테이너에 새로 요청하는 것
static class ClientBean {
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
- ac.getBean()을 통해 항상 새로운 프로토타입 빈이 생성됨
- Dependency Lookup = 의존관계 조회(탐색)
- 의존관계를 외부에서 주입(DI)받는 게 아니라 직접 필요한 의존관계를 찾는 것
- 스프링 애플리케이션 컨텍스트 전체(private ApplicationContext ac;)를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고 단위 테스트도 어려워짐
- DL 정도의 기능만 제공하는 것을 적용하는 게 좋음
b. ObjectFactory, ObjectProvider
지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공함
- 과거에는 ObjectFactory가 있었는데, 편의 기능을 추가한 ObjectProvider가 만들어짐
- ObjectFactory
- 기능이 단순하고, 별도의 라이브러리가 필요하지 않음
- 스프링에 의존
- ObjectProvider
- ObjectFactory 상속, 옵션, 스트림 처리 등의 편의 기능이 많고, 별도의 라이브러리가 필요하지 않음
- 스프링에 의존
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
- prototypeBeanProvider.getObject()를 통해 항상 새로운 프로토타입 빈이 생성됨
- 메서드 호출 시 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환 (=DL)
- 스프링이 제공하는 기능을 사용하지만, 기능이 단순하므로 단위 테스트를 만들거나 mock 코드를 만들기 쉬워짐
c. JSR-330 Provider
jakarta.inject.Provider라는 JSR-330 자바 표준을 사용하는 방법
- 사용하기 위해 "jakarta.inject:jakarta.inject-api:2.0.1" 라이브러리를 build.gradle에 추가해야 함
- 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용 가능
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
- provider.get()을 통해 항상 새로운 프로토타입 빈이 생성됨
- 메서드를 호출하면 스프링 컨테이너를 통해 해당 빈을 찾아서 반환 (=DL)
- get() 메서드 하나로 기능이 매우 단순함
- 자바 표준이고, 기능이 단순하므로 단위 테스트를 만들거나 mock 코드를 만들기 쉬워짐
프로토타입 빈을 언제 사용하는지
매번 사용할 때마다 의존관계 주입이 완료된 새로운 객체가 필요한 경우 사용하면 됨
- 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드묾
- ObjectProvider, JSR-330 Provider 등은 프로토타입 뿐만 아니라 DL이 필요한 경우 언제든지 사용 가능
참고: 스프링이 제공하는 메서드에 @Lookup 어노테이션을 사용하는 방법도 있지만 생략함
참고: 자바 표준인 JSR-330 Provider와 스프링이 제공하는 ObjectProvider 중 실무에서 무엇을 사용해야 하는지
- 특별히 다른 컨테이너를 사용할 일이 없다면 대부분 스프링이 다양하고 편리한 기능을 제공해주기 때문에 스프링이 제공하는 기능을 사용하는 것이 편함
3. 웹 스코프
웹 스코프는 웹 환경에서만 동작하며, 프로토타입과는 다르게 스프링이 해당 스코프의 종료 시점까지 관리하기 때문에 종료 메서드가 호출됨
- 웹 스코프는 웹 환경에서만 동작하므로 build.gradle에 "org.springframework.boot:spring-boot-starter-web" 라이브러리를 추가해야 함
- 추가 시 스프링 부트는 내장 톰켓 서버를 활용해 웹 서버와 스프링을 함께 실행시킴
- 웹 라이브러리가 없으면 AnnotationConfigApplicationContext를 기반으로 실행시킴
- 웹 라이브러리가 추가되면 AnnotationConfigServletWebServerApplicationContext를 기반으로 실행시킴
- 기본 포트인 8080 포트를 다른 곳에서 쓰고 있다면 9090 포트로 변경해 오류 해결 가능 (application.yml 수정)
- 추가 시 스프링 부트는 내장 톰켓 서버를 활용해 웹 서버와 스프링을 함께 실행시킴
웹 스코프의 종류
request | HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리됨 |
session | HTTP Session과 동일한 생명주기를 갖는 스코프 |
application | 서블릿 컨텍스트(Servlet Context)와 동일한 생명 주기를 갖는 스코프 |
websocket | 웹 소켓과 동일한 생명 주기를 갖는 스코프 |
request 스코프 예시
HTTP request 요청 당 각각 할당되는 request 스코프
- 나머지 3개의 스코프도 범위만 다르지 동작 방식은 비슷함
a. request 스코프 예제 개발
동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어려움
- 이때 request 스코프를 사용하면 로그를 남길 수 있음 (request마다 UUID를 지정해 구분 가능)
로그를 출력하기 위한 MyLogger class
@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() {
...
}
@PreDestroy
public void close() {
...
}
}
- request 스코프로 지정했기 때문에 해당 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸됨
- 생성 시점에 @PostConstructor 초기화 메서드를 사용해 uuid를 생성해 저장 → 다른 HTTP 요청과 구분 가능
- 소멸 시점에 @PreDestroy를 사용해 종료 메시지를 남김
- requestURL은 빈 생성 시점엔 알 수 없으므로 외부에서 setter로 입력받음
- 외부에서 입력할 때, 컨트롤러보다는 공통 처리가 가능한 스프링 인터셉터나 서블릿 필터 같은 곳을 활용하는 게 좋음
이대로 스프링 애플리케이션을 실행하면 오류가 발생
Error creating bean with name 'myLogger': Scope 'request' is not active for the current thread;
consider defining a scoped proxy for this bean if you intend to refer to it from a singleton;
- 스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 아직 생성되지 않음 → 실제 고객의 요청이 와야 생성 가능함
스코프와 Provider
앞서 말한 문제를 Provider로 해결 가능
- 컨트롤러와 서비스에 ObjectProvider를 추가해 ObjectProvider.getObject() 메서드를 호출하는 시점까지 request 스코프 빈의 생성을 지연시킬 수 있음
- 해당 메서드 호출 시점엔 HTTP 요청이 진행 중이므로 request 스코프 빈의 생성이 정상 처리됨
- 해당 메서드를 컨트롤러와 서비스에서 각각 한 번씩 따로 호출해도 같은 HTTP 요청이라면 동일한 스프링 빈이 반환됨
MyLogger myLogger = myLoggerProvider.getObject();
스코프와 프록시
Provider 말고 프록시 방식을 사용할 수도 있음
- 웹 스코프가 아니어도 사용 가능
- 어노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체 가능
a. 코드
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
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() {
...
}
@PreDestroy
public void close() {
...
}
}
- proxyMode 옵션에서 적용 대상이 interface가 아닌 class면 TARGET_CLASS, interface면 INTERFACES 선택
- MyLogger에서 가짜 프록시 class를 만들어두고, HTTP request와 상관없이 가짜 프록시 class를 다른 빈에 미리 주입해 둘 수 있음
b. 동작 원리
주입된 MyLogger를 확인해보면, CGLIB이라는 라이브러리로 내 class를 상속받은 가짜 프록시 객체를 만들어서 주입한 걸 확인할 수 있음
System.out.println("myLogger = " + myLogger.getClass());
// 출력 결과
// myLogger = class hello.core.common.MyLogger$$EnhanceBySpringCGLIB$$nfjfs2ed
- @Scope의 proxyMode 옵션을 설정하면 스프링 컨테이너는 CGLIB이라는 바이트코드를 조작하는 라이브러리를 사용해, MyLogger를 상속받은 가짜 프록시 객체를 생성함
- 스프링 컨테이너에 myLogger라는 이름으로 진짜 대신 이 가짜 프록시 객체를 등록함 (의존관계 주입도 가짜로)
- 가짜 프록시 객체는 요청이 오면 내부에서 실제 빈을 요청하는 위임 로직을 호출함
- 클라이언트가 myLogger.log()를 호출하면 사실은 가짜 프록시 개체의 메서드를 호출한 것
- 가짜 프록시 개체가 request 스코프의 실제 myLogger.log()를 호출함
- 클라이언트 입장에서는 원본인지 아닌지 모름 (다형성)
- 진짜 객체 조회를 필요한 시점까지 지연 처리한다는 것이 중요함
- 클라이언트가 myLogger.log()를 호출하면 사실은 가짜 프록시 개체의 메서드를 호출한 것
- 실제 request 스코프와는 관계가 없고, 내부에 단순한 위임 로직만 있으며 싱글톤처럼 동작함
-
- 따라서, 프록시 객체 덕분에 클라이언트는 싱글톤 빈을 사용하듯 편리하게 request 스코프 사용 가능