[Spring Basic] 빈의 생명주기와 스코프
싱글톤 패턴에서 스프링 빈의 라이프사이클은 다음과 같다.
스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 객체 사용 -> 소멸 전 콜백 -> 종료
스프링 컨테이너가 초기화되는 시점에 스프링 빈이 생성되고, 이후 객체 초기화 과정에서 빈으로 등록된 객체들이 주입된다.
별도로 설정하지 않았으면 빈으로 관리되는 객체들은 싱글톤으로 관리되고 한 번 주입되 객체들이 공유돼서 사용된다.
내부 값을 조금 변경하는것과 같이 단순한 경우에는 생성자를 통해 두 작업을 함께 처리해도 괜찮지만, 일반적으로 초기화 작업은 메모리를 할당해 객체를 생성하는 작업보다 무거운 작업이기 때문에 분리되어야 한다.
여기서 초기화 콜백과 소멸 전 콜백이 낯선데, 각각 초기화 작업이 수행된 후 메서드를 호출하고, 소멸되기 직전에 메서드를 호출하는 역할을 한다.
콜백을 구현하는 방법으로는 세 가지가 있는데, 자주 사용되는 방법을 살펴보자.
@PostConstruct @PreDestory 애너테이션을 사용한다.
public class NetworkClient {
private String url;
public NetworkClient() {
System.out.println("생성자 호출, url = " + url);
}
public void setUrl(String url) {
this.url = url;
}
//서비스 시작시 호출
public void connect() {
System.out.println("connect: " + url);
}
public void call(String message) {
System.out.println("call: " + url + " message = " + message);
}
//서비스 종료시 호출
public void disConnect() {
System.out.println("close + " + url);
}
@PostConstruct
public void init() {
System.out.println("NetworkClient.init");
connect();
call("초기화 연결 메시지");
}
@PreDestroy
public void close() {
System.out.println("NetworkClient.close");
disConnect();
}
}
콜백에 사용될 메서드를 애너테이션을 사용해서 지정해준다.
이 외에도 @Bean(initMethod = "init", destroyMethod = "close") 을 사용해 설정 정보를 조작해서 구현하는 방법과 InitializingBean, DisposableBean 인터페이스를 사용해서 구현하는 방법이 있다.
스코프는 빈이 존재할 수 있는 범위는 @Scope("~~) 애너테이션으로 지정한다.
싱글톤 스코프 : 스프링 컨테이너의 시작될 때 부터 종료될 때 까지 빈이 존재한다.
프로토타입 스코프 : 스프링 컨테이너는 빈의 생성과 의존관계 주입까지만 관여하고 더 이상 관리하지 않는다. (종료 메서드 호출이 안됨)
객체가 한 번 생성되고 사용자들이 해당 객체를 공유해서 사용하는 싱글톤 패턴과 싱글톤 스코프는 다른 개념이다.
(스프링 프레임워크 개발 당시 싱글톤 패턴과 프로토타입 패턴의 개념을 빈의 생명주기에 적용해 이름을 차용했다.)
싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 컨테이너에 있는 같은 인스턴스의 빈을 반환하고, 프로토타입 스코프의 빈을 조회하면 새로운 인스턴스를 생성해서 반환한다.
프로토타입 스코프 빈은 반환한 후 스프링 컨테이너는 해당 빈을 더 이상 관리하지 않는다.
관리하지 않으니 또 같은 요청이 오면 매번 인스턴스를 만들 수 밖에 없다. 이제 프로토타입 빈을 관리할 책임은 요청한 클라이언트에게 넘어간다.
클라이언트가 싱글톤 빈을 요청하고, 싱글톤 빈은 프로토타입 빈을 요청하는 경우를 생각해보자.
프로토타입 빈이 생성자 주입으로 연결되어있다고 할 때 싱글톤 빈이 프로토타입 빈을 요청하면 스프링 컨테이너는 의존관계를 주입하고 더 이상 프로토타입 빈을 관리하지 않는다.
여기서 클라이언트가 싱글톤 빈을 통해 프로토타입 빈의 logic을 계속 호출하면 싱글톤 빈 내부의 프로토타입 빈은 생성될 때 만들어진 빈을 계속 사용한다.
프로토타입 빈을 사용하는거는 항상 새로운 인스턴스를 쓰고 싶어서인데.. 이렇게 되면 의도와는 맞지 않는다.
지금 상황처럼 의존관계를 외부에서 찾아서 사용해야 할 경우가 있는데, 이걸 Dependency Lookup (DL) 이라고 부른다.
싱글톤 빈과 프로토타입 빈을 함께 사용할 때 프로토타입 빈을 의도대로 사용하기 위해, 즉 DL 정도의 기능을 사용하기 위해 Provider를 사용한다.
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
스프링이 기본적으로 제공하는 기능으로, getObject 메서드를 호출하면 컨테이너에서 프로토타입 빈을 찾아서 반환해준다.
프로바이더는 컨테이너에서 요소를 찾아주는 도구로 생각하자.
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
스프링 말고 자바에서 제공하는 Provider를 사용할 수도 있다.
(특정 기능을 사용할 때 자바에서 제공하는 기능인지 스프링에서 제공하는 기능인지 구분하자)
사실 대부분의 문제는 싱글톤 빈으로 해결돼서 프로토타입 빈은 거의 사용하지 않는다.
대신 Provider는 프로토타입 빈 뿐만 아니라 DL이 필요한 경우에 사용할 수 있다.
(lazy or optional / breaking circular dependencies)
스프링의 Provider, 자바의 Provider 중 뭘 사용할지는 그때마다 다른데..
보통 스프링이 제공하는 기능을 사용하는 편이 좋다.
웹 관련 스코프도 알아보자.
request : 웹 요청이 들어오고 나갈 때 까지 유지됨
session : 세션의 생성 ~ 종료 까지 유지됨
application : 웹의 서블릿 컨텍스트와 같은 범위로 유지됨
request는 클라이언트의 요청에 따라 해당 클라이언트만의 요소를 할당하고, 다 사용하면 파기되는 방식으로 작동한다.
동시에 여러 HTTP요청이 들어오면 어떤 요청이 어떤 로그를 남긴지 확인해야 하는데, 이 때 request 스코프를 사용한다.
@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";
}
}
스프링을 실행할 때 싱글톤 빈은 가져올 수 있지만 request 스코프 빈은 아직 생성되지 않아 가져올 수 없다.
실제 고객의 요청이 와야 생성할 수 있는데, 여기서 request 스코프 빈의 생성을 지연시키고 provider의 getObject() 메서드를 호출할 때는 정상적으로 빈을 가져오도록 할 수 있다.
즉, Provider를 사용하지 않고 Request 스코프의 빈을 바로 주입받는식으로 작성하면 스프링 애플리케이션이 실행될 때 빈을 주입할 수 없어 오류가 발생한다.
클라이언트의 요청이 오지 않았고 애플리케이션의 시작 부분에서 빈을 주입하려고 했기 때문인데, 바로 주입받는 방법 대신 ObjectProvider를 주입받아서 실제 요청이 들어왔을 때 ObjectProvider를 통해 Request 스코프의 빈을 생성하도록 했다.
프록시를 사용하면 Provider를 사용할 때 보다 좀 더 간단하게 구현할 수 있다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
문제는 아직 생성되지 않은 request 스코프의 빈을 주입하려고 할 때 발생했다.
proxyMode = ... 을 사용하면 바이트코드를 조작하는 CGLIB 라이브러리를 사용해 해당 클래스를 상속받은 가짜 프록시 객체를 만들어 컨테이너에 등록한다. (가짜 객체는 request 스코프와 무관하고, 싱글톤처럼 동작한다.)
가짜 프록시 객체는 요청이 오면 그 때 내부에서 진짜 빈을 요청하는 로직을 수행한다.
객체를 사용하는 클라이언트는 가짜 프록시 객체를 통해서 받든, 진짜 객체를 통해서 받든 상관하지 않는다. 가짜 프록시 객체는 원본 클래스의 자손이기 때문에 다형성이 적용된다.
Provider와 Proxy 두 가지 방법 모두 진짜 객체의 조회를 필요한 시점까지 지연해서 문제를 해결한다.
'Spring > Spring' 카테고리의 다른 글
[Spring Basic] 예외처리 (0) | 2023.06.13 |
---|---|
[Spring Basic] 로그 (0) | 2022.08.16 |
[Spring Basic] 의존관계 주입 (0) | 2022.08.08 |
[Spring Basic] Singleton (0) | 2022.08.07 |
[Spring Basic] 스프링과 IoC (0) | 2022.08.06 |
댓글
이 글 공유하기
다른 글
-
[Spring Basic] 예외처리
[Spring Basic] 예외처리
2023.06.13 -
[Spring Basic] 로그
[Spring Basic] 로그
2022.08.16 -
[Spring Basic] 의존관계 주입
[Spring Basic] 의존관계 주입
2022.08.08 -
[Spring Basic] Singleton
[Spring Basic] Singleton
2022.08.07