Intro::
김영한님의 스프링의 프로토타입 스코프 강의 정리본 입니다.
빈 스코프란?
지금까지 우리는 스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료될 때 까지 유지된다고 학습했다. 이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. 스코프는 번역 그대로 빈이 존재할
수 있는 범위를 뜻한다.
스프링은 다음과 같은 다양한 스코프를 지원한다.
- 싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다. 기본으로 설정되어진다.
- 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.
- 웹 관련 스코프
- request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프이다.
- session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프이다.
- application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프이다.
@Scope("prototype")// 생략하면 singleton @Bean PrototypeBean HelloBean() { return new HelloBean(); }
스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다. 클라이언트에 빈을 반환하고, 이후 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않는다. 프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있다. 그래서
@PreDestroy
같은 종료 메서드가 호출되지 않는다.
프로토타입 빈의 특징 정리
- 스프링 컨테이너에 요청할 때 마다 새로 생성된다.
- 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입 그리고 초기화까지만 관여한다.
- 종료 메서드가 호출되지 않는다.
- 그래서 프로토타입 빈은 프로토타입 빈을 조회한 클라이언트가 관리해야 한다. 종료 메서드에 대한 호출도 클라이언트가 직접 해야한다.
프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점
스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환한다. 하지만 싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않으므로 주의해야 한다.
해결 방법
- 직접 ApplicationContext 로 호출 할때마다 새로운 빈
@Autowired private ApplicationContext ac; public int logic() { PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class); prototypeBean.addCount(); int count = prototypeBean.getCount(); return count; }
- ObjectProvider
ObjectProvider
의getObject()
를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)- 스프링에 의존
@Autowired private ObjectProvider<PrototypeBean> prototypeBeanProvider; public int logic() { PrototypeBean prototypeBean = prototypeBeanProvider.getObject(); prototypeBean.addCount(); int count = prototypeBean.getCount(); return count; }
- JSR-330 Provider
- JSR-330 자바 표준을 사용하는 방법이다.
jakarta.inject:jakarta.inject-api:2.0.1
라이브러리를 gradle에 추가해야 한다.
@Autowired private Provider<PrototypeBean> provider; public int logic() { PrototypeBean prototypeBean = provider.get(); prototypeBean.addCount(); int count = prototypeBean.getCount(); return count; }
request 스코프
동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다. 이럴때 사용하기 딱 좋은것이 바로 request 스코프이다.
@Getter @Setter @Component @Scope(value = "request") public class MyLogger { private String uuid; private String requestUrl; public void log(String msg) { System.out.println("[" + uuid + "] " + "[" + requestUrl + "]" + msg); } @PostConstruct public void init() { String uuid = UUID.randomUUID().toString(); System.out.println("[" + uuid + "] request scope bean created: " + this); } @PreDestroy public void close() { System.out.println("[" + uuid + "] request scope bean closed: " + this); } }
- 로그를 출력하기 위한
MyLogger
클래스이다.
@Scope(value = "request")
를 사용해서 request 스코프로 지정했다. 이 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸된다.
- 이 빈이 생성되는 시점에 자동으로
@PostConstruct
초기화 메서드를 사용해서 uuid를 생성해서 저장해둔다.
- 이 빈은 HTTP 요청 당 하나씩 생성되므로, uuid를 저장해두면 다른 HTTP 요청과 구분할 수 있다.
- 이 빈이 소멸되는 시점에
@PreDestroy
를 사용해서 종료 메시지를 남긴다.
requestURL
은 이 빈이 생성되는 시점에는 알 수 없으므로, 외부에서 setter로 입력 받는다.
해당 객체를
private final MyLogger myLogger;
로 DI한다면 오류가 발생한다.
이유는 MyLogger
객체는 HTTP 요청에 따라 생성되어지는 빈이기 때문에 스프링 부트를 띄울때 존재하지 않기 떄문이다.스코프와 Provider
첫번째 해결방안은 앞서 배운 Provider를 사용하는 것이다.
@Controller @RequiredArgsConstructor public class LogDemoController { private final LogDemoService logDemoService; private final ObjectProvider<MyLogger> myLoggerProvider; @RequestMapping("log-demo") @ResponseBody public String logDemo(HttpServletRequest request) { String requestURL = request.getRequestURL().toString(); MyLogger myLogger = myLoggerProvider.getObject(); myLogger.setRequestURL(requestURL); myLogger.log("controller test"); logDemoService.logic("testId"); return "OK"; } }
스코프와 프록시
@Component @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) public class MyLogger { }
private final MyLogger myLogger;//싱글톤 처럼 사용
CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.
myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d
가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
- 가짜 프록시 객체는 내부에 진짜
myLogger
를 찾는 방법을 알고 있다.
- 클라이언트가
myLogger.log()
을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이다.
- 가짜 프록시 객체는 request 스코프의 진짜
myLogger.log()
를 호출한다.
- 가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 원본인지 아닌지도 모르게, 동일하게 사용할 수 있다(다형성)
동작 정리
- CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.
- 이 가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다.
- 가짜 프록시 객체는 실제 request scope와는 관계가 없다. 그냥 가짜이고, 내부에 단순한 위임 로직만 있고, 싱글톤 처럼 동작한다.
특징 정리
- 프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 있다.
- 사실 Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다.
- 단지 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 이것이 바로 다형성과 DI 컨테이너가 가진 큰 강점이다.
- 꼭 웹 스코프가 아니어도 프록시는 사용할 수 있다.
주의점
- 마치 싱글톤을 사용하는 것 같지만 다르게 동작하기 때문에 결국 주의해서 사용해야 한다.
- 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자, 무분별하게 사용하면 유지보수하기 어려워진다.
Loading Comments...