김영한 님의 스프링 핵심 원리 - 기본편 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard
스프링 빈은 기본적으로 싱글톤 스코프로 생성
스프링 컨테이너의 시작과 종료까지 유지되는 스코프
스프링 컨테이너가 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 스코프
의존관계를 주입하고 초기화 메소드까지 호출해준다
그 후 클라이언트에게 반환하면 스프링 컨테이너에서 관리하지 않기 때문에 프로토타입 빈을 관리할 책임은 클라이언트에게 있다
그래서 @PreDestroy
와 같은 종료 메서드가 호출되지 않는다
지정방법 : @Scope("prototype")
스프링 웹과 관련된 기술이 들어가야 사용할 수 있는 스코프
request : 웹 요청이 들어오고 나갈때 까지 유지되는 스코프
session : 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
application : 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
싱글톤 빈 요청
프로토타입 빈 요청
프로토타입 스코프의 빈을 스프링 컨테이너에 요청하면 스프링 컨테이너는 요청이 들어온 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입하고 클라이언트에게 반환한다
하지만 프로토타입 빈은 클라이언트에게 반환된 이후 스프링 컨테이너가 관리하지 않기 때문에 요청을 할 때마다 새로운 프로토타입 빈이 생성되어 반환된다
스프링 컨테이너 생성 이후 ( 빈 조회 전 )에 초기화 메서드(init)가 1번만 실행
빈을 조회하면 같은 객체(인스턴스)임을 확인
스프링 컨테이너가 종료(Closing)까지 유지되기 때문에 종료 메서드(destroy)가 실행
스프링 컨테이너에서 빈을 조회할 때 생성되고, 초기화 메소드도 실행된다
빈을 2번 조회했으므로 2개의 서로 다른 스프링 빈이 생성되고, 초기화도 2번 실행
스프링 컨테이너가 생성과 의존관계 주입 그리고 초기화 까지만 관여하기 때문에 스프링 컨테이너가 종료될 때(Closing) 종료 메서드가 실행되지 않음
prototypeBean1.destroy()
처럼 직접 호출해주어야 함
clientBean이라는 싱글톤 빈이 의존관계 주입을 통해 프로토타입 빈을 주입받아서 사용하는 예시
clientBean은 싱글톤이므로, 스프링 컨테이너 생성 시점에 함께 생성되고, 의존관계 주입이 발생한다
프로토타입 빈이 의존관계이기 때문에 스프링 컨테이너에 요청하고 생성된 프로토타입 빈을 받는다
clientBean은 프로토타입 빈을 내부 필드에 보관 ( 프로토타입 빈을 관리할 책임은 클라이언트인 clientBean에게 있음 )
클라이언트 A가 스프링 컨테이너에 clientBean을 요청 ( 싱글톤이므로 항상 같은 clientBean이 반환 )
클라이언트 A가 clientBean.logic() 호출하면 clientBean은 prototypeBean의 addCount()를 호출해서 프로토타입 빈의 count를 증가 ( 0 -> 1 )
마찬가지로 클라이언트 B가 스프링 컨테이너에 clientBean을 요청 ( 싱글톤이므로 항상 같은 clientBean이 반환 )
clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다
의존관계 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지, 프로토타입 빈을 사용 할 때마다 새로 생성되는 것이 아니다
그러므로 클라이언트 B가 clientBean.logic() 호출하면 clientBean은 이미 주입받은 prototypeBean의 addCount() 를 호출해서 프로토타입 빈의 count를 증가 ( 1 -> 2 )
스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환한다
싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 싱글톤 빈과 프로토타입 빈이 같이 사용되면 프로토타입 빈이 새로 생성되지만 싱글톤 빈과 함께 계속 유지된다
그렇기 때문에 클라이언트 B가 동일한 작업을 했을 때, 이미 존재하던 프로토타입 빈이 사용된 것이다
주입 시점에만 프로토타입 빈이 생성되는 것이 아니라 사용할 때마다 프로토타입 빈이 생성되게 하려면?? ( 아래에서 설명하는 내용 )
static class ClientBean {
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
...
}
}
clientBean이 @Autowired
로 ApplicationContext를 받는다
ac.getBean()
을 통해 항상 새로운 프로토타입 빈이 생성 ( 프로토타입 빈은 조회할 때 생성되기 때문 )
의존관계를 외부에서 주입(DI) 받는게 아니라 이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup (DL) 의존관계 조회(탐색) 이라한다
but> ApplicationContext 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다
ApplicationContext 전체를 주입받는 것이 아니라 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 DL 정도의 기능만 제공하는 무언가가 있으면 된다
ObjectProvider<T>
사용@Scope("singleton")
static class ClientBean {
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
ObjectProvider<T>
스프링에 의존적임
우리가 직접 찾는 것이 않도록 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공
필요할 때마다 스프링 컨테이너에 요청하는 기능을 사용할 수 있게 된다
예전에 사용하던 ObjectFactory<T>
를 상속함
ObjectFactory<T>
는 getObject()
하나만을 제공getObject()
를 호출하면 스프링 컨테이너에서 프로토타입 빈을 찾아서 반환한다 (DL)
위의 결과 사진을 보면 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다
javax.inject.Provider
라는 JSR-330 자바 표준을 사용@Scope("singleton")
static class ClientBean {
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
...
}
}
이 방법을 사용하려면 javax.inject:javax.inject:1
라이브러리를 gradle에 추가해야 한다
get()
이 위의 getObject()
와 동일한 기능을 제공한다 ( DL )
자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있음
웹 스코프는 웹 환경에서만 동작한다
웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리하기 때문에 종료 메서드가 호출된다
request
HTTP 요청이 들어오고 나갈 때 까지 유지되는 스코프
각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리된다
session : HTTP Session과 동일한 생명주기를 가지는 스코프
application : 서블릿 컨텍스트( ServletContext
)와 동일한 생명주기를 가지는 스코프
websocket : 웹 소켓과 동일한 생명주기를 가지는 스코프
웹 스코프는 웹 환경에서만 동작하기 때문에 build.gradle에 라이브러리를 추가해서 사용해야 한다
implementation 'org.springframework.boot:spring-boot-starter-web'
위의 라이브러리를 추가하면 스프링 부트는 내장 톰켓 서버를 활용해서 웹 서버와 스프링을 함께 실행하기 때문에 main 메서드를 실행하면 웹 애플리케이션이 실행된다
스프링 부트는 웹 라이브러리가 없으면 AnnotationConfigApplicationContext
을 기반으로 애플리케이션을 구동한다
웹 라이브러리가 추가되면 웹과 관련된 추가 설정과 환경들이 필요하므로
AnnotationConfigServletWebServerApplicationContext
를 기반으로 애플리케이션을 구동한다
동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어려운데 이럴 때 사용하기 좋은것이 request 스코프
클라이언트 A, B가 스프링 컨테이너에 동시에 요청한 경우
Controller 로직에서 request scope와 관련된 것을 호출하면 각각의 request 요청에 맞춰 해당 클라이언트 전용 객체( 스프링 빈 )가 생성돼서 A와 B에 다른 인스턴스가 할당된다
특정 클라이언트가 Service를 호출하는 경우에도 특정 클라이언트에 맞는 객체를 바라보게 된다
HTTP request에 대한 response가 나갈 때 객체 파괴
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
// 이 부분이 오류
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
...
}
}
위처럼 Controller와 Service를 작성하고 서버를 실행시키면 오류가 발생
@RequiredArgsConstructor
가 있기 때문에 스프링 컨테이너가 생성될 때 MyLogger
객체 의존관계 주입이 자동으로 이루어진다 ( @Autowired
와 동일 )
의존관계 주입을 위해 스프링 컨테이너에 MyLogger
객체를 달라고 요청
MyLogger는
request scope이고 request scope 는 고객의 요청이 들어와야 생성된다
but> 서버를 실행하는 시점에는 HTTP request가 없기 때문에 request scope이 생성되지 않았고 생성되지 않은 것을 주입하려고 해서 오류가 발생함
BeanCreationException
발생@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerObjectProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
MyLogger myLogger = myLoggerObjectProvider.getObject();
...
}
}
Service도 동일하게 코드 변경
ObjectProvider
덕분에 getObject()
를 호출하는 시점까지 request scope 빈을 스프링 컨테이너에 요청하는 것을 지연할 수 있음
즉, getObject()
를 호출하는 시점에 MyLogger가
생성된다
getObject()
를 호출하시는 시점에는 HTTP 요청이 진행중이므로 request scope 빈이 생성된다
getObject()
를 LogDemoController , LogDemoService에서 각각 한 번씩 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
...
}
LogDemoController , LogDemoService는 Provider 적용 이전 코드로 다시 작성
proxyMode = ScopedProxyMode.TARGET_CLASS
를 추가
적용 대상이 클래스면 TARGET_CLASS
적용 대상이 인터페이스면 INTERFACES
MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입한다
@Scope
의 proxyMode = ScopedProxyMode.TARGET_CLASS)
를 설정하면 스프링 컨테이너는 CGLIB 라는 바이트코드를 조작하는 라이브러리를 사용해서, MyLogger를 상속받은 가짜 프록시 객체를 생성
생성된 객체를 출력해보면 MyLogger$$EnhancerBySpringCGLIB
이라는 클래스로 만들어진 객체가 대신 등록된 것을 확인할 수 있다
스프링 컨테이너에 myLogger라는 이름으로 가짜 프록시 객체를 등록하기 때문에 ac.getBean()
으로 조회해도 프록시 객체가 조회되며, 의존관계 주입 역시 이 가짜 프록시 객체가 주입된다
생성된 가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다
클라이언트가 myLogger.logic()
을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이고
가짜 프록시 객체는 request scope의 진짜 myLogger.logic()
을 호출한다
가짜 프록시 객체는 실제 request scope과 관계없이 내부에 단순한 위임 로직만 있고 싱글톤 처럼 동작한다
가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 원본인지 아닌지 모르게, 동일한 방식으로 사용할 수 있다 (다형성)
Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다
단지 어노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있는데 이게 다형성과 DI 컨테이너가 가진 큰 강점
웹 스코프가 아니어도 프록시는 사용할 수 있다
Ctrl + Alt + N