빈 스코프
스코프란 빈이 존재할 수 있는 범위를 뜻하며,
스프링은 기본적으로 싱글톤 스코프로 생성되며 그 외에 다양한 스코프도 지원한다.
종류
- `싱글톤` 스코프
- `프로토타입` 스코프
- `웹` 스코프
빈 스코프 지정 방법
// 컴포넌트 스캔 자동 등록
@Scope("prototype")
@Component
public class HelloBean {}
// 수동 등록
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}
싱글톤 스코프
- 싱글톤의 빈을 요청하면 스프링 컨테이너는 `항상 같은 인스턴스`를 반환한다.
- 같은 객체가 사용되어 성능 최적화를 이룬다.
- 컨테이너가 빈의 생명주기를 관리한다. (@PreDestory 호출 O)
1. 싱글톤 스코프의 빈을 스프링 컨테이너에 요청
2. 스프링 컨테이너가 관리하는 빈 저장소에서 스프링 빈 반환
3. 이후 같은 요청이 와도 같은 객체 인스턴스의 스프링 빈 반환
프로토타입 스코프
- 프로토타입의 빈을 요청하면 스프링 컨테이너는 `항상 새로운 인스턴스`를 생성해서 반환한다.
- 컨테이너는 빈 생성, 의존관계 주입, 초기화까지만 관여한다.
- 그 이후 컨테이너는 더 이상 해당 빈을 관리하지 않는다. (@PreDestory 호출 X)
1. 프로토타입 스코프의 빈을 스프링 컨테이너에 요청
2. 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계를 주입한 후 초기화 실행하여 반환. 후에 관리 X
3. 이후 같은 요청이 와도 항상 새로운 프로토타입 빈을 생성(의존관계 주입, 초기화)하여 반환. 후에 관리 X
스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않으므로 관리할 책임은 빈을 받은 클라이언트에 있다.
(종료 메서드 호출은 클라이언트가 직접 해야한다.)
웹 스코프
- 웹 환경에서만 동작한다.
- 스프링이 해당 스코프의 종료 시점까지 관리한다. (@PreDestory 호출 O)
`request` : HTTP 요청이 시작될 때 생성되고, 요청이 끝나면 소멸
`session` : HTTP 세션이 생성될 때 같이 생성되고, 세션이 종료될 때 소멸
`application` : 서블릿 컨텍스트와 동일한 생명주기(애플리케이션 실행 ~ 종료)를 가지는 스코프
`websoket` : 웹 소켓이 유지되는 동안만 존재
싱글톤 빈이 다른 스코프 빈을 사용할 때 발생하는 문제점들
- 싱글톤 빈이 프로토타입 빈을 사용할 때
- 싱글톤 빈이 request 빈(session 빈 등)을 사용할 때
`ObjectProvider` / `Provider` 와 `프록시 객체` 을 사용하여 `지연 생성`으로 문제를 해결할 수 있다.
싱글톤 빈이 프로토타입 빈을 사용할 때 문제점 - Provider
싱글톤 빈이 생성될 때 프로토타입의 빈을 주입받으면 프로토타입 빈을 사용할 때마다 새로운 객체가 생성되지 않고 기존 객체를 계속 사용하게 된다.
이는 프로토타입의 의미가 사라지는 문제가 발생한다.
가장 간단한 해결 방법은 싱글톤 빈이 프로토타입 빈을 사용할 때마다 스프링 컨테이너에 새로 요청하는 것이다.
따라서 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 `Provider` 를 사용하여 해결할 수 있다.
(ApplicationContext 의 getBean 을 사용해도 되지만
스프링 컨테이너 전체를 생성하면 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다.
따라서 단순한 DL 기능만 제공하는 Provider 를 사용한다.)
> 참고 <
- DL : Dependency Lookup 으로 의존관계 조회(탐색)이라고 한다.
ObjectProvider
- 별도의 라이브러리가 필요 없다.
- 스프링에 의존적이다.
다른 컨테이너에서 사용하지 않을 거라면 ObjectProvider 사용하는 것을 권장한다.
> 참고 <
- 과거에는 ObjectFactory 가 있었는데, 최근에는 편의 기능(옵션, 스트림 처리 등)을 추가한 ObjectProvider 를 사용한다.
PrototypeBean 클래스가 이 프로토타입 스코프라고 가정하자.
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
return prototypeBean.getCount();
}
이렇게 하면 `prototypeBeanProvider.getObject()` 를 통해 PrototypeBean 타입으로 빈을 조회할 때, 새로운 프로토타입 빈이 생성된다.
JSR-330 Provider
- 기능이 매우 단순하다. (get 메서드 하나)
- 별도의 라이브러리가 필요하다.
- 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
PrototypeBean 클래스가 이 프로토타입 스코프라고 가정하자.
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
return prototypeBean.getCount();
}
이렇게 하면 `provider.get()` 를 통해 PrototypeBean 타입을 조회할 때, 새로운 프로토타입 빈이 생성된다.
Provider 상세 작동 순서
1. @Autowired 로 ObjectProvider 를 주입할 때
> 스프링 컨테이너에서는 PrototypeBean 의 BeanDefinition 만 저장
> 실제 객체는 생성되지 않음
2. ObjectProvider.getObject()가 호출되어 PrototypeBean 타입의 빈을 요청할 때
> 스프링 컨테이너는 PrototypeBean 의 BeanDefinition 을 기반으로 새로운 PrototypeBean 객체를 생성하여 반환
> 스프링 컨테이너에 등록하지 않고, 호출될 때마다 새로운 객체를 반환
3. PrototypeBean 이 싱글톤이라면
> 이미 빈 저장소에 등록되어 있으므로 등록된 PrototypeBean 빈을 반환
> 참고 <
- 프로토타입 스코프에는 프록시보다 `ObjectProvide` 를 사용하는 것이 일반적이다.
싱글톤 빈이 request 빈(session 빈 등)을 사용할 때 문제점 - 프록시
싱글톤 빈은 애플리케이션 실행 시 한 번만 생성하고, request 빈은 HTTP 요청이 있을 때마다 생성된다.
하지만 애플리케이션 실행 시점에는 HTTP 요청이 없으므로 request 빈을 생성할 수 없어 문제가 발생한다.
따라서 requset 빈을 가짜 클래스로 주입하고 실제 HTTP 요청이 발생하면 원본 객체를 요청하는 `프록시`를 사용하여 해결할 수 있다.
프록시
- 원본 객체 대신 가짜 객체(프록시)를 먼저 주입하고, 실제 객체가 필요할 때 원본 객체를 호출하는 방식이다.
- 스프링 컨테이너가 `CGLIB`라는 라이브러리를 이용하여 원본 클래스를 상속한 프록시 객체를 생성한다.
- 프록시 객체는 실제 객체를 바로 생성하지 않고, 필요할 때만 원본 객체를 요청하여 생성한다.
(`ac.getBean("해당 메서드 이름", 해당 객체)로 조회해도 프록시 객체가 조회된다.)
- 기존 코드 변경 없이 다양한 스코프에서 활용 가능하다.
- 싱글톤 빈에서 애노테이션 설정만 변경하여 사용하기 때문에 주의해서 사용해야 한다. (싱글톤으로 헷갈릴 수 있음)
`@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)` 프록시를 사용할 수 있다.
> 참고 <
- 프록시 대상이 인터페이스라면 `INTERFACES` 사용하고, 프록시 대상이 클래스라면 `TARGET_CLASS` 사용한다.
request 빈에 프록시를 적용한 예제를 보여주기 위해서는 우선 웹 환경을 추가해야 한다.
build.gradle 에 web 라이브러리를 추가하면 된다. (내장 톰켓 서버를 활용하여 웹 서버와 스프링 함께 실행)
implementation 'org.springframework.boot:spring-boot-starter-web'
> 참고 <
- 스프링 컨테이너는 웹 라이브러리가 추가된 `AnnotationConfigServletWebServerApplicationContext` 로 생성해야 한다.
@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() {
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);
}
}
@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();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
빈이 생성되는 시점에 자동으로 @PostConstruct 초기화 메서드를 통해 uuid 를 생성하여 저장한다.
MyLogger 는 request 빈이기 때문에 uuid 가 HTTP 요청마다 다르므로, uuid 를 통해 HTTP 요청을 구분할 수 있다.
그리고 requestURL 은 해당 빈이 생성되는 시점에는 알 수 없으므로, 외부에서 setter로 입력받는다.
프록시를 적용하여 request 스코프 빈을 생성하여
애플리케이션 실행 시 MyLogger 빈이 없어도 프록시 객체가 먼저 주입된다.
그리고, HTTP 요청이 들어오면 프록시 객체가 실제 MyLogger 객체를 생성하여 반환한다.
main() 메서드로 스프링을 실행하고, 웹 브라우저에 `http://localhost:8080/log-demo` 를 입력하면 잘 작동한다.
> 참고 <
- request 스코프에는 ObjectProvide 보다 `프록시`를 사용하는 것이 일반적이다.
출처 | 스프링 핵심 원리 - 기본편(김영한) - 인프런
'💠프로그래밍 언어 > Java' 카테고리의 다른 글
[Spring] 서블릿의 HttpServletRequest 와 HttpServletResponse 란?? (1) | 2025.03.13 |
---|---|
[Spring] 웹 서버와 웹 애플리케이션 서버(WAS) + 서블릿 컨테이너 (0) | 2025.03.13 |
[Spring] 빈 생명주기 콜백 (@PostConstruct, @PreDestroy) (0) | 2025.02.26 |
[Spring] 특정 타입의 빈을 동적으로 가져오는 방법 (0) | 2025.02.26 |
[Spring] @Autowired 에서 조회되는 빈이 2개 이상이라면 ?? (@Primary, @Qualifier) (0) | 2025.02.26 |