빈이 존재하는 생명 주기 범위를 지정하는 것이다.
스프링에서는 기본적으로 모든 빈이 싱글톤이다.
즉, 애플리케이션 시작 시 한 번만 생성되고 모든 의존 주입 시 동일한 인스턴스를 공유하게 된다.
그런데 이렇게 항상 같은 객체를 공유하는 것이 불편한 상황도 있다.
예를 들어, 매번 새로운 객체가 필요한 경우나 요청마다 다른 객체가 필요할 경우이다. 이럴 때 사용하는 개념이 바로 빈 스코프이다.
👉 스프링은 아래와 같은 스코프를 지원한다.
싱글톤 : 기본값. 컨테이너 시작~종료까지 1개의 인스턴스 유지프로토타입 : 요청할 때마다 매번 새로운 인스턴스 생성request : 웹 요청마다 새로운 빈 생성 session : HTTP 세션마다 하나의 인스턴스 유지application : 서블릿 컨텍스트 범위와 같은 스코프 websocket : 웹 소켓과 동일한 생명주기를 가지는 스코프 👉 빈 스코프 지정하는 방법
@Component
@Scope("prototype")
public class HelloBean {}
또는 수동으로
@Bean
@Scope("prototype")
public HelloBean helloBean() {
return new HelloBean();
}
싱글톤과 달리 프로토타입(prototype) 스코프는 스프링 컨테이너가 매번 새로운 인스턴스를 생성해서 반환한다.
💡 동작 흐름
@PreDestory 메서드도 호출되지 않음!!@Scope("prototype")
static class PrototypeBean {
@PostConstruct
public void init() {
System.out.println("init");
}
@PreDestroy
public void destroy() {
System.out.println("destroy");
}
}
이렇게 작성하고 ac.getBean(PrototypeBean.class)을 2번 호출하면 서로 다른 인스턴스가 생성되어init()은 각각 호출되지만 destroy()는 호출되지 않는다.
‼️ 즉, 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입 그리고 초기화까지만 관여한다.
아래 그림은 싱글톤 빈(clientBean)이 프로토타입 빈(PrototypeBean)을 주입받아 사용하는 구조를 보여준다.
clientBean이 생성될 때 프로토타입 빈이 한 번 주입logic()을 호출하면 count는 1clientBean을 사용하므로 다시 호출 시 count가 2로 증가⚠️ 싱글톤 빈은 생성 시점에만 의존성이 주입되기 때문에 프로토타입 빈이 사용 시마다 새로 생성되지 않고 처음 주입된 인스턴스를 계속 사용하게 된다. 즉, 프로토타입의 의미가 사라지는 것이다!
// 생략된 코드 동일하게 유지
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int count1 = clientBean1.logic(); // count = 1
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int count2 = clientBean2.logic(); // count = 2
프로토타입의 생명주기를 활용하고 싶다면 매번 새롭게 요청하는 방식으로 로직을 수정할 필요가 있다.
싱글톤 빈에서 프로토타입 빈을 사용할 때마다 새로 생성되게 하려면 가장 간단한 방법은 필요할 때 스프링 컨테이너에 직접 요청하는 것(DL)이다.
이는 의존관계를 외부에서 주입받는 것(DI)이 아니라 필요한 의존관계를 직접 찾는 것(Dependency Lookup)이다.
그런데 ApplicationContext 전체를 주입받는 방식은 스프링 컨테이너에 종속적인 코드가 되고 단위 테스트도 어려워진다.
@Autowired
private ObjectProvider<PrototypeBean> provider;
public int logic() {
// 매번 새 인스턴스 반환
PrototypeBean prototypeBean = provider.getObject();
prototypeBean.addCount();
return prototypeBean.getCount();
}
ObjectProvider를 제공한다 provider.getObject()를 호출하는 시점에 스프링 컨테이너가 새로운 프로토타입 빈을 생성하여 반환한다.@Autowired
private javax.inject.Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
return prototypeBean.getCount();
}
javax.inject:javax.inject:1자바 표준과 스프링 기능이 겹치는 경우, 특별히 다른 컨테이너를 사용할 일이 없다면 스프링이 제공하는 기능을 사용하자!
스프링에서는 웹 애플리케이션을 개발할 때 요청마다 새롭게 생성되어야 하는 객체가 필요할 때가 있다. 예를 들어, HTTP 요청마다 고유한 로깅 정보를 담고 싶은 경우이다.
이럴 때 사용하는 것이 바로 웹 스코프(Web Scope)이다.
아래는 가장 많이 사용되는 request 스코프를 중심으로 설명한다.
MyLogger라는 클래스를 만들어 요청마다 고유한 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 created: " + this);
}
@PreDestroy
public void destroy() {
System.out.println("[" + uuid + "] request scope bean closed: " + this);
}
}
다음처럼 MyLogger를 일반 싱글톤 컨트롤러에 그냥 주입하면 애플리케이션 실행 시점에 에러가 난다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final MyLogger myLogger; // ❌ 에러 발생
...
}
💡 왜 에러가 날까?
@Scope("request")는 HTTP 요청이 있을 때만 생성될 수 있다.MyLogger 빈도 만들 수 없다. @Controller
@RequiredArgsConstructor
public class LogDemoController {
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("/log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
// 요청 시점에 빈 생성
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.setRequestURL(request.getRequestURL().toString());
myLogger.log("controller test");
return "OK";
}
}
이렇게 하면 getObjcet()를 호출하는 시점에 HTTP 요청이 진행중이므로MyLogger 빈이 생성된다.
즉, 빈 생성을 지연시켜서 문제를 해결하는 것이다.
더 간단히 처리할 수 있는 방법은 프록시 객체를 사용하는 것이다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
...
}
이 설정을 추가하면 스프링이 MyLogger를 상속받은 가짜 객체(프록시)를 먼저 만들어서 싱글톤 빈들에 미리 주입해놓는다.
그럼 컨트롤러는 아래와 같이 평범하게 사용할 수 있다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final MyLogger myLogger; // ➡️ 프록시 객체가 들어옴
@RequestMapping("/log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
myLogger.setRequestURL(request.getRequestURL().toString());
myLogger.log("controller test");
return "OK";
}
}
프록시 객체는 내부에서 실제 request scope 빈을 찾아서 필요할 때 위임한다.
즉, 개발자는 마치 싱글톤처럼 쓰지만 내부에서는 요청마다 새 객체가 사용되는 것이다.
프록시가 실제로 동작하는지 확인하려면
System.out.println("myLogger = " + myLogger.getClass()); 로 출력을 하면
myLogger = ..MyLogger$$EnhancerBySpringCGLIB... 라고 출력이된다.
특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자. 무분별하게 사용하면 유지보수하기 어려워진다.
🌟 두 방식의 핵심 공통점은 모두 실제 빈 생성을 필요한 시점까지 미룬다는 것이다. 실제 객체는 지연 생성되고 로직은 그대로 유지할 수 있다는 점이 큰 장점이다.