애플리케이션 시작 시점에 필요한 커넥션(DB 커넥션 풀, 네트워크 소켓)을 미리 생성하고 종료 시점에 커넥션을 종료하는 작업을 진행해야하는 경우에는, 객체의 초기화와 종료 작업이 필요하다.
public class NetworkClient {
private String url;
public NetworkClient() {
System.out.println("생성자 호출, url = " + url);
connect();
call("초기화 연결 메시지");
}
public void setUrl(String url) {
this.url = url;
}
...
}
생성자 부분을 보면 url 정보 없이 connect
가 호출되는데, 당연히 객체를 생성하는 단계에서는 url이 없고 이후 수정자 주입을 통해 setUrl()
이 호출되어야 url 값이 존재하기 때문이다.
생성자 주입을 제외한 나머지 필드와 세터 주입은, 객체 생성 -> 의존관계 주입이라는 라이프사이클을 가진다. 따라서 초기화 작업은 의존관계 주입까지 완료된 이후에 호출해야 위와 같은 문제 상황이 발생하지 않을 것이다.
🔖 객체 생성과 초기화 분리
생성자 주입을 통해 생성과 주입을 한번에 완료해도 괜찮지만, 가능한 객체 생성과 초기화 작업을 분리하자. 생성자의 메모리를 할당해서 객체를 생성하는 책임과 달리 초기화 작업은 외부 커넥션에 연결하는 등 무거운 작업이 일어나기 때문이다.
그렇다면 의존관계 주입이 완료되었는지 어떻게 알 수 있을까?
스프링은 의존관계 주입이 완료되면 콜백 메서드를 통해서 스프링 빈에게 초기화 시점을 알려주고, 컨테이너 종료 전 소멸 콜백을 알려주는 다양한 기능들을 제공한다. 인터페이스, 메소드, 어노테이션 세가지 방법이 존재한다.
초기화 콜백
은 빈이 생성되고 의존관계 주입 완료 후 호출되고, 소멸 전 콜백
은 빈이 소멸되기 직전에 호출된다.
이 인터페이스는 스프링 전용 인터페이스므로, 해당 코드가 스프링 전용 인터페이스에 의존하게 된다. 또한 초기화, 소멸 메서드의 이름을 변경할 수 없으며
직접 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다는 단점들이 존재한다.
따라서, 현재는 거의 사용하지 않는 방법이다.
public class NetworkClient implements InitializingBean, DisposableBean {
...
//의존관계 주입이 끝나면 호출
@Override
public void afterPropertiesSet() throws Exception {
connect();
call("초기화 메시지");
}
//빈 소멸 전에 호출
@Override
public void destroy() throws Exception {
disconnect();
}
}
설정 정보에 @Bean(initMethod = "init", destroyMethod = "close")
처럼 초기화, 소멸 메서드를 지정해주면 된다.
메서드 이름을 자유롭게 지을 수 있으며, 스프링 코드에 전혀 의존하지 않는다는 장점이 있다. 더불어 설정 정보만 고치기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 사용 가능하다.
public class NetworkClient{
...
//의존관계 주입이 끝나면
public void init() {
System.out.println("NetworkClient.init");
connect();
call("초기화 메시지");
}
}
@Configuration
static class LifeCycleConfig {
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient networkClient(){
NetworkClient networkClient = new NetworkClient(); //url : null 값
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
🔖 destoryMethod의 추론 기능
외부 라이브러리의 종료 메소드인close
,shutdown
을 자동으로 호출해준다.destroyMethod
는 기본값이(inferred)
, 즉 추론으로 등록되어 있기 때문에 추론 기능을 끄고 싶다면 옵션 값에 공백을 설정해주도록 하자.
메서드 위에 어노테이션만 붙이면 되므로 매우 편리하고, 스프링에 종속적이지 않아서(javax
) 다른 컨테이너에서도 동작한다는 장점이 존재한다.
하지만 코드를 고칠 수 없는 외부 라이브러리의 경우에는 빈 등록 초기화 및 소멸 메서드를 사용하자.
@PostConstruct
public class NetworkClient{
@PostConstruct
public void init() throws Exception {
System.out.println("NetworkClient.init");
connect();
call("초기화 메시지");
}
@PreDestroy
public void close() throws Exception {
System.out.println("NetworkClient.close");
disconnect();
}
}
스코프란 빈이 존재할 수 있는 범위를 의미하며, 스프링 빈은 기본적으로 싱글톤 스코프로 생성한다.
@Scope("prototype")
@Component
public class HelloBean {}
싱글톤
: 기본 스코프로써, 스프링 컨테이너와 같은 생명주기를 가짐프로토타입
: 스프링 컨테이너가 빈의 생성과 의존관계 주입까지만 관여(초기화 메소드까지는 불러줌)하는 매우 짧은 범위의 스코프웹 관련 스코프
request
: 웹 요청이 들어오고 나갈때까지 유지되는 스코프session
: 웹 세션이 생성되고 종료될 때까지 유지되는 스코프application
: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프 싱글톤 스코프가 조회 시 항상 같은 인스턴스의 스프링 빈을 반환하는 것과 달리, 프로토타입 스코프는 항상 새로운 인스턴스를 생성해서 반환한다.
싱글톤 빈 내부에는 의존 관계 주입으로 프로토타입 빈의 참조를 가지고 있을 것이다. 다만 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 프로토타입이 요청 시마다 새로 생성되는 것이 아니라 주입 시에만 한번 생성되어 같은 빈이 유지되기 때문에 문제가 발생한다.
@Scope("singleton") //싱글톤
static class ClientBean{
private final PrototypeBean prototypeBean; //생성 시점에 이미 주입(계속 같은 것)
@Autowired
public ClientBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic(){
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
ClientBean
객체는 싱글톤이기 때문에 주입 된 PrototypeBean
은 이미 과거(생성 시점)에 주입이 끝난 빈이다. 따라서 서로 다른 클라이언트가 요청해도, 같은 프로토타입 빈이 반환된다.
그렇다면 어떻게 사용할 때 마다 항상 새로운 프로토타입 빈을 생성할 수 있을까? 즉 프로토 타입을 주입 시점에만 생성하는 것이 아니라, 요청 시마다 생성하려면 어떻게 해야할까?
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class); //항상 새로운 프로토타입 빈 생성
...
}
외부에서 주입 받는 것이 아니라, 스프링 컨테이너로부터 직접 필요한 의존관계를 찾아서 해결할 수 있다(Dependency Lookup
). 하지만, 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다.
ObjectProvider
는 스프링 컨테이너에서 필요한 빈을 대신 조회하는 DL
서비스를 제공한다. getObject()
를 통해, 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한는데 프로토타입 스코프는 요청 시 마다 새로운 객체가 반환되니 문제가 해결이 된다.
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject(); //컨테이너에서 해당 빈 찾아서 반환
...
}
웹 환경에서만 동작하며, 프로토 타입과 다르게 스프링이 스코프의 종료시점까지 관리한다. 따라서, 종료 메소드 호출이 가능하다.
request
: HTTP 웹 요청 하나가 들어오고 나갈때까지 유지되는 스코프, 각각의 클라이언트 요청마다 별도의 빈 인스턴스 생성되고 관리된다. 만약 컨트롤러에서 불러진 다른 객체라 하더라도 하나의 라이프 사이클 동안에는 같은 객체가 유지되므로 서비스에서 불러도 같은 객체가 반환될 것이다.session
: HTTP Session과 동일 생명주기 가지는 스코프이다.application
: 서블릿 컨텍스와 같은 범위로 유지되는 스코프이다. websocket
: 웹 소켓과 동일한 생명주기 가지는 스코프이다.implementation 'org.springframework.boot:spring-boot-starter-web'
spring-boot-starter-web
를 추가하면, 스프링 부트는 내장 톰캣 서버 활용하여 웹서버와 스프링 함께 실행시킨다. 웹 라이브러리 추가 시, 웹 관련 기능이 더 추가된 AnnotationConfigServletWebServerApplicationContext
를 기반으로 애플리케이션을 구동하게 된다.
Request 스코프를 활용한다면 동시에 여러 HTTP 요청이 올때에도 정확히 어떤 요청이 남긴 로그인지 구분이 가능하다. 빈 생성 시점에, 초기화 메소드 사용하여 자동으로 uuid
생성해 저장해두면 항상 요청이 유지되는 동안은 까지는 같은 객체를 반환해주므로 다른 HTTP 요청과 구분 가능해지기 때문이다.
@Component
@Scope(value = "request") //http 요청 당 하나씩 생성, 요청 끝나는 시점에 소멸
public class MyLogger {
private String uuid;
private String requestURL;
//빈 생성 시점에 알 수 없어서 외부에서 setter 로 입력
public void setRequestURL(String requestURL) {
this.requestURL = requestURL;
}
...
@PostConstruct
public void init(){
uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid + "] request scope bean create:" + this);
}
@PreDestroy
public void close(){
...
}
}
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger; // 의존 관계 주입
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request){
String requestURL = request.getRequestURI().toString(); //받은 URL값을 myLogger에 저장
myLogger.setRequestURL(requestURL);
// myLogger 로그 기능 수행
...
}
}
하지만, HTTP 요청은 빈이 생성되고 난 이후에 일어난다.
따라서 의존 관계 주입 과정에서 객체가 없어서 에러가 발생하는데, 이때 컨테이너에게 빈을 요청하는 단계는 의존관계 주입 단계가 아니라 뒤로 지연시켜야 빈이 초기화 되지 않았다는 오류가 발생하지 않을 것이다.
해결 방안으로는 앞에서 배운 Provider
와 프록시 두 가지 방법이 존재한다. 두 가지 방법 모두 진짜 객체 조회를 꼭 필요한 시점 까지 지연처리 한다는 점이 핵심이다.
🔖 Provider
ObjectProvider.getObject()
를 호출하는 시점까지request scope
빈의 생성을 지연 가능하다.HTTP
요청만 유지되고 있다면 컨트롤러, 서비스 상관없이 같은MyLogger
객체를 반환해준다. (비밀은 스레드 로컬에..)private final ObjectProvider<MyLogger> myLoggerProvider; public String logDemo(HttpServletRequest request){ String requestURL = request.getRequestURI().toString(); MyLogger myLogger = myLoggerProvider.getObject(); //필요한 시점에 처음 생성되고 초기화 메서드인 init이 실행된다. myLogger.setRequestURL(requestURL); ... }
proxyMode
를 설정해주면, 스프링 컨테이너는 CGLIB
로 MyLogger
를 상속 받은 가짜 프록시 객체를 생성 후 등록해주므로, HTTP
요청과 관계 없이 미리 주입이 가능해진다. 요청이 오면 가짜 프록시 객체가 실제 myLogger
를 호출하는 방식이다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}