이 게시글은 인프런의 유료 강의 스프링 핵심 원리 - 기본편을 수강하며 공부한 내용을 담고있습니다.
저작권에 의해 강의내용에 해당하는 전체 코드는 공개할 수 없습니다.
저작권에 의해 모든 내용을 담을 수 없습니다. 중복 및 반복되는 내용은 생략했습니다.
빈 스코프(Bean Scope)
여기서 Scope 가 의미하는 것은 C언어나, Java 기초에서 배우는 Scope 와 의미상 동일하다.
즉, 존재하는 범위를 의미한다.
우선 여태까지 배운 내용대로라면 Bean 은 스프링 컨테이너의 시작부터 종료까지 생존한다고 알고있다.
이는 사실 Singleton Scope 라서 그런 것이고, 다른 Scope 에 해당되면 그렇지 않다.
스코프 종류
- Singleton : 스프링 컨테이너 시작부터 종료까지 유지되는 가장 넓은 범위의 스코프
- Prototype : 스프링 컨테이너가 Bean의 생성과 의존관계 주입, 초기화 콜백까지만 관여하고 더 이상 관리하지 않는다. 즉, 소멸자 콜백도 호출하지 않기 때문에 직접 관리를 해줘야한다.
웹 관련 스코프
웹과 관련된 기능이 있어야 사용가능
- request : 웹 요청이 들어오고 나갈때까지 유지되는 스코프이다. 즉, 요청이 들어오면 빈이 생성되고 요청을 완료하면 소멸 전 콜백을 호출한다.
- session : 웹 세션이 유지되는 동안 유지되는 스코프.
- application : 웹 서블릿 컨텍스트와 같은 범위로 유지되는 스코프.
몇몇 스코프에 대한 개념만 배우면 request, session 과 같이 이름에서 유지되는 범위를 유추할 수 있다.
유지가 끝난다는 것은 생명주기가 끝난다는 의미다. 즉, 소멸 전 콜백을 호출한다.
1
2
3
4
5
6
7
8
9
10
11
12
// 컴포넌트 스캔방식에서 스코프를 지정하는 방법
@Scope("prototype")
@Component
public void myBean() {}
// 수동 빈 등록에서 스코프를 지정하는 방법
@Configuration
static class MyConfig {
@Scope("prototype")
@Bean
public void myBean() {}
}
prototype 스코프
Singleton Bean 은 스프링 컨테이너에서 Singleton 상태를 계속 유지할 수 있게 관리해준다.
그레 반해 Prototype 인 Bean 은 스프링 컨테이너가 Bean의 생성과 DI(의존관계 주입)까지만 관여하고 이후로는 관리하지 않는다.
이 말은 다음 두가지를 의미한다.
- Prototype Bean 에 대한 요청이 올 때 마다 새로 생성해서 돌려준다.
- 그리고 소멸 전 콜백도 호출하지 않는다.
스프링 컨테이너가 관리를 하지 않기 때문에 Bean 을 요청한 클라이언트가 관리를 해줘야한다. 필요하다면 직접 @PreDestroy와 같은 소멸 메서드를 호출해야한다.
아래는 예제 코드이다.
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
public class PrototypeTest {
@Test
void prototypeBeanTest() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
System.out.println("Prototype Bean 찾기");
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
System.out.println("Prototype Bean 한번 더 찾기");
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
System.out.println("Prototype Bean 1 : " + prototypeBean1);
System.out.println("Prototype Bean 2 : " + prototypeBean2);
Assertions.assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
// prototypeBean1.destroy_callback(); // 소멸 전 콜백 직접 호출
// prototypeBean2.destroy_callback();
ac.close();
}
@Scope("prototype")
@Component
static class PrototypeBean {
@PostConstruct
public void init_callback() {
System.out.println("초기화 콜백 호출됨");
}
@PreDestroy
public void destroy_callback() {
System.out.println("소멸 전 콜백 호출됨");
}
}
}
1
2
3
4
5
6
Prototype Bean 찾기
초기화 콜백 호출됨
Prototype Bean 한번 더 찾기
초기화 콜백 호출됨
Prototype Bean 1 : hello.core.scope.PrototypeTest$PrototypeBean@4e858e0a
Prototype Bean 2 : hello.core.scope.PrototypeTest$PrototypeBean@435fb7b5
출력결과에 오류가 발생하지 않았으므로 Assertions.assertThat(prototypeBean1).isNotSameAs(prototypeBean2); 는 True 인 것을 확인할 수 있다.
Bean 을 각각 확인했을 때 고유번호가 @4e858e0a, @435fb7b5 로 서로 다르다는 것을 확인할 수 있고,
초기화 콜백은 호출하지만 소멸 전 콜백은 호출되지 않은것을 확인할 수 있다.
prototype Bean 과 singleton Bean을 함께 사용할 시 문제점
스코프가 prototype 인 Bean 과 singleton 인 Bean 두개가 있다고 하자.
현재 singleton Bean 은 prototype Bean 을 하나 의존하고 있다.
그림과 코드로 확인하면 다음과 같다.
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class SingletonWithPrototypeTest1 {
@Test
void 싱글톤에서프로토타입호출() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class, PrototypeBean.class);
SingletonBean clientA = ac.getBean(SingletonBean.class);
int countA = clientA.logic();
SingletonBean clientB = ac.getBean(SingletonBean.class);
int countB = clientB.logic();
System.out.println("A의 count = " + countA);
System.out.println("B의 count = " + countB);
}
@Scope("singleton") // 생략가능
@Component
static class SingletonBean {
private final PrototypeBean prototypeBean;
@Autowired // 생략 가능
public SingletonBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic() {
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
@Scope("prototype")
@Component
static class PrototypeBean {
private int count = 0;
public void addCount() {
count++;
}
public int getCount() {
return count;
}
@PostConstruct
public void init() {
System.out.println("초기화 콜백 호출됨");
}
@PreDestroy
public void destroy() {
System.out.println("소멸 전 콜백 호출됨");
}
}
}
1
2
A의 count = 1
B의 count = 2
코드를 보면 singletonBean 이 생성될 때 prototypeBean 을 주입받는다. 이 때, prototypeBean 이 하나 생성된다.
이후 clientA 와 clientB 가 prototypeBean 에 있는 count 의 값을 증가시키지만 동일한 prototypeBean 의 count가 증가한다.
코드에 작성된 대로 잘 동작한다. 문제는 원래 의도는 이게 아니라는 것이다.
원래 의도는 singletonBean 에서 prototypeBean 이 필요할 때마다 새로 생성하는 것이다.
이를 구현하기 위해서는 Provider 를 사용해야한다.
Provider
ObjectFactory, ObjectProvider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Scope("singleton") // 생략가능
@Component
static class SingletonBean {
// private final PrototypeBean prototypeBean;
private ObjectProvider<PrototypeBean> objectPrototypeBeanProvider;
@Autowired // 생략 가능
public SingletonBean(ObjectProvider<PrototypeBean> objectPrototypeBeanProvider) {
this.objectPrototypeBeanProvider = objectPrototypeBeanProvider;
}
public int logic() {
PrototypeBean prototypeBean = objectPrototypeBeanProvider.getObject();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
1
2
A의 count = 1
B의 count = 1
기존 코드에서 private final PrototypeBean prototypeBean 을 지우고 대신 ObjectProvider 를 넣었다.
생성자 주입처럼 코드를 작성했지만, 출력결과를 보면 예상할 수 있듯이 PrototypeBean 은 생성자 주입이 되지 않고 필요할 때마다 생성된 것을 알 수 있다.
기존에 배운 것은 DI(의존관계 주입)이고 지금처럼 필요할 때마다 일일히 찾는 방식을 DL(Dependency Lookup)이라고 한다.
ObjectFactory 는 getObject() 메서드 하나만 존재한다.
ObjectProvider 는 ObjectFactory 를 상속받아 더 많은 기능을 추가한 형태이다.
ObjectFactory 와 ObjectProvider 는 스프링에서만 사용할 수 있다.(스프링에 의존적이다.)
JSR-330 Provider
Java 표준(JSR-330)은 Provider 를 지원한다.
Provider 는 javax.inject.Provider 라이브러리에 존재한다.
Provider 는 ObjectFactory 와 동일하게 get() 메서드 하나만 존재하고 동일한 기능을 수행한다.
따라서 위 코드에서 ObjectProvider 를 지우고 Provider 를 넣으면 완벽하게 호환된다.
JSR-330 Provider 는 Java 표준이기 때문에 Spring 이 아닌 다른 컨테이너에서도 호환이 된다.
웹 스코프
종류
- request : HTTP request 가 들어오고 나갈 때 까지 유지되는 스코프. 각각의 request 별로 Bean 인스턴스가 생성된다.
- session : HTTP session 과 동일한 생명주기를 가지는 스코프
- application : Servlet Context 와 동일한 생명주기를 가지는 스코프
- websocket : web socket 과 동일한 생명주기를 가지는 스코프
어떤걸 기준으로 삼느냐에 따라 생명주기가 달라질 뿐 동작하는 방식은 비슷하다.
예제 코드
우선 build.gradle 파일의 dependencies 에 다음 코드를 추가하고 Sync Gradle Changes 버튼을 눌러 적용한다.
1
implementation 'org.springframework.boot:spring-boot-starter-web'
이제 프로그램 실행 시 웹 서버도 같이 구동된다.
참고로 이전까지는 AnnotationConfigApplicationContext 를 사용했지만 이제 AnnotationConfigServletWebServerApplicationContext 를 사용하게된다.
웹 구동에 필요한 설정과 환경이 저기에 포함되어 있기 때문이다.
request
요청 별로 특정 동작(ex. 로그 남기기)를 하고 싶으면 request 스코프를 사용하면 된다.
예를 들어 동시에 여러 요청이 들어오면 로그가 뒤죽박죽 섞여서 알아보기 힘들게 된다.
이때 로그에 요청별로 ID 를 남기면 알아보기 쉬워진다.
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
package hello.core.common;
@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 생성 : " + this);
}
@PreDestroy
public void destroy() {
System.out.println("[" + uuid + "]" + " request scope bean 삭제 : " + this);
}
}
request 스코프인 Bean 을 만들고 log 메서드를 통해 콘솔창에 값을 출력한다.
@PostConstruct 를 통해 Bean이 초기화된 후 UUID 값을 하나 할당받는다.
requestURL 은 초기화 단계에서 스스로 알 수 없으므로 외부에서 setter 로 주입받는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package hello.core.web;
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private LogDemoService logDemoService;
private MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("log-demo 에서 로그를 남김");
logDemoService.logic("testId");
return "OK";
}
}
MyLogger 를 테스트하기 위한 컨트롤러도 작성한다.
log-demo 로 요청이 오면 URL 을 myLogger Bean에 주입 후 콘솔에 출력한 뒤 OK 를 응답하는 컨트롤러를 만들었다.
매개변수 request 는 Spring 이 알아서 만들고 브라우저가 보낸 정보를 주입해준다.
별개로
String requestURL = request.getRequestURL().toString();
myLogger.setRequestURL(requestURL);
이 코드는 스프링 인터셉터나 서블릿 필터에서 처리하는게 좋다고 한다. 하지만 아직 배우지 않았으므로 컨테이너에서 사용한 것이다.
1
2
3
4
5
6
7
8
9
10
11
package hello.core.web;
@Service
@RequiredArgsConstructor
public class LogDemoService {
private MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id : " + id);
}
}
request scope Bean인 MyLogger 를 만들지 않고 서비스 계층에서 바로 print문을 써서 [uuid][requestURL]message 를 출력할 수도 있다.
하지만 이러한 로직은 서비스와 전혀 상관 없기 때문에 계층에 맞지 않을 뿐더러 코드가 많아져서 유지보수하기 힘들다.
그리고 Controller 의 myLogger 와 Service 의 myLogger 는 Spring 에서 알아서 동일한 Bean 을 주입시켜 준다.
아무튼 이렇게 작성하고 프로젝트를 작동하면 오류가 난다.
오류가 발생하는 이유
Controller 를 보고 잘 생각해보자.
LogDemoController 가 생성될 때 생성자 주입을 통해 MyLogger 를 주입받는다.
하지만 MyLogger 는 클라이언트로부터 request 가 와야 생성되는 Bean 이므로 주입받을 수 없다.
그래서 오류가 발생한다.
이를 해결하기 위해서는 앞서 공부했던 Provider 를 이용하면된다.
Scope, Provider
Controller 와 Service 에 작성한 코드를 아래와 같이 수정한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 수정 전
private MyLogger myLogger;
// 수정 후(Controller 기준)
private ObjectProvider<MyLogger> myLoggerProvider;
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerProvider.getObject(); // 추가
myLogger.setRequestURL(requestURL);
myLogger.log("log-demo 에서 로그를 남김");
logDemoService.logic("testId");
return "OK";
}
그러면 생성자 주입 타이밍에 MyLogger 가 아니라 ObjectProvider 가 주입되므로 정상적으로 작동한다.
또, Controller에서 myLoggerProvider.getObject() 하는 순간에는 무조건 MyLogger Bean 이 존재할 수 밖에 없으니 논리적으로도 문제가 없다.
Scope, Proxy
Provider 를 사용하면 편리하기는 하지만 코드가 길어지고 Provider 에서 .get() 메서드로 Bean 을 꺼내야한다는 사소한 문제점이 있다.
이를 간편하게 해결하는 방법이 Proxy 이다.
이름이 Proxy 인 이유는 Proxy 개념을 그대로 사용하기 때문이다.
1
2
3
4
5
6
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
// MyLogger 가 interface 이면 TARGET_CLASS 대신에 INTERFACES 를 사용하면 된다.
public class MyLogger {
// 생략
}
1
2
3
4
5
6
7
8
9
10
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id : " + id);
System.out.println("myLogger : " + myLogger.getClass());
}
}
@Scope 에 proxyMode = ScopedProxyMode.TARGET_CLASS 한줄 추가해주면 생성자 주입 시점에 MyLogger 가 없다고 발생하는 오류가 더이상 발생하지 않는다.
왜냐하면 생성자 주입 시점에 MyLogger 가 아닌 가짜 MyLogger(이하 Proxy Bean) 를 생성해서 주입해주기 때문이다.
클라이언트의 요청을 받은 Proxy Bean 은 컨테이너에서 실제 MyLogger 를 찾아서 요청을 수행한다.
1
myLogger : class hello.core.common.MyLogger$$SpringCGLIB$$0
myLogger.getClass() 출력 결과를 확인해보면 원본 MyLogger 객체가 아니라 CGLIB 으로 만들어진 새로운 객체임을 알 수 있다. 즉 Proxy Bean 이 주입된 것을 알 수 있다.
myLogger.log() 는 Proxy Bean 의 log 메서드를 호출하게 되고 Proxy Bean 은 실제 MyLogger 객체의 log() 를 수행하게 된다.
Provider 를 사용하지 않고 마치 싱글톤 처럼 사용할 수 있다.
대신 코드가 싱글톤처럼 보이는 탓에 @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) 를 모르는 사람이 테스트 코드를 작성하거나 유지보수를 하면 MyLogger 를 싱글톤으로 생각해 오류가 발생할 수 있다.
그리고 Proxy 는 request 와 같은 scope 여야지만 사용할 수 있는게 아니다. 그냥 Proxy 라고 생각하고 필요한 곳에 사용하면 된다.
이전 포스팅 : 빈 생명주기 콜백
목차의 마지막 포스팅입니다.
