스프링 빈 생면주기 콜백과 빈스코프

justindevcode·2023년 5월 29일
0

스프링부트

목록 보기
25/39
post-thumbnail

스프링 빈 생면주기 콜백과 빈스코프


빈 생명주기 콜백 시작

우리가 자동으로 등록된다고 생각한 이 빈도 언제 시작되고 어떻게 사라지는지 알아야한다.

스프링 빈의 이벤트 라이프사이클
스프링 컨테이너 생성 스프링 빈 생성 의존관계 주입 초기화 콜백 사용 소멸전 콜백 스프링
종료
초기화 콜백: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
소멸전 콜백: 빈이 소멸되기 직전에 호출

우리가 지금까지 써왔고 일반적인 빈들은 싱글콘 빈 이기때문에 은 스프링 컨테이너가 종료될 때 싱글톤 빈들도 함께 종료된다.

그래서 이 주기들에 맞춰서 콜백함수를 지원해주는데 빈으로 등록할 클레스 함수에 각각 @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();
 }
}

빈 스코프

지금까지 우리는 스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료될 때
까지 유지된다고 학습했다. 이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. 스코프는
번역 그대로 빈이 존재할 수 있는 범위를 뜻한다.

스프링은 다음과 같은 다양한 스코프를 지원한다.

  • 싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
  • 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는
    매우 짧은 범위의 스코프이다.
  • 웹 관련 스코프
    • request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프이다.
    • session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프이다.
    • application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프이다.

스코프 변경법

@Scope("prototype")
@Component
public class HelloBean {}
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
 return new HelloBean();
}

프로토 타입 스코프

싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다. 반면에
프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서
반환한다.

더불어 가장큰 특징은

스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다는
것이다. 클라이언트에 빈을 반환하고, 이후 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않는다.
프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있다. 그래서 @PreDestroy 같은 종료 메서드가 호출되지 않는다.

이런식으로 별개의 객체로 사용되는 경우 프로토타입 빈에 count라는 변수가 있다면 유저마다 독립적으로 사용할 수 있게된다.
A유저가 count에 +1을해서 count = 1이 되었다해도
B유저가 새로 요청을하면 완전히 다른 객체가 생성되기에 count는 초기값인 0으로 따로 쓸 수있다.


싱글톤 빈과 같이 사용할 때 주의점

만약 싱글톤 빈안에 프로토타입빈이 존재해서 유저는 싱글톤 빈을 사용하는 구조라면
이때는 위와 같은 예시처럼 안돌아간다.

싱글톤빈 입장에서 프로토타입빈 하나를 계속 가지고 있고 이 싱글톤빈 하나를 계속 사용하는 구조가 되기때문에 의도한 프로토타입의 방식으로 동작하지 않을수있다.

대신 이런 구조의 빈이 여러개라면 그 내부에서의 주입받은 프로토타입빈은 다르긴 하겠다.

프로토타입빈을 주입할때 각각 새로 생성이 되는것이니깐


위 문제를 해결 하기위해 Provider로 문제 해결

@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를 참고하는게 좋다.


웹 스코프

웹 스코프의 특징
웹 스코프는 웹 환경에서만 동작한다.
웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다. 따라서 종료 메서드가 호출된다.

웹 스코프 종류

  • request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다.
  • session: HTTP Session과 동일한 생명주기를 가지는 스코프
  • application: 서블릿 컨텍스트( ServletContext )와 동일한 생명주기를 가지는 스코프
  • websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프

동시에 여러 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();
이렇게 찾아주지 않으면 오류가난다. 이유는 LogDemoServiceLogDemoController 는 싱글톤 빈이라서 프로그램 실행시키자마자 바로 생성되며
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를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다.


나의 생각

복습 하면서 좀더 스프링 컨테이너를 유기적으로 사용할 수있게된거같다. 요즘 프로젝트도 하면서 스프링 빈와 서블릿필터에대해 이해도가 높아졌는데 이를 이용해서 편의 데이터 수집 기능을 구현할 수 있을거같다.

profile
("Hello World!");

0개의 댓글