강의를 들으며 생각 정리
빈 스코프
앞서 배운 싱글톤 빈은 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료될 때까지 유지된다. 이는 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. 스코프는 빈이 존재할 수 있는 범위를 뜻한다.
스프링은 다음과 같은 다양한 스코프를 지원한다.
- 싱글톤 : 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
- 프로토타입 : 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리 하지 않는 매우 짦은 범위의 스코프이다.
- 웹 관련 스코프 : 대표적으로 request 스코프가 있다. 웹 요청이 들어오고 나갈때까지 유지되는 스코프이다.
빈 스코프는 다음과 같이 지정할 수 있다.
@Scope("prototype")
@Component
public class HelloBean {}
(@Bean, 수동 빈에도 가능하다. 따로 Scope를 명시하지 않으면 기본적으로 singleton을 사용한다.)
프로토타입 스코프
특징
- 스프링 컨테이너에 요청할 때마다 객체가 새롭게 생성된다.
- 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입 그리고 초기화까지만 관여한다.
- 종료 메서드가 호출되지 않는다.
- 그래서 프로토타입 빈은 프로토타입 빈을 조회한 클라이언트가 관리해야 한다. 종료 메서드에 대한 호출도 클라이언트가 직접 해야한다.
싱글톤 빈과 함께 사용시 문제점
만약 싱글톤 빈에서 프로토타입 빈을 의존하는 경우를 생각해 보자.
싱글톤 빈이 의존관계 주입 요청을 하면 스프링 컨테이너는 프로토타입 빈을 생성해서 싱글톤 빈에 반환한다. 이제 싱글톤 빈은 프로토타입 빈을 내부 필드에 보관한다.
이 때, 두 클라이언트가 싱글톤 빈을 통해 프로토타입 빈의 로직을 수행하면 각 클라이언트 별로 다른 프로토타입 빈이 참조되는 것이 아닌 싱글톤 빈 안에 같은 프로토타입 빈을 참조하는 상황이 발생한다. 즉, 사용할 때마다 프로토타입 빈이 새로 생성되는 것이 아니다.
-> 정리 : 스프링은 일반적으로 싱글톤 빈을 사용하므로 싱글톤 빈이 프로토타입 빈을 사용하게 된다. 그런데 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지되는 것이 문제다.
싱글톤 빈과 함께 사용시 문제점 -> 문제 해결
지정한 빈을 컨테이너에서 대신 찾아주는 서비스를 DL이라고 하는데 이 서비스를 스프링에서는 ObjectProvider, JAVA에서는 Provider로 제공해준다.
다음은 JAVA에서 제공하는 Provider의 예시이다.
@Scope("singleton")
static class ClientBean{
@Autowired
private Provider<PrototypeBean> prototypeBeanProvider;
public int logic(){
PrototypeBean prototypeBean = prototypeBeanProvider.get();
prototypeBean.addCount();
int count=prototypeBean.getCount();
return count;
}
}
Provider의 get 메서드를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.(DL)
그런데 실무에서 웹 애플리케이션을 개발해보면, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.
실무에서 자바 vs 스프링??
실무에서 비슷한 기능에 대해서 자바 표준을 사용할 것인지, 스프링이 제공하는 기능을 사용할 것인지 고민이 된다. DL을 예로 들면 ObjectProvider는 DL을 위한 편의 기능을 많이 제공해주고 스프링 외에 별도의 의존관계가 추가로 필요 없기 때문에 편리하다. 만약 코드를 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야 한다면 자바 표준 Provider을 사용해야 한다. 그런데 이런 경우는 극히 드물다.
스프링을 사용하다 보면 이 기능 뿐만 아니라 다른 기능들도 자바 표준과 스프링이 제공하는 기능이 겹칠 때가 많이 있다. 대부분 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에, 특별히 다른 컨테이너를 사용할 일이 없다면, 스프링이 제공하는 기능을 사용하면 된다.(ex. Autowired)
웹 스코프
웹 스코프는 웹 환경에서만 동작한다. 프로토타입과 다르게 해당 스코프의 종료시점까지 관리된다. 따라서 종료 메서드가 호출된다. 대표적으로 request 스코프가 있다.
request 스코프는 HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프이다. 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다.
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id){
myLogger.log("service id = "+id);
}
}
위와 같은 서비스가 있다고 가정하자. MyLogger는 request 스코프 빈이다. 그러나 실제로 코드를 실행시키면 오류가 발생한다. 스프링 애플리케이션은 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 생성하지 않는다. 이 빈은 실제 고객의 요청이 와야 생성할 수 있다.
이는 프로토타입의 경우와 마찬가지로 간단히 ObjectProvider을 사용해서 해결할 수 있다.
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " + id);
}
}
ObjcetProvider 덕분에 getObject 메서드를 호출하는 시점까지 request 스코프 빈의 생성을 지연하고 호출하는 시점에 HTTP 요청이 진행중이므로 request 스코프 빈의 생성이 정상 처리된다.
여기서 프록시 방식을 이용해 코드를 더 줄일 수 있다.
requrest 스코프 빈에 프록시 옵션을 추가해보자.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
(적용 대상이 클래스면 TARGET_CLASS, 인터페이스면 INTERFACES를 선택한다.)
그리고 서비스도 다음과 같이 원래 상태로 돌려 놓을 수 있다.
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id){
myLogger.log("service id = "+id);
}
}
실행해보면 잘 동작하는 것을 확인할 수 있다. 어떻게 된 것일까?
실제로 myLogger의 클래스를 출력해보면 다음과 같다.
myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d
역시 기본 클래스가 아닌 CGLIB이라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체가 만들어졌다.
가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다. 클라이언트가 logic메서드를 호출하면 이 가짜 프록시 객체는 request 스코프의 진짜 myLogger.logic 메서드를 호출한다.
-> 정리
- CGLIB이라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어 주입한다.
- 이 가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다.
프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request 스코프를 사용할 수 있다. 사실 Provider, 프록시 어느 것을 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연 처리한다는 점이다. 위 코드에서는 myLogger.logic()이 수행되기 전까지 가짜 프록시 객체만 존재하고 있는 것이다.
클라이언트 코드를 전혀 고치지 않고 단지 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체한다. 이것이 다형성과 DI 컨테이너가 가진 큰 장점이다.
그래도 이런 특별한 scope는 원할한 유지보수를 위해 최소화하는 것이 좋다.
'java > spring' 카테고리의 다른 글
스프링 - 롬복(lombok) 라이브러리 설치 (0) | 2021.02.09 |
---|---|
JUnit Test error - No runnable Methods (1) | 2021.01.15 |
[Spring] 빈 생명주기 콜백 (0) | 2021.01.11 |
[Spring] 의존관계 자동 주입 (0) | 2021.01.11 |
[Spring] 컴포넌트 스캔 (0) | 2021.01.08 |