빈 스코프란?
스프링의 지원 스코프
singleton: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프prototype: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까짐나 관려하고 더는 관리하지 않는 매우 짧은 범위의 스코프request: 웹 요청이 들어오고 나갈 때 까지 유지되는 스코프session: 웹 세션이 생성되고 종료될 떄 까지 유지되는 스코프application: 웹 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
컴포넌트 스캔 자동 등록
1
2
3
4
5
|
@Scope("prototype")
@Component
public class Bean {
}
|
컴포넌트 스캔 수동 등록
1
2
3
4
5
6
|
@Scope("prototype")
@Bean
Bean bean() {
return new Bean();
}
|
프로토타입 스코프
- 빈 요청 시, 매번 새로운 객체를 생성하여 반환하고 스프링 컨테이너가 관리하지 않음
- 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존 관계 주입, 초기화까지만 처리
- 따라서,
@Predestory 같은 종료 메서드가 호출되지 않음
싱글톤 빈과 함께 사용 시 문제점
싱글톤 빈에서 프로토타입을 의존 주입 받아 사용하게 되면 사용 할 때마다 새로운 객체이길 기대하겠지만, 의존 주입은 생성 시점에 한 번 주입되는 것이므로, 처음 주입된 객체가 유지됩니다.
싱글톤 빈과 함께 사용시 Provider로 문제 해결
가장 간단한 방법은 매 요청마다 ApplicationContext에서 빈을 조회하여 사용하는 것입니다.
1
2
3
4
5
6
7
8
| public int logic() {
// 매번 새로운 prototypeBean을 반환
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
|
- 의존 관계를 외부에서 주입(
DI) 받는게 아니라 이렇게 직접 필욯란 의존 관계를 찾는 것을 Dependency Lookup(DL) 의존관계 조회(탐색)이라 함 - 이렇게 스프링의 어플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고 단위 테스트도 어려워 짐
DL 정도의 기능만 제공하는 ObjectFactory, ObjectProvider를 사용하여 이 문제를 해결
ObjectFactory, ObjectProvider
1
2
3
4
5
6
7
8
9
10
|
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
|
ObjectProvider의 getObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환(DL)ObjectFactory: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리 등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존
JSR-330 Provider
1
2
3
4
| dependencies {
// ...
implementation("jakarta.inject:jakarta.inject-api:2.0.1")
}
|
1
2
3
4
5
6
7
8
9
10
|
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
|
get() 메서드 하나로 기능이 매우 단순- 별도의 라이브러리가 필요
- 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있음
💡 ObjectProvider vs JSR-330 Provider
스프링이 아닌 다른 컨테이너를 사용할 상황이 있다면 JSR-330 Provider를 사용하면 되지만, 사실상 그럴 일은 거의 없기 때문에 스프링에서 제공하는 ObjectProvider를 사용하면 됩니다.
웹 스코프
request: HTTP 요청 하나가 들어오고 나갈 떄 까지 유지되는 스코프, 각각으 HTTP 요청 마다 별도의 빈 인스턴스가 생성되고 관리됨session: HTTP Session과 동일한 생명주기를 가지는 스코프application: 서블릿 컨텍스트와 동일한 생명주기를 가지는 스코프websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프
request 스코프 예제 만들기
- 매 요청 마다 사용자를 구분하기 위해 위와 같이 로그가 남도록 만들어 보겠습니다.
1
2
3
4
| [d06b992f...] request scope bean create
[d06b992f...][http://localhost:8080/log-demo] controller test
[d06b992f...][http://localhost:8080/log-demo] service id = testId
[d06b992f...] request scope bean close
|
1
2
3
4
| dependencies {
// ...
implementation("org.springframework.boot:spring-boot-starter-web")
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
@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);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@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";
}
}
|
1
2
3
4
5
6
7
8
9
10
11
|
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
|
log-demo로 요청- 컨트롤러에서 로깅 찍음
- 서비스에서 로깅 찍음
- 이 둘은 같은
request 스코프를 가지므로 같은 UUID를 가짐
이렇게 예상하고 실행을 시켜보면 에러가 발생합니다.
그 이유는 MyLogger는 요청이 들어와야 생성되는 객체인데 스프링을 띄우는 시점에는 생성되어 있지 않으니 의존 주입을 할 수 없기 때문입니다.
스코프와 Provider
컨트롤러와 서비스에서 MyLogger 필드를 ObjectProvider<MyLogger>로 수정하고, ObjectProvider.getObject()를 통해 MyLogger 호출하도록 수정 후 실행 해보면 정상적으로 실행하면 정상 동작합니다.
1
2
3
4
5
6
7
8
9
10
11
12
|
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " + id);
}
}
|
ObjectProvider.getObject()를 호출하는 시점까지 request.scope빈의 생성을 지연할 수 있음ObjectProvider.getObject()를 호출하는 시점에는 HTTP 요청이 진행중이므로 request scope빈의 생성이 정상 처리 됨
스코프와 프록시
ObjectProvider를 사용하지 않고 바로 MyLogger를 사용하게 할 수도 있습니다.
1
2
3
4
5
|
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
|
proxyMode = ScopedProxyMode.TARGET_CLASS를 추가- 적용 대성이 클래스면
TARGET_CLASS - 적용 대상이 인터페이스면
INTERFACES
- 이렇게 하면
MyLogger의 가짜 프록시 클래스를 만들어 두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있음
1
2
3
4
5
6
7
8
9
10
11
12
|
@Service
@RequiredArgsConstructor
public class LogDemoService {
// 프록시 객체 주입
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
|
동작 원리
- CGLIB 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입
- 이 가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어 있음
- 가짜 프록시 객체는 실제
request scope와는 관계가 없음. 그냥 가짜이고 내부에 단순한 위임 로직만 있고, 싱글톤 처럼 동작 함
참고