우리가 일반적으로 빈(Bean)이라고 생각하는 것은, 스프링 컨테이너가 시작 될 때 함께 생성되고, 스프링 컨테이너가 종료될 때까지 유지되는 것이다. 이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. 스코프란 빈이 존재할 수 있는 범위를 말한다.
싱글톤: 디폴트 스코프. 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존 관계 주입까지만 관여하고, 더는 관리하지 않는 매우 짧은 범위의 스코프이다. 따라서 종료 메서드는 호출되지 않는다.
웹 관련 스코프
빈 스코프는 다음과 같이 지정할 수 있다.
@Scope("prototype")
@Component
public class HelloBean {}
수동 등록
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}
싱글톤 스코프의 빈을 조회하면, 스프링 컨테이너는 항상 같은 인스턴스를 반환한다. 반면 프로토타입 스코프의 경우, 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다. 또한 프로토타입 스코프의 빈은 싱글톤과 달리 생성된 이후에는 스프링 컨테이너에 의해 관리되지 않는다. 따라서 종료 메서드는 호출되지 않는다.
싱글톤 스코프 빈 테스트
package hello.core.scope;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
public class SingletonTest {
@Test
void singletonBeanFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
System.out.println("singletonBean1 = " + singletonBean1);
System.out.println("singletonBean2 = " + singletonBean2);
Assertions.assertThat(singletonBean1).isSameAs(singletonBean2);
ac.close();
}
@Scope("singleton")
static class SingletonBean {
@PostConstruct
public void init() {
System.out.println("SingletonBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("SingletonBean.destroy");
}
}
}
실행 결과
SingletonBean.init
singletonBean1 = hello.core.scope.PrototypeTest$SingletonBean@54504ecd
singletonBean2 = hello.core.scope.PrototypeTest$SingletonBean@54504ecd
org.springframework.context.annotation.AnnotationConfigApplicationContext -
Closing SingletonBean.destroy
프로토타입 스코프 빈 테스트
package hello.core.scope;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import static org.assertj.core.api.Assertions.*;
public class PrototypeTest {
@Test
void prototypeBeanFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
System.out.println("find prototypeBean1");
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
System.out.println("find prototypeBean2");
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
System.out.println("prototypeBean1 = " + prototypeBean1);
System.out.println("prototypeBean2 = " + prototypeBean2);
assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
prototypeBean1.destroy();
prototypeBean2.destroy();
ac.close();
}
@Scope("prototype")
static class PrototypeBean {
@PostConstruct
public void init() {
System.out.println("PrototypeBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}
실행 결과
find prototypeBean1
PrototypeBean.init
find prototypeBean2
PrototypeBean.init
prototypeBean1 = hello.core.scope.PrototypeTest$PrototypeBean@13d4992d
prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@302f7971
org.springframework.context.annotation.AnnotationConfigApplicationContext -
Closing
싱글톤 빈은 스프링 컨테이너 생성 시점에 초기화 메서드가 실행되지만, 프로토타입 스코프의 빈은 스프링 컨테이너에서 빈을 조회할 때 생성되고, 초기화 메서드도 실행된다.
프로토타입 빈을 2번 조회했으므로 완전히 다른 스프링 빈이 생성되고, 초기화도 2번 실행된 것을 확인할 수 있다.
싱글톤 빈은 스프링 컨테이너가 관리하기 때문에 스프링 컨테이너가 종료될 때 빈의 종료 메서드가 실행되지만, 프로토타입 빈은 스프링 컨테이너가 생성과 의존관계 주입 그리고 초기화까지만 관여하고, 더는 관리하지 않는다. 따라서 프로토타입 빈은 스프링 컨테이너가 종료될 때 @PreDestroy
같은 종료 메서드가 실행되지 않는다.
프로토타입 스코프의 빈은 해당 타입의 빈이 스프링 컨테이너에 요청될 떄마다 생성된다. 헌테 싱글톤 스코프의 빈이 프로토타입의 빈을 주입 받는다면, 싱글톤 스코프의 빈이 생성되고 의존 관계가 주입되는 시점에만 프로토타입 빈이 조회될 것이고, 이후에는 계속 같은 빈이 사용될 것이다. 대부분의 경우 개발자는 이를 의도하지 않았을 것이다. 애초에 매번 새로운 객체가 필요하기 때문에 프로토타입으로 선언했을 것이기 때문이다. 따라서 아래와 같은 테스트 코드를 실행한다면, 테스트는 실패할 것이다.
싱글톤 + 프로토타입
class SingletonWithPrototypeTest1 {
@Test
void singletonClientUserPrototype() {
final AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class, ClientBean.class);
final ClientBean clientBean1 = ac.getBean(ClientBean.class);
final int count1 = clientBean1.logic();
assertThat(count1).isEqualTo(1);
final ClientBean clientBean2 = ac.getBean(ClientBean.class);
final int count2 = clientBean2.logic();
assertThat(count2).isEqualTo(1);
ac.close();
}
@Scope("singleton")
static class ClientBean {
private final PrototypeBean prototypeBean; // 생성 시점에 주입
public ClientBean(final PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic() {
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
@Scope("prototype")
static class PrototypeBean {
private int count = 0;
public void addCount() {
count++;
}
public int getCount() {
return count;
}
@PostConstruct
public void init() {
System.out.println("PrototypeBean.init " + this);
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}
프로토타입 빈을 의도대로 사용하기 위해선 Provider를 추가해주면 된다.
ObjectProvider는 지정한 빈을 컨테이너에서 대신 찾아주는 DL(Dependency Lookup) 서비스를 제공한다. 원래는 ObjectFactory만 있었는데, 여기에 편의 기능을 추가해서 ObjectProvider가 만들어졌다. 아래와 같이 사용할 수 있다.
@Scope("singleton")
static class ClientBean {
private final ObjectProvider<PrototypeBean> prototypeBeanProvider;
public ClientBean(final ObjectProvider<PrototypeBean> prototypeBeanProvider) {
this.prototypeBeanProvider = prototypeBeanProvider;
}
public int logic() {
final PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
이 방법은 javax.inject.Provider 라는 JSR-330 자바 표준을 사용하는 방법이다. 이 방법을 사용하기 위해선 jakarta.inject:jakarta.inject-api:2.0.1(스프링 부트 3.0 이상) 라이브러리를 gradle에 별도로 추가해야 한다. 아래와 같이 사용할 수 있다.
@Scope("singleton")
static class ClientBean {
private final Provider<PrototypeBean> prototypeBeanProvider;
ClientBean(final Provider<PrototypeBean> prototypeBeanProvider) {
this.prototypeBeanProvider = prototypeBeanProvider;
}
public int logic() {
final PrototypeBean prototypeBean = prototypeBeanProvider.get();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
웹 스코프는 웹 환경에서만 동작한다. 웹 스코프는 프로토타입과는 다르게, 스프링 컨테이너가 해당 스코프의 종료 시점까지 관리를 한다. 따라서 종료 메서드가 호출된다.
HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다.
HTTPP request 요청 당 각각 할당되는 request 스코프
@Component
@Scope(value = "request")
public class MyLogger {
private String uuid;
private String requestUrl;
public void setRequestUrl(final 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);
}
}
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("/log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) throws InterruptedException {
final String requestURL = request.getRequestURL().toString();
final MyLogger myLogger = myLoggerProvider.getObject();
myLogger.setRequestUrl(requestURL);
myLogger.log("controller test");
Thread.sleep(1000);
logDemoService.logic("testId");
return "OK";
}
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(final String id) {
final MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " + id);
}
}
ObjectProvider
덕분에 ObjectProvider.getObject()
를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다.ObjectProvider.getObject()
를 호출하시는 시점에는 HTTP 요청이 진행중이므로 request scopeObjectProvider.getObject()
를 LogDemoController
, LogDemoService
에서 각각 한번씩 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환된다!proxyMode를 사용하면 마치 싱글톤 스코프 빈을 주입받는 것처럼 코드를 간결하게 만들 수 있다. 적용 대상이 클래스라면 proxyMode = ScopedProxyMode.TARGET_CLASS, 인터페이스라면 proxyMode = ScopedProxyMode.INTERFACES 를 선택하면 된다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
private String uuid;
private String requestUrl;
public void setRequestUrl(final 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);
}
}
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("/log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) throws InterruptedException {
final String requestURL = request.getRequestURL().toString();
myLogger.setRequestUrl(requestURL);
myLogger.log("controller test");
System.out.println("myLogger = " + myLogger);
Thread.sleep(1000);
logDemoService.logic("testId");
return "OK";
}
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(final String id) {
myLogger.log("service id = " + id);
}
}
프록시 모드를 사용하면, 스프링은 CGLIB이라는 라이브러리를 이용하여, 내 클래스를 상속 받은 프록시 객체를 만들어서 주입한다. 따라서 스프링 컨테이너거 생성되고 빈의 의존 관게를 주입할 때, 프록시 객체가 주입되는 것이다.
가짜 프록시 객체는 HTTP 요청이 들어왔을 때 내부에서 진짜 빈을 요청하는 위임 logic을 가지고 있다. 프록시 객체는 원본 클래스를 상속해서 만들어졌기 때문에, 이 객체를 사용하는 클라이언트 입장에서는 원본인지 아닌지 모르고 동일하게 사용할 수 있다(다형성).
특징 정리
주의점