섹션 10 : 빈 스코프
스프링 빈은 기본적으로 싱글톤 스코프(빈이 존재할 수 있는 범위)로 생성된다.
스프링이 지원하는 스코프
- 싱글톤 : 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
- 포로토타입 : 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관여하지 않은 매우 짧은 범위의 스코프
- 웹 관련 스코프
- request : 웹 요청이 들어오고 나갈때 까지 유지되는 스코프
- session : 웹 세션이 생성되고 종료될 때까지 유지되는 스코프
- application : 웹의 서블릿 컨텍스와 같은 범위로 유지되는 스코프
import static org.assertj.core.api.Assertions.assertThat;
public class SingletonTest {
@Test
void singletonBeanFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
SingletonBean SingletonBean = ac.getBean(SingletonBean.class);
SingletonBean SingletonBean2 = ac.getBean(SingletonBean.class);
System.out.println("SingletonBean = " + SingletonBean);
System.out.println("SingletonBean2 = " + SingletonBean2);
assertThat(SingletonBean).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@1e0f9063) 동일한 빈으로 나온다.
종료 메서드까지 정상 호출된 것을 확인할 수 있다.
컴포넌트 스캔 자동 등록
@Scope("prototype")
@Component
public class HelloBean {}
컴포넌트 스캔 수동 등록
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}
프로토타입 스코프
싱글톤 스코프의 스포크를 스프링 컨테이너에 조회하면 항상 같은 인스턴스의 빈을 반환
but) 프로토타입 스코프를 스프링 컨테이너에 조회하면 항상 새로운 인스턴스를 생성해서 반환
프로토타입 빈 요청
- 스프링 컨테이너에 프로토타입 스코프 빈 요청
- 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입
- 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환
- 스프링 컨테이너에 같은 요청이 다시 들어오면, 또 다른 새로운 인스턴스를 생성해 반환
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);
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");
}
}
}
프로토타입 스코프의 빈은 스프링 컨테이너에서 빈을 조회할 때 생성되고, 초기화 메서드도 실행된다.
프로토타입 빈을 2번 조회했으므로 2개의 다른 스프링 빈이 생성되고, 초기화도 2번 실행된 것을 볼 수 있다.
- 싱글톤 빈은 스프링 컨테이너가 전 과정을 관리한다.
→ 컨테이너 종료 시 @PreDestroy가 자동 호출된다. - 프로토타입 빈은 스프링이 생성, 의존관계 주입, 초기화까지만 관여한다.
→ 이후의 생명주기 관리 책임은 클라이언트에게 있다.
즉, 프로토타입 빈의 @PreDestroy 메서드는 자동으로 호출되지 않는다.
‼️ 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리 ‼️
이후 소멸 메소드 호출을 포함한 생명주기 관리는 클라이언트가 직접 수행해야 한다.
프로토타입 스코프와 싱글톤 빈을 함께 사용할 때의 문제점
싱글톤 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체를 생성해서 반환한다.
@Test
void singletonClientUsePrototype(){
AnnotationConfigApplicationContext ac =
new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int count1 = clientBean1.logic();
assertThat(count1).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int count2 = clientBean2.logic();
assertThat(count2).isEqualTo(2);
}
위 테스트에서는 ClientBean을 두 번 요청하고 있다. 하지만 ClientBean은 싱글톤이기 때문에 두 번 요청해도 같은 인스턴스가 반환된다.
🔹 코드 실행 결과
테스트 결과에서 count1 == 1, count2 == 2가 나왔다.
이는 PrototypeBean의 count 값이 증가하고 있다는 의미이며, 같은 PrototypeBean 인스턴스를 계속 사용하고 있다는 증거이다.
@Scope("singleton")
static class ClientBean{
private final PrototypeBean prototypeBean; // 생성시점에 주입
@Autowired // 스프링 컨테이너가 프로토타입 빈을 만들어서 할당해줌
public ClientBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic(){
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
- ClientBean은 싱글톤이므로, 스프링 컨테이너에서 한 번만 생성된다.
- PrototypeBean은 생성 시점에 한 번만 주입되며, 이후에도 계속 같은 인스턴스를 사용한다.
- 즉, logic()을 여러 번 호출해도 같은 PrototypeBean을 사용하기 때문에 count 값이 유지된다.
우리가 @Scope("prototype")을 사용하는 이유는 필요할 때마다 새로운 인스턴스를 사용하고 싶어서다. 주입 시점에만 한 번 생성해서 사용할 거라면 굳이 프로토타입 스코프를 사용할 이유가 없다.
ObjectFactory, ObjectProvider
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
prototype.getObject()을 통해 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
ObjectProvider의 getObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)
ObjectFactory, ObjectProvider 모두 스프링에 의존한다.
- ObjectFactory : 기능이 단순하고 별도의 라이브러리가 필요없다.
- ObjectProvider : ObjectFactory 상속, 옵션, 스트림 처리등의 편이 기능이 많고 별도의 라이브러리가 필요없다.
jakarta.inject.Provider`
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
provider.get()을 통해 항상 새로운 프로토타입이 생성된다.
provider의 get을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)
Provider
- get() 메서드 하나로 기능이 매우 단순하다.
- 별도의 라이브러리가 필요하다. (build.gradle에 선언이 필요하다 ! )
- 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
웹 스코프
- 스프링에서 웹 스코프는 웹 환경에서만 동작한다.
- 스프링이 해당 스코프의 종료 시점까지 빈의 생명주기를 직접 관리한다. (종료 시 @PreDestroy 메서드도 정상 호출된다.)
웹 스코프의 종류
- request : 하나의 HTTP 요청이 들어오고 나갈 때까지 빈이 유지된다 → 요청마다 별도 인스턴스 생성
- session : HTTP Session과 동일한 생명주기
- application : 서블릿 컨텍스트와 동일한 생명주기를 가짐
- websocket : 웹 소켓과 동일한 생명주기를 가짐
동시에 여러 HTTP 요청이 오면 어떤 요청이 남긴 로그인지 구분하기 어렵다.
package hello.core.common;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.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 create : " + this);
}
@PreDestroy
public void close(){
System.out.println("[" + uuid + "] request scope bean close : " + this);
}
}
- @Scope("request") : 요청 당 하나의 빈 생성
- uuid : 요청 식별용 고유 ID
- requestURL : 요청 URL 정보 설정
요청 식별용 UUID와 URL을 로그에 함께 출력함으로써 여러 요청 간 로그를 구분할 수 있다.
package hello.core.web;
import hello.core.common.MyLogger;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request){
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
ObjectProvider를 사용하면, 빈의 생성을 필요한 시점으로 미룰 수 있어 안전하게 사용할 수 있다.
- ObjectProvider<MyLogger> : 지연 조회 기능 제공
- getObject() 호출 시점에 빈이 생성되므로, HTTP 요청 범위 안에서 안전하게 생성됨
- 같은 HTTP 요청 내에서는 컨트롤러와 서비스에서 동일한 MyLogger 인스턴스가 사용됨
스코프와 프록시
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
- proxyMode = ScopedProxyMode.TARGET_CLASS -> 스프링이 MyLogger를 상속한 프록시 객체(CGLIB)를 생성한다.
- 이 프록시는 진짜 MyLogger 객체가 필요해지는 시점에 내부에서 실제 빈을 찾아 동작한다.
package hello.core.web;
import hello.core.common.MyLogger;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@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();
System.out.println("myLogger = " + myLogger.getClass());
myLogger.setRequestURL(requestURL);
myLogger.log("controller test")
logDemoService.logic("testId");
return "OK";
}
}
System.out.println("myLogger = " + MyLogger.getClasse()); 코드를 통해 주입된 myLogger를 확인해보자.
→ 프록시 객체가 실제로 주입되었음을 확인할 수 있다.
- 프록시는 원본 클래스를 상속받아 만들어졌기 때문에, 클라이언트 입장에서는 원본인지 가짜인지 모르게 동일하게 사용할 수 있다.
- 프록시 덕분에 마치 싱글톤 빈처럼 의존성 주입이 가능하다.
- 실제 요청이 오면 그때 진짜 빈을 찾아서 동작함 (위임 방식)
'Spring Study' 카테고리의 다른 글
[스프링 MVC] #2 (0) | 2025.04.13 |
---|---|
[스프링 MVC] #1 (0) | 2025.04.10 |
[스프링 핵심 원리] - 기본편 #8 (0) | 2025.04.03 |
[스프링 핵심 원리] - 기본편 #7 (0) | 2025.04.02 |
[스프링 핵심 원리] - 기본편 #6 (0) | 2025.04.02 |