[Spring] 기본편 09. 빈 스코프
이 글은 스프링 [핵심원리 - 기본편]을 듣고 정리한 내용입니다
📌 빈 스코프란?
스프링이 지원하는 다양한 스코프
- 싱글톤: 기본 스코프, 스플이 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프.
- 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프
- 웹 관련 스코프
- request: 웹 요청이 들어오고 나갈 때 까지 유지되는 스코프
- session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
- application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
- 컴포넌트 스캔 자동 등록
@Scope("prototype")
@Component
public class HelloBean {}
- 수동 등록
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}
📌 프로토타입 스코프
- 싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다.
- 반면, 프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다.
싱글톤 빈 요청
1. 싱글톤 스코프의 빈을 스프링 컨테이너에 요청한다.
2. 스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환한다.
3. 이후에 스프링 컨테이너에 같은 요청이 오면, 같은 객체 인스턴스의 스프링 빈을 반환한다.
프로토타입 빈 요청
1. 프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.
2. 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.
3. 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환한다.
4. 이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환한다.
정리
- 스프링컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다.
- 이후 프로토타입 빈을 관리할 책임은 빈을 받은 클라이언트에게 있다.
- 싱글톤 스코프 빈 테스트
package hello.core.scope;
public class SingletonTest {
@Test
void singletonBeanfind(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
System.out.println("singletonBean1 = " + singletonBean1);
System.out.println("singletonBean2 = " + singletonBean2);
//결과: 같음
Assertions.assertThat(singletonBean1).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");
}
}
}
- 싱글톤 스코프 결과 출력
- 빈 초기화 메서드 실행
- 같은 인스턴스의 빈을 조회
- 종료 메서드 정상 호출 확인.
- 프로토타입 스코프 빈 테스트
package hello.core.scope;
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 prototypeBean1");
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
System.out.println("prototypeBean1 = " + prototypeBean1);
System.out.println("prototypeBean2 = " + prototypeBean2);
//bean1과 bean2는 다름
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번 실행되었다. -> 서로다른 참조값이 나온다.
프로토타입 빈의 특징 정리
- 스프링 컨테이너에 요청할때마다 새로 생성됨.
- 스프링 컨테이너는 프로토타입의 빈의 생성과 의존관계 주입, 초기화 까지만 관여함
- 종료 메서드가 호출되지 않음.
- 프로토타입 빈은 프로토타입 빈을 조회한 클라이언트가 관리해야 한다. 종료 메서드에 대한 호출도 클라이언트가 직접 해야함.
📌 프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점
-
스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환한다.
-
하지만 싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않으므로 주의해야한다.
-> 여기서 잘 동작하지 않는다는건, 프로토타입 빈을 사용할때마다 새로 생성해서 사용하는것을 기대하지만 싱글톤 빈과 함께 사용하면 프로토타입 빈을 주입 시에만 새로 생성한다는 뜻이다. -
스프링 컨테이너에 프로토타입 빈 직접 요청
public class SingletonWithPrototypeTest1 {
@Test
void prototypeFind() {
AnnotationConfigApplicationContext ac = new
AnnotationConfigApplicationContext(PrototypeBean.class);
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
prototypeBean1.addCount();
assertThat(prototypeBean1.getCount()).isEqualTo(1);
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
prototypeBean2.addCount();
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.destroy");
}
} }
- 각 프로토타입 빈의 count는 둘다 1로 검증이 성공한다.
- 싱글톤에서 프로토타입 빈 사용
package hello.core.scope;
public class SingletonWithPrototypeTest1 {
@Test
void prototypeFind(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
prototypeBean1.addCount();
assertThat(prototypeBean1.getCount()).isEqualTo(1);
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
prototypeBean2.addCount();
assertThat(prototypeBean2.getCount()).isEqualTo(1);
}
@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);
}
@Scope("singleton")
static class ClientBean{
private final PrototypeBean prototypeBean; //생성시점에 주입
@Autowired
public ClientBean(PrototypeBean prototypeBean){
this.prototypeBean=prototypeBean;
}
public int logic(){
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
@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.destroy");
}
}
}
- 두번째 프로토타입 빈의 count=2임을 볼 수 있다.
- 스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 된다.
- 그런데 싱글톤 빈은 생성 시점에만 의존관계를 주입받기 때문에, 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지되는 것이 문제이다.
- 원하는 것은, 프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라, 사용할 때마다 새로 생성해서 사용하는 것이다.
*참고
- 여러 빈에서 같은 프로토타입 빈을 주입 받으면, 주입 받는 시점에 각각 새로운 프로토타입 빈이 생성된다.
- 예를 들어, clientA, clinetB가 각각 의존관계 주입을 받으면 각각 다른 인스턴스의 프로토타입을 주입 받는다.
- clientA -> prototypeBean@x01
- clientB -> prototypeBean@x02
- 물론 사용할때마다 새로 생성되는 것은 아니다.
📌 프로토타입 스코프 - 싱글톤 빈과 함께 사용 시 Provider로 문제 해결
- 싱글톤 빈과 프로토타입 빈을 함께 사용할 때, 사용할 때마다 항상 새로운 프로토타입 빈을 생성하려면 ?
스프링 컨테이너에 요청
- 가장 간단한 방법은 싱글톤 빈이 프로토타입을 사용할 때마다 스프링 컨테이너에 새로 요청하는 것이다.
@Autowired
private ApplicationContext ac;
public int logic() {
//그러나 굉장히 무식한 방법임.
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
ac.getBean()
을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 볼 수 있다.- 의존관계를 외부에서 주입(DI) 받는게 아니라 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL) 의존관계 조회(탐색)이라 한다.
🌱 ObjectFactory, ObjectProvider
ObjectProvider
: 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공한다.- 과거에는
ObjectFactory
가 있었는데, 여기에 편의 기능을 추가해서ObjectProvider
가 만들어졌다.
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
prototypeBeanProvider.getObject()
을 통해서 항상 새로운 프로토타입 빈이 생성된다.ObjectProvider
의getObject()
를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)
특징
ObjectFactory
: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존ObjectProvider
:ObjectFactory
상속, 옵션, 스트림 처리 등의 편의기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존
🌱 JSR-330 Provider
javax.inject.Provider
라는 JSR-330 자바 표준을 사용하는 방법javax.inject:javax.inject:1
라이브러리를 gradle에 추가 해야한다.
@Autowired
private Provider<PrototypeBean> prototypeBeanProvider;
public int logic(){
PrototypeBean prototypeBean = prototypeBeanProvider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
provider.get()
을 통해서 항상 새로운 프로토타입 빈이 생성되는것을 확인할수 있다.provider의 get()
을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다(DL)- 별도의 라이브러리가 필요하고(gradle에 추가), 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
정리
- 프로토타입 빈을 언제 사용할까?
- 매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용하면 되는데, 실무에서 사용하는 일은 매우 드물다!
ObjectProvider
,JSR330 Provider
등은 프로토타입 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있다.
스프링이 제공하는 기능을 사용하자
- 스프링을 사용하다 보면 이 기능 뿐만 아니라 다른 기능들도 자바 표준과 스프링이 제공하는 기능이 겹칠때가 많다.
- 대부분 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에, 특별히 다른 컨테이너를 사용할 일이 없다면, 스프링이 제공하는 기능을 사용하면 된다.
📌 웹 스코프
웹 스코프 특징
- 웹 환경에서만 동작한다
- 웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점 까지 관리한다. -> 종료 메서드가 호출된다.
웹 스코프 종류
- request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프로, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.
- session: HTTP Session과 동일한 생명주기를 가지는 스코프
- application: 서블릿 컨텍스트(ServletContext)와 동일한 생명주기를 가지는 스코프
- websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프
📌 request 스코프 예제 만들기
🌱 환경 세팅
- 웹 스코프는 웹 환경에서만 동작하므로 web 환경이 동작하도록 라이브러리를 추가한다.
- build.gradle에 추가
//web 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
- CoreApplication을 실행하면 웹 어플리케이션이 잘 실행된다.
omcat started on port(s): 8080 (http) with context path ''
Started CoreApplication in 0.914 seconds (JVM running for 1.528)
*참고
- 스프링 부트는 웹 라이브러리가 없으면
AnnotationConfigApplicationContext
을 기반으로 애플리케이션을 구동한다.- 웹 라이브러리가 추가되면 웹가 관련된 추가 설정과 환경들이 필요하므로
AnnotationConfigServletWebServerApplicationContext
를 기반으로 애플리케이션을 구동한다.
🌱 request 스코프 예제 개발
- 동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다
- 이럴 때 request스코프를 사용한다.
- 다음과 같이 로그가 남도록 request 스코프를 활용하여 추가기능 개발
[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
- 공통 포멧: [UUID][requestURL]{message}
- MyLogger.class
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 create: "+this);
}
@PreDestroy
public void close(){
System.out.println("");
System.out.println("["+uuid+"]"+" request scope bean close: "+this);
}
}
- 로그를 출력하기 위한 클래스.
@Scope(value="reuqest)
를 사용하여 request 스코프로 지정.- 이제 이 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸된다.
@PostConstruct
,@PreDestory
메서드 추가해서 초기화 시 uuid부여하고, 초기화 및 종료 메세지를 남긴다.
- LogDemoController.class
package hello.core.web;
import javax.servlet.http.HttpServletRequest;
// 로거가 잘 작동하는지 확인하는 테스트용 컨트롤러
@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을 받는다.requesetURL
값:http://localhost:8080/log-demo
myLogger
는 HTTP 요청 당 각각 구분되므로 다른 HTTP 요청 때문에 값이 섞이는 걱정은 안해도 된다.
- LogDemoService.class
package hello.core.web;
import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = "+ id);
}
}
- *중요한 점
request scope
를 사용하지않고, 파라미터로 이 모든 정보를 서비스 계층에 넘기게 되면 파라미터가 많아서 지저분해진다.- 더 문제는
requestURL
같은 웹과 관련된 정보가 웹가 관련없는 서비스 계층까지 넘어가게 된다. - 웹과 관련된 부분은 컨트롤러 까지만 사용해야 한다. 서비스 계층은 웹 기술에 종속되지 않고, 가급적 순수하게 유지하는 것이 유지보수 관점에서 좋다.
실행 시 오류
Error creating bean with name 'myLogger': Scope 'request' is not active for the
current thread; consider defining a scoped proxy for this bean if you intend to
refer to it from a singleton;
- 스프링 애플리케이션을 실행하면 오류가 발생한다.
- 스프링 애플리케이션을 실행하는 시점에, 싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 아직 생성되지 않는다.
request
스코프 빈은 실제 고격의 요청이 와야 생성할 수 있다!!
📌 스코프와 Provider
- 첫번째 해결방안: Provider 사용하기
package hello.core.web;
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider; //ObjectProvider 사용
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) throws InterruptedException {
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
Thread.sleep(1000);
logDemoService.logic("testId");
return "OK";
}
}
package hello.core.web;
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;//ObjectProvider 사용
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = "+ id);
}
}
- 실행 확인 (localhost:8080/log-demo 접속)
- 새로고침 여러번 했을 때
ObjectProvider
덕분에ObjectProvider.getObject()
를 호출하는 시점까지 request scope빈의 생성을 지연할 수 있다.ObjectProvider.getObject()
를 호출하는 시점에서는 HTTP 요청이 진행중이므로 request scope빈의 생성이 정상 처리된다.ObjectProvider.getObject()
를LogDemoController
,LogDemoService
에서 각각 한번씩 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환된다!
📌 스코프와 프록시
프록시란?
- 프록시(Proxy)란 '대신'이라는 의미를 가지고 있다.
- 프로토콜에 있어서 대리 응답 등에서 사용하는 개념이다.
- 보안상의 문제로 직접 통신을 주고 받을 수 없는 사이에서 프록시를 이용해서 중계를 하는 개념이다.
- 이렇게 중계 기능을 하는것을 프록시 서버 라고 부른다.
프록시 서버의 특징
- 프록시 서버는 클라이언트와 서버의 입장에서 볼 때 서로 반대의 역할을 하는것 처럼 보여지게 된다.
- 클라이언트가 프록시를 바라보면 '서버'처럼 동작하게 되는 거고, 서버가 프록시를 바라보면 '클라이언트'처럼 작동을 하는것과 같다.
- 프록시는 프록시 서버에 요청이 된 내용들을 '캐시'를 이용해 저장해 둔다. 이렇게 캐시로 저장을 해 두면 다시 데이터를 가져올 상황이 발생하지 않으므로 전송시간을 절약할 수 있다.
- 프록시 방식 사용
@Component
@Scope(value="request", proxyMode= ScopedProxyMode.TARGET_CLASS)
//proxy 설정, MyLogger가 class이므로 TARGET_CLASS로 설정.
//적용 대상이 인터페이스면 INTERFACE 선택
public class MyLogger {
}
- 이렇게하면 MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다.
- Controller, Service 이전 코드로 변경
package hello.core.web;
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) throws InterruptedException {
String requestURL = request.getRequestURL().toString();
System.out.println("myLogger.getClass() = " + myLogger.getClass());
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
Thread.sleep(1000);
logDemoService.logic("testId");
return "OK";
}
}
package hello.core.web;
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = "+ id);
}
}
- myLogger.getClass 출력
- 일반
MyLogger
클래스가 아니라CGLIB
가 적힌 결과가 출력된다.
동작 정리
CGLIB
라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.- 이 가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다.
- 가짜 프록시 객체는 실제 requeset scope와는 관계가 없다. 그냥 가짜이고, 내부에 단순한 위임 로직만 있고 싱글톤 처럼 동작한다.
특징 정리
- 프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 있다.
Provider
를 사용하던,프록시
를 사용하던 중요한 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 것이다.- 단지 어노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체 할 수 있다. 이것이 바로 다형성과 DI 컨테이너가 가진 큰 강점이다.
- 꼭 웹스코프가 아니어도 프록시는 사용 가능하다.
주의점
- 싱글톤을 사용하는 것 같지만, 동작은 다르므로 주의해서 사용하자
- 이런 특별한 scope는 꼭 필요할때만 사용해야 유지보수에 무리가 안간다.
Author And Source
이 문제에 관하여([Spring] 기본편 09. 빈 스코프), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@somyeong0623/Spring-기본편-07.-빈-스코프저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)