우리가 자동으로 등록된다고 생각한 이 빈도 언제 시작되고 어떻게 사라지는지 알아야한다.
스프링 빈의 이벤트 라이프사이클
스프링 컨테이너 생성 스프링 빈 생성 의존관계 주입 초기화 콜백 사용 소멸전 콜백 스프링
종료
초기화 콜백: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
소멸전 콜백: 빈이 소멸되기 직전에 호출
우리가 지금까지 써왔고 일반적인 빈들은 싱글콘 빈 이기때문에 은 스프링 컨테이너가 종료될 때 싱글톤 빈들도 함께 종료된다.
그래서 이 주기들에 맞춰서 콜백함수를 지원해주는데 빈으로 등록할 클레스 함수에 각각 @PostConstruct, @PreDestroy 애노테이션을 사용면 된다.
단 코드를 고칠 수 없는 외부 라이브러리를 초기화, 종료해야 하면 @Bean 의 initMethod , destroyMethod
를 사용하자
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();
}
}
지금까지 우리는 스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료될 때
까지 유지된다고 학습했다. 이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. 스코프는
번역 그대로 빈이 존재할 수 있는 범위를 뜻한다.
스프링은 다음과 같은 다양한 스코프를 지원한다.
스코프 변경법
@Scope("prototype")
@Component
public class HelloBean {}
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}
싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다. 반면에
프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서
반환한다.
더불어 가장큰 특징은
스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다는
것이다. 클라이언트에 빈을 반환하고, 이후 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않는다.
프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있다. 그래서 @PreDestroy
같은 종료 메서드가 호출되지 않는다.
이런식으로 별개의 객체로 사용되는 경우 프로토타입 빈에 count
라는 변수가 있다면 유저마다 독립적으로 사용할 수 있게된다.
A유저가 count에 +1을해서 count = 1이 되었다해도
B유저가 새로 요청을하면 완전히 다른 객체가 생성되기에 count는 초기값인 0으로 따로 쓸 수있다.
만약 싱글톤 빈안에 프로토타입빈이 존재해서 유저는 싱글톤 빈을 사용하는 구조라면
이때는 위와 같은 예시처럼 안돌아간다.
싱글톤빈 입장에서 프로토타입빈 하나를 계속 가지고 있고 이 싱글톤빈 하나를 계속 사용하는 구조가 되기때문에 의도한 프로토타입의 방식으로 동작하지 않을수있다.
대신 이런 구조의 빈이 여러개라면 그 내부에서의 주입받은 프로토타입빈은 다르긴 하겠다.
프로토타입빈을 주입할때 각각 새로 생성이 되는것이니깐
@Test
void providerTest() {
AnnotationConfigApplicationContext ac = new
AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int count1 = clientBean1.logic();
assertThat(count1).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int count2 = clientBean2.logic();
assertThat(count2).isEqualTo(1);
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
위 예시는 ClientBean.class, PrototypeBean.class
를 빈으로 등록하고 ClientBean.class는 싱글톤 PrototypeBean.class는 프로토타입이다.
logic 함수에다가 이 함수를 사용할때마다 ac.getBean(PrototypeBean.class);
가져와서 사용하고
ClientBean clientBean1 = ac.getBean(ClientBean.class);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
이렇게 각각 따로 다시 달라고하면
기존의 ClientBean.class자체는 싱글톤이니 같은 객체일것이다.
다만 내부 logic에서 다시 프로토타입빈 ac.getBean(PrototypeBean.class);
을 요청할때는 프로토타입이니 각각 다른게 주입되서 사용될것이다.
이런식으로 내가 스프링 컨테이너 안에있는것을 "찾아서 꺼내줘" 하는방식으로 싱글톤과 프로토타입 빈의 특성일 이용하는걸 Dependency Lookup (DL) 의존관계 조회(탐색) 이라한다.
ac.getBean(PrototypeBean.class);
이것을 좀더 명확하게 하고 옵션 상속같은걸 지원주는 방식이 있으니 공부한 pdf를 참고하는게 좋다.
웹 스코프의 특징
웹 스코프는 웹 환경에서만 동작한다.
웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다. 따라서 종료 메서드가 호출된다.
웹 스코프 종류
동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다.
이럴때 사용하기 딱 좋은것이 바로 request 스코프이다.
다음과 같이 로그가 남도록 request 스코프를 활용해서 추가 기능을 개발해보자
@Component
@Scope(value = "request")
public class MyLogger {
private String uuid;
private String requestURL;
public void setRequestURL(String requestURL) {
this.requestURL = requestURL;
}
public void log(String message) {
System.out.println("[" + uuid + "]" + "[" + requestURL + "] " +
message);
}
@PostConstruct
public void init() {
uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid + "] request scope bean create:" + this);
}
@PreDestroy
public void close() {
System.out.println("[" + uuid + "] request scope bean close:" + this);
}
}
요청이 들어올때마다 그 요청에서 지나는것들을 uuid의 고유값과 함께 확인할수있는 클레스를 만들어 @Component를통에 빈에 등록한다.
@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";
}
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " + id);
}
}
참고로 requestURL을 MyLogger에 저장하는 부분은 컨트롤러 보다는 공통 처리가 가능한 스프링 인터셉터나 서블릿 필터 같은 곳을 활용하는 것이 좋다.
이때 사용시점마다
MyLogger myLogger = myLoggerProvider.getObject();
이렇게 찾아주지 않으면 오류가난다. 이유는 LogDemoService
와 LogDemoController
는 싱글톤 빈이라서 프로그램 실행시키자마자 바로 생성되며
private final MyLogger myLoggerProvider;
이런식의 값으로 사용하면 의존관계 주입을 바로 하려고하는데 MyLogger
는 request 빈이라서 아직 생성을 안한다 그래서 위와 같이 아까 배운것을 이용
private final ObjectProvider<MyLogger> myLoggerProvider;
이런식으로 프로바이더를 사용해주면 ObjectProvider.getObject() 를 호출하는 시점까지 request scope 빈의
생성을 지연할 수 있다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
이런식으로 사용하면 ScopedProxyMode.TARGET_CLASS
이 이름으로 가짜 빈이 싱글톤 빈에 주입될수있다.
이렇게 쓰면 위의 예시에서
MyLogger myLogger = myLoggerProvider.getObject();
이거 지우고
private final MyLogger myLoggerProvider;
이거로 사용가능하다.
CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.
@Scope 의 proxyMode = ScopedProxyMode.TARGET_CLASS) 를 설정하면 스프링 컨테이너는 CGLIB 라는 바이트코드를 조작하는 라이브러리를 사용해서, MyLogger를 상속받은 가짜 록시 객체를 생성한다.
가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다
사실 Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다.
복습 하면서 좀더 스프링 컨테이너를 유기적으로 사용할 수있게된거같다. 요즘 프로젝트도 하면서 스프링 빈와 서블릿필터에대해 이해도가 높아졌는데 이를 이용해서 편의 데이터 수집 기능을 구현할 수 있을거같다.