빈 스코프
지금까지 스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료될 때 까지 유지된다고 했다. 이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. 스코프는 빈이 존재할 수 있는 범위를 뜻한다.
스프링이 지원하는 다양한 스코프
- 싱글톤 : 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
- 프로토타입 : 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.
- 웹 관련 스코프
- request : 웹 요청이 들어오고 나갈때 까지 유지되는 스코프
- session : 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
- application : 웹의 서블릿 컨텍스와 같은 범위로 유지되는 스코프
스코프 지정
1
2
3
4
5
6
7
8
9
@Scope("prototype")
@Component
public class ~{}
@Scope("prototype")
@Bean
PrototypeBean HelloBean(){
return new HelloBean();
}
프로토타입 스코프
싱글톤 스코프의 빈을 조회하면 항상 같은 인스턴스의 스프링 빈을 반환한다. 하지만 프로토타입 빈은 조회하면 항상 새로운 인스턴스를 생성해 반환한다.
스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리하기 때문에 클라이언트에 빈을 반환하고 스프링 컨테이너의 역할은 끝이다.
따라서 프로토타입 빈을 받은 클라이언트가 프로토타입 빈을 관리하는 책임을 가진다. 그래서 @PreDestory 같은 종료 메서드가 호출되지 않는다.
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
28
29
30
31
32
33
34
35
36
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("prototypeBean1 = " + prototypeBean2);
Assertions.assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
ac.close(); //스프링 컨테이너 닫기.
}
@Scope("prototype")
/* @Component 없는데 되는 이유는
new AnnotatinoConfigApplicationContext(~.class);
에서 class를 지정해주면 Component로 알아서 인식한다. */
static class PrototypeBean{
@PostConstruct
public void init(){
System.out.println("PrototypeBean.init");
}
@PreDestroy
public void destroy(){
System.out.println("PrototypeBean.close");
}
}
}
- 스프링 컨테이너에 조회 요청할 때 마다 새로 생성
- 종료 메서드 호출 X
프로토타입 스코프 - 싱글톤 빈과 함께 사용시
싱글톤 빈과 함께 사용할 때 의도한 대로 잘 동작하지 않는 경우가 있다.
아래의 예제를 보자.
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
28
29
30
31
32
33
34
35
36
37
public class SingletonWithPrototypeTest1 {
@Test
void prototypeFind(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
prototypeBean1.addCount();
Assertions.assertThat(prototypeBean1.getCount()).isEqualTo(1);
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
prototypeBean2.addCount();
Assertions.assertThat(prototypeBean2.getCount()).isEqualTo(1);
}
@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.close");
}
}
}
당연히 계속 새로운 객체를 불러오는 것이기 때문에 Count는 누적되지 않고 1씩 증가한다.
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
@Test
void singletonClientUsePrototype(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int count1 = clientBean1.logic();
Assertions.assertThat(count1).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int count2 = clientBean2.logic();
Assertions.assertThat(count2).isEqualTo(2);
}
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이 주입이 된다.
싱글톤 빈은 생성시점에만 의존관계 주입을 받기 때문에, 프로토타입 빈이 새로 생성은 되지만 싱글톤 빈과 함께 계속 유지된다. 따라서 의도와 다르게 프로토타입 빈임에도 계속 같은 객체를 쓰게 된다.
따라서 count가 2가 된다.
스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 되는데 이걸 어떻게 해결해야 할까?
Provider로 문제 해결
ObjectFactory, ObjectProvider
ClientBean에 스프링컨테이너를 내부 변수로 사용하고 logic 함수 호출 시 ac.getBean() 메서드를 통해 PrototypeBean을 조회해서 사용하면 당연히 싱글톤인 ClientBean 내부 변수에 PrototypeBean이 있는 것이 아니기 때문에 객체를 계속 생성해 사용할 수 있다.
하지만 이런 경우 의존관계를 외부에서 주입받는 것이 아닌 직접 필요한 의존관계를 찾는 DL 상태가 된다.
이렇게 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면 스프링 컨테이너에 종속적인 코드가 되고 단위 테스트도 어려워진다.
1
2
3
4
5
6
7
8
9
10
11
static class ClientBean{
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic(){
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
이렇게 하면 생성할 때 prototypeBean을 의존관계 주입 받을 일도 없고, getObject() 함수 호출 시 컨테이너에 해당 빈을 조회하도록 하기 때문에 계속해서 새로운 객체를 생성하게 된다.
특징
- 스프링에 의존한다.
- 상속, 옵션, 스트림 처리 등 편의 기능이 많다.
JSR-330 Provider
ObjectProvider은 스프링에 의존한다.
자바 표준을 사용하는 방법이 있다.
1
2
3
4
//gradle.build
dependencies{
implementation 'jakarta.inject:jakarta.inject-api:2.0.1'
}
추가해 주어야 한다.
1
2
3
4
5
6
7
8
9
10
static class ClientBean{
@Autowired
private Provider<PrototypeBean> prototypeBeanProvider;
public int logic(){
PrototypeBean prototypeBean = prototypeBeanProvider.get();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
provider의 get() 기능을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아 반환한다.
스프링에 종속되어 있지 않으므로 다른 라이브러리에도 사용 가능하다.
그래서 프로토타입 빈 언제 사용?
매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요할 때 사용한다.
하지만 실무에서 웹 애플리케이션을 개발해보면, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.
웹 스코프
- 웹 스코프는 웹 환경에서만 동작한다.
- 웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다. 따라서 종료 메서드가 호출된다.
종류
- request : HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.
- session : HTTP Session과 동일한 생명주기를 가지는 스코프
- application : ServletContext와 동일한 생명주기를 가지는 스코프
- websocket : 웹 소켓과 동일한 생명주기를 가지는 스코프
1
2
//build.gradle 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다. 이럴때 사용하기 좋은 것이 request 스코프이다.
아래와 같이 로그가 남도록 request 스코프를 활용해보자.
- 기대하는 공통 포맷 : UUID requestURL message
- UUID를 사용해 HTTP 요청을 구분한다.
- requestURL 정보를 넣어 어떤 URL을 요청해서 남은 로그인지 확인한다.
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
28
//로그를 출력하기 위한 MyLogger 클래스
@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(); //java.util의 UUID이다.
System.out.println("[" + uuid + "] request scope bean create:" + this);
}
@PreDestroy
public void close(){
System.out.println("");
System.out.println("[" + uuid + "] request scope bean close:" + this);
}
}
- requestURL은 빈이 생성되는 시점에 알 수 없으므로 외부에서 setter로 받는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//LogDemoController
@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";
}
}
- HttpServletRequest를 통해 요청 URL을 받는다.
1
2
3
4
5
6
7
8
9
10
11
//LogDemoService
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
- 비즈니스 로직이 있는 서비스계층에서도 로그를 출력해본다.
- request scope를 사용하지 않고 파라미터로 모든 정보를 서비스 계층에 넘긴다면 파라미터가 많아 지저분해진다. 게다가 requestURL같은 웹과 관련된 정보가 웹과 관련없는 서비스계층까지 넘어가게 된다. 웹과 관련된 부분은 컨트롤러 까지만 사용해야 한다.
- request scope인 MyLogger 덕분에 이런 부분을 파라미터로 넘기지 않고, MyLogger의 멤버변수에 저장해 코드와 계층을 깔끔하게 유지할 수 있다.
이 상태로 실행하면 오류가 발생한다.
Request가 되어야 MyLogger빈이 생성되는데 이 상태로 서버를 실행하면 Request를 하지 않아 MyLogger를 조회할 수 없기 때문이다.
Provider로 해결
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//LogDemoController
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request){
MyLogger myLogger = myLoggerProvider.getObject();
String requestURL = request.getRequestURL().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
프록시로 해결
1
2
3
4
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
- 적용 대상이 인터페이스이면 INTERFACES
- 클래스면 TARGET_CLASS
MyLogger 클래스에 proxyMode 추가하고, 컨트롤러와 서비스 계층에서는 MyLogger를 Provider 없이 변수로 선언하고 그대로 사용하면 된다.
이렇게 하면 MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관없이 가짜 프록시 클래스를 MyLogger라는 이름으로 컨테이너에 등록한다. (원래는 Request 요청이 없기 때문에 request scope인 MyLogger는 빈 등록 자체가 안된다.)
ac.getBean()으로 조회해보면 가짜 객체가 조회된다.
MyLogger의 함수를 호출했을 때 가짜 프록시 객체가 내부에서 진짜 빈을 요청하는 위임 로직이 들어있어 그 때 정상 작동하게 되는 것이다.
주의점
마치 싱글톤을 사용하는 것 같지만 다르게 동작하기 때문에 잘 사용해야 한다.
특별한 스코프는 꼭 필요한 곳에서만 최소화해서 사용해야 한다. 유지보수가 힘들어진다.