빈 스코프
김영한님의 스프링 핵심 원리 - 기본편 강의를 듣고 작성했습니다.
스프링 핵심 원리 - 기본편 - 인프런 | 강의
스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 개발자가 되어보세요! 📣 확인해주
www.inflearn.com
빈 스코프란?
다른 프로그래밍 언어에서의 스코프와 마찬가지로 '빈이 존재 할 수 있는 범위'를 뜻한다.
스프링은 다음과 같은 스코프를 지원한다
- 싱글톤: 기본 스코프, 스프링 컨테이너의 시작부터 종료될 때 까지 유지된다.
- 프로토타입: 스프링 컨테이너가 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는다
- 웹 스코프 : 웹과 관련된 스코프를 다룬다
웹 관련 스코프
- request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프
- session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
- application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
스코프 지정
컴포넌트 스캔 자동 등록
수동 등록
@Scope("prototype")
@Bean
PrototypeBean BeanTest() {
return new BeanTest();
}
프로토타입 스코프
프로토타입 스코프를 가진 빈을 스프링 컨테이너에 요청하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다.
1. 프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다
2. 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존 관계를 주입한다
3. 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환한다
4. 이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입을 생성해서 반환한다.
* 스프링 컨테이너는 프로토타입를 생성, 의존관계 주입, 초기화까지만 관여하고 그 이후 관리는 일절 관여하지 않는다. 따라서 관리할 책임은 클라이언트에 있고, 이전에 알아봤던 빈 생명주기에서 사용하는 @PreDestroy등의 기능을 사용 할 수 없다.
싱글톤 스코프 빈 테스트
CGLIB에서 붙여주는 주소가 같고 singletonBean의 종료전 콜백도 정상적으로 작동하는 것을 확인 할 수 있다.
프로토타입 스코프 빈 테스트
CGLIB의 값이 다르고 종료 후 @PreDestroy가 작동하지 않는것을 볼 수 있다.
프로토타입 스코프를 싱글톤 빈과 함께 사용시 생기는 문제점
다음과 같은 코드가 있다고 해보자
위의 코드는 싱글톤 빈 안에 프로토타입 빈이 들어간 형태로 다음과 같은 방식으로 실행되는 코드이다
이를 개발한 개발자의 의도는 clientBean을 사용할때마다 prototypebean을 새로 발행하여 각각의 클라이언트들에게 다른 상태값을 부여해주는 의도를 가지고 만들었다고 해보자.
하지만 위와 같이 개발할경우 문제점이 있는데
위와 같이 새로운 유저가 logic()을 호출할 경우 Prototypebean을 새로 생성하는것이 아니라 이미 초기화된 프로토타입 빈을 사용하게 되는 것이다.
이는 클라이언트 빈이 싱글톤빈이라 해당되는 문제인데 어떻게하면 사용 할 때마다 항상 새로운 프로토타입 빈을 생성 할 수 있을까?
1. 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청하기
이와같이 의존관계를 외부에서 주입(DI)받는 것이 아니라 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL) 의존관계 조회(탐색)이라 한다.
하지만 이와같이 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 테스트도 어려워진다.
다른 방법은 무엇이 있을까?
2. ObjectFactory, ObjectProvider
ObjectProvide는 지정한 빈을 컨테이너에서 찾아주는 기능을 한다. 과거에는 ObjectFactory를 사용했는데, 여기에 편의 기능을 추가해서 ObjectProvider가 만들어졌다.
ObjectProvider의 getObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다(DL)
3. JSS-330 Provider
javax.inject.Provider라는 JSR-330 자바 표준을 사용하는 방법이다.
사용하려면 javax.inject:javax.inject:1 라이브러리를 gradle에 추가해야 한다.
provider.get()을 통해서 새로운 프로토타입 빈이 생성되는 것을 확인 할 수 있다.
자바 표준이고 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 쉬워진다.
자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용 할 수 있다.
웹 스코프
웹 스코프틑 웹 환경에서만 동작한다.
웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다. 따라서 종료 메서드를 호출 할 수 있다.
웹 스코프 종류
request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, HTTP request 요청당 각각 할당된다.
session: HTTP Session과 동일한 생명주기를 가지는 스코프
application: 서블릿 컨텍스트와 동일한 생명주기를 가지는 스코프
websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프
Request Log를 찍는 다음과 같은 예제가 있다고 해보자
이와같이 코드를 짜고 실행해보면
위와 같은 에러가 나온다
이는 Logger가 웹스코프로 지정되어있어 사용자의 요청이 있을때만 빈이 생성되기 때문에
LogController가 생성될 때에는 Logger를 DI 할 수 없기에 생겨나는 에러이다.
이를 해결할 방법으로는 2가지가 있다.
1. Provider 사용
2. 프록시 방법 사용
1. Provider
ObjectProvider<Object>를 사용하고 MyLogger가 필요할때 getObejct()를 사용해 호출하는 방법이다.
2. 프록시 사용
MyLogger의 @Scope 애노테이션에 proxyMode = ScopedProxyMode.TARGET_CLASS를 추가해주면 된다.
만약 인터페이스라면 proxyMode = ScopedProxyMode.INTERFACE를 추가해주면 된다.
웹 스코프와 프록시 동작 원리
어떻게 프록시와 provider는 웹 스코프의 에러를 잡을 수 있었을까?
주입된 myLogger를 확인해보면 CGLIB의 바이트 코드 조작이 사용 된 것을 볼 수 있다.
CGLIB은 스프링 컨테이너에 "myLogger"라는 이름으로 진짜 대신에 가짜 프록시 객체를 등록한다.
일단 생성 된 뒤에 객체에 요청이 오면 그 때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
클라이언트가 myLogger.logic()을 호출하면 사실 가짜 프록시 객체의 메서드를 호출한것이다.
가짜 프록시 객체는 request 스코프의 진짜 myLogger.logic()을 호출한다.
*이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자, 무분별하게 사용하면 유지보수하기 어려워진다.