✅ ObjectProvider ✅ Provider
빈 스코프는 번역 그대로 빈이 존재할 수 있는 범위를 뜻한다.
싱글톤 스코프
프로토타입 스코프
웹 스코프
@Scope("prototype")
@Component
public class HelloBean {}
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}
스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환
public class SingletonTest {
@Test
public 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);
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");
}
}
}
SingletonBean.init // 빈 초기화 메서드를 실행
singletonBean1 = hello.core.scope.PrototypeTest$SingletonBean@54504ecd // 같은 인스턴스의 빈을 조회
singletonBean2 = hello.core.scope.PrototypeTest$SingletonBean@54504ecd // 같은 인스턴스의 빈을 조회
org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing SingletonBean.destroy // 종료 메서드까지 정
스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다.
싱글톤의 경우 객체를 1개만 생성한다는 장점은 있으나
Web 같이 여러명이 같은 객체를 사용하는 과정에서 문제가 발생할 소지가 있다.
예를 들어 클래스의 멤버변수를 만들고 그것을 메소드에 넣은 파라미터가 해당 멤버변수의 값을 변경할 수 있는 구조로 설계가 되어 있을 경우
여러 사람이 같은 객체를 사용하기 때문에 내가 변경한 값을 다른 사람이 또 변경시킬수 있는 문제가 있는 것이다.
그래서 Bean Scope를 Prototype으로 바꿔서 Spring Bean 객체를 만들때 여러사람이 공유하는 변수가 없게끔 설계하고
외부에서 입력받은 파라미터값이 내부 멤버변수의 값을 변경하지 못하게끔 해주는게 좋다.
그러면 요청할때마다 Bean 객체가 생성되기 때문에 충돌현상이 발생하지를 않는다.
매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용하면 된다.
그런데 실무에서 웹 애플리케이션을 개발해보면, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에
프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.
public class PrototypeTest {
@Test
public 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("prototypeBean2 = " + prototypeBean2);
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");
}
}
}
find prototypeBean1
PrototypeBean.init // 싱글톤 빈은 스프링 컨테이너 생성 시점에 초기화 메서드가 실행 되지만, 프로토타입 스코프의 빈은 스프링 컨테이너에서 빈을 조회할 때 생성
find prototypeBean2
PrototypeBean.init
prototypeBean1 = hello.core.scope.PrototypeTest$PrototypeBean@13d4992d // 프로토타입 빈을 2번 조회했으므로 완전히 다른 스프링 빈이 생성 (초기화도 2번 실행)
prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@302f7971
org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing
스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 된다.
그런데 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지된다.
서로 다른 스코프 처리 방식이 충돌을 원하던 로직대로 처리가 안되는 문제가 발생한다.
가장 간단한 방법은 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청하는 것이다.
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 ObjectProvider 이다.
Dependency Lookup (DL)
의존관계 조회(탐색) : 의존관계를 외부에서 주입(DI) 받는게 아니라 이렇게 직접 필요한 의존관계를 찾는 것@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
👉 하지만 스프링에 의존하기 때문에 다른 컨테이너에서도 사용할 수 없다.
자바 표준을 사용하여 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공
//implementation 'javax.inject:javax.inject:1' gradle 추가 필수
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
웹 환경에서만 동작한다.
프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다.
request
: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다.session
: HTTP Session과 동일한 생명주기를 가지는 스코프application
: 서블릿 컨텍스트( ServletContext )와 동일한 생명주기를 가지는 스코프websocket
: 웹 소켓과 동일한 생명주기를 가지는 스코프//build.gradle에 web 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어려운데 이럴때 사용하기 딱 좋음
requestURL을 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();
System.out.println("[" + uuid + "] request scope bean create:" + this);
}
@PreDestroy
public void close() {
System.out.println("[" + uuid + "] request scope bean close:" + this);
}
}
👉 스프링 애플리케이션 실행 시점에는 http 요청이 없기 때문에 request 스코프 빈은 아직 생성되지 않아 오류가 발생하므로 Provider를 사용해야 한다.
즉, ObjectProvider을 사용하여 ObjectProvider.getObject() 를 호출하는 시점까지 request scope 빈의 생성을 지연해야 한다.
@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";
}
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " + id);
}
}
👉 프록시 방식을 사용하면 코드를 더 간소화 할 수 있다. (강의 자료 참고)