-
스프링 핵심 원리 (5) - 빈 생명주기Web/Spring 2023. 8. 22. 12:30
1. 빈 생명주기 콜백 시작
- DB 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 지점에 필요한 연결을 미리 해두고,
종료 시점에 연결을 모두 종료하는 작업 진행시, 객체 초기화 - 종료 작업이 따로 필요하다.
public class NetworkClient{ private String url; public NetworkClient(){ connect(); call("연결 메시지"); } public void serUrl(Strin url){ this.url =url; } public void disconnect(){ //.. } //.. } @Configuration static class CycleConfig{ @Bean public NetworkClient networkClient(){ NetworkClient nct = new NetworkClient(); nct.setUrl("~"); return nct; } }
- 이를 실행시 원하는 결과를 얻을 수 없다.
- 객체 생성과 동시에 url을 설정하고, connect()를 작동시켜두고 싶은데,
생성과 설정이 분리되어 있기 때문이다.
> 스프링 빈은 객체 생성 -> 의존관계 주입의 라이프 사이클을 가진다.
- 의존관계 주입이 완료되야 필요 데이터 사용할 수 있는 준비가 완료된다.
- 따라서 객체 초기화 작업(여러 설정을 넘겨서 new 하는 과정)은 의존관계 주입 후 호출되어야 함
- 따라서 의존관계 주입이 완료되었다는 것을 알리기 위해 주입 완료시 콜백 메서드를 통해 초기화 시점을 알려주는 다양한 기능을 제공
- 스프링은 컨테이너 생성과 소멸 직전에 콜백을 준다.
- 스프링 빈 이벤트 라이프 사이클
컨테이너 생성 -> 빈 생성 -> 의존관계 주입 -> 초기화 콜백-> 사용->소멸 전 콜백 -> 스프링 종료
* 객체 생성과 초기화 분리
-> 생성자는 필수 정보만 받고, 초기화는 생성된 값 활용해서 외부 커넥션 연결등의 무거운 동작
-> 싱글톤 빈은 스프링 컨테이너 종료시 함께 소멸 컨테이너 종료 전에 콜백 발생
-> 물론 싱글톤 빈과 다르게 생명주기 짧은 애들도 있다.
* 컨테이너 수동종료를 위해서는 close() 메서드 필요
-> ConfugutableApplicationContext, Annotaion등 ApplicationContext의 자손 사용하자
* 스프링 3가지 방법으로 콜백을 지원
- 인터페이스
- 메서드
- 어노테이션
2. 콜백
- 인터페이스 initializingBean, DisposableBean
빈으로 등록할 클래스가 해당 인터페이스를 구현하도록 작성하면 된다.
initializingBean (afterPropertiesSet()) : 초기화 메서드 지원
DisposableBean(destroy()): 소멸 메서드 지원
* 스프링 전용 인터페이스 > 스프링에 의존적
메서드 명 변경 불가
코드를 고칠 수 없는 외부라이브러리에 적용 불가
- 빈 등록 초기화, 소멸 메서드 지정
@Bean(initMethod ="init", destroyMethod="close") 로 초기화 메서드 지정
- 메서드 이름 자유 사용, 스프링 빈이 스프링 코드에 의존하지 않음
- 외부라이브러리 빈으로 등록하면서 자유롭게 사용가능
* 종료 메서드 추론
destroyMethod 속성은 대부분의 라이브러리 종료 메서드인 close,shutdown 이름 메서드 추론해서
자동으로 사용함, 직접 스프링 빈 등록시 종료 메서드는 따로 안적어줘도 됨
- 어노테이션 @PostConstruct, PreDestroy
최신 스프링에서 권장하는 방법, 관리가 쉽다., 스프링에 종속된 기술 x (javax)
외부라이브러리에 적용불가 -> 외부라이브러리는 @Bean을 사용하자
2. 빈 스코프
2.1 빈 스코프란?
- 지금까지 생성한 빈은 싱글톤빈으로 컨테이너 시작과 함께 생성 - 종료까지 유지됨
- 스프링은 다양한 생명주기를 가진 빈 스코프을 지원한다.
싱글톤: 기본 스코프, 스프링 컨테이너 시작과 종료시까지 유지됨
프로토타입: 스프링 컨테이너에서 빈의 생성과 의존관계 주입까지만 관여하는 매우 짧은 범위 스코프
웹 관련스코프
request: 웹 요청 - 응답 까지 유지
session: 세션의 생성과 종료
application: 웹 서블릿 컨텍스트와 같은 범위 유지(하나의 웹 어플리케이션 시작-종료)
//자동등록 @Scope("prototype") @Component //수동등록 @Scope("prototype") @Bean
2.2 프로토타입 스코프
- 싱글톤 빈은 생성시 스프링 컨테이너에 몇번을 요청해도 같은 빈 반환
- 프로토 타입 빈을 스프링 컨테이너에 요청하면, 컨테이너는 빈을 생성하고, 의존관계 주입
- 생성한 빈을 반환하고, 컨테이너에서 관리하지 않는다 -> 이후 관리는 클라이언트 몫
- 요청시마다 새로운 빈을 생성해서 반환
* 빈생성, 의존관계주입, 초기화처리까지만 관여
public void prototypeBean(){ AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class); PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class); PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class); } } @Scope("prototype") static class PrototypeBean{ @PostConstruct public void init(){ //.. } @PreDestroy public void destroy(){ //.. } }
- init만 작동하고, destroy는 작동하지 않는다.
- 빈을 두번 조회했음 -> 두 개의 서로다른 빈 생성
- 따라서 조회한 빈 관리는 클라이언트 몫, 종료 메서드 호출도 클라이언트가 직접
2.3 프로토타입 스코프와 싱글톤 빈 함께 사용시 문제점
- 프로토타입의 의도는 요청시 마다 새로운 빈 생성 싱글톤과 같이 사용시 주의해야함
- 스프링 컨테이너에 프로토타입 빈 직접 요청시 잘 작동
- 싱글톤 빈에 프로토타입 빈을 주입받은 후 싱글톤 빈을 통해 프로토타입 빈을 활용한다면,
프로토타입 빈은 이미 생성된 후 관리를 싱글톤 빈에게 맡긴 것이나 다름없음
따라서 원하는대로 작동하지 않는다.
public class SingletonClientUsePrototype(){ AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class); ClientBean cli = ac.getBean(ClientBean.class); int c1 = cli.logic(); ClientBean cli = ac.getBean(ClientBean.class); int c2 = cli.logic(); } static class ClientBean{ private final PrototypeBean prototypeBean; public ClientBean(PrototypeBean prototypeBean){ this.prototypeBean = prototypeBean; } public int logic(){ prototypeBean.addCount(); return prototypeBean.getCount(); } } @Scope("prototype") static lcass PrototypeBean{ //.. }
* 여러 빈에서 같은 프로토타입 빈을 주입받으면, 각기 다른 프로토타입 빈을 주입받음
2.4 Provider를 통한 문제 해결
- Provider를 함께 사용하면, 지연을 발생시켜 원하는대로 사용할 수 있다.
@Autowired private ApplicationContext; public int logic(){ PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class); }
- Client를 위와 같이 고치면 getBean을 통해 항상 새로운 프로토타입 빈이 생성됨
- 위와 같이 외부에서 주입받는게 아니라, 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL)
의존관계 탐색이라고 한다. (빈 정보는 이미 넘겼고, 의존관계 따로 설정하지않고, 필요시 불러와서 사용)
- 하지만, 위와 같이 전체 컨텍스트를 주입받으면, 테스트가 어렵고, 스프링 컨테이너에 종속적인 코드가 됨
- DL기능만 제공해주는 것 사용하자
> ObjectFactory, ObjectProvider
- 스프링에서 DL을 대신해주는 객체 (ObjectFactory에서 편의기능 추가 -> Provider)
@Autowired private ObjectProvider<PrototypeBean> prototypeBeanProvider; public int logic() { PrototypeBean prototypeBean = prototypeBeanProvider.getObject(); //.. }
- ObjectProvider의 getObject를 호출하면, 내부에서 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.
- 단위테스트, mock코드 만들기 쉬워짐
- 별도 라이브러리 필요없음, 스프링에 의존
> JSR-330 Provider
- 자바 표준 JSR-330 사용하는 방법이다.
- 별도 라이브러리가 필요하다.
@Autowired private Provider<PrototypeBean> provider; public int logic() { PrototypeBean prototypeBean = provider.get(); //.. }
- ObjectProvider와 동일하게 작동한다.
- 별도의 라이브러리가 필요하지만, 자바 표준이므로 스프링이 아닌 다른 컨테이너에서 사용가능
* 스프링 사용하다보면, 스프링 제공 기능 vs 표준 기능이 겹칠때가 있다. 스프링이 더 다양하고 편리한 기능을 제공하므로, 특별히 다른 컨테이너를 사용할 일이 없다면, 스프링 제공기능을 사용하자
* 컴포넌트 스캔이랑 오토와이어드가 항상 같이 사용해야하는 그런건 아님, 오토와이어드는 단지 의존관계 자동으로 설정해주는 것을 뿐임 수동 빈등록 후에도 사용할 수 있음
* 순환 참조에서 발생하는 문제 해결 가능하다 -> A와 B가 서로 의존 but 필요시기가 다름 -> Provider사용
2.5 웹 스코프
- 스프링 빈의 웹 스코프에 대해 알아보자
> 웹 스코프의 특징
- 웹 환경에서만 작동함
- 프로토 타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리해준다 (종료메서드 호출해준다)
> 웹 스코프 종류
- request,session,application,websoket
> request 스코프
- 클라이언트 요청 별로 전용 bean이 생성되고, 요청의 주기와 함께 소멸
- 요청을 구분할 수 있음 (클라이언트 구분하는 거 아님)
@Controller @RequiredArgsConstructor public class LogController{ private final LogDemoService logDemoService; private final MyLogger mylooger; // requestScope로 빈에 등록된 객체 @RequestMapping("log-demo") @ResponseBody public String logDemo(HttpServletRequest request){ String requestURL = request.getRequestURL().toString(); myLogger.setRequestURL(requestURL); myLooger.log("test"); logDemoService.logic("testid"); return "OK"; } } @Service @RequiredArgsConstructor public class LogDemoService { private final MyLogger myLogger; public void logic(String id) { myLogger.log("service id = " + id); } }
- 비지니스 로직이 있는 서비스 계층에도 로그를 출력
- 여기서 중요한 점은 request 스코프를 사용하지 않고, 파라미터로 모든 정보를 넘긴다면,
파라미터가 지저분해진다.
- 또한 requestURL같은 웹 관련 정보가 웹과 관련없는 서비스 계층까지 넘어간다.
웹과 관련된 부분은 컨트롤러까지만 사용하자, 서비스 계층은 웹 기술에 종속되지 않고, 순수하게 유지하는 것이 좋다.
> 위 예제를 실행하면 오류가 발생한다 그 이유는 requestScope인 MyLogger때문이다.
컨트롤러에 의존관계를 주입하려고 하는데, 스프링 애플리케이션 실행 시점에는 싱글톤 빈은
생성해서 주입이 가능하지만, request 스코프 빈은 아직 생성되지 않는다 -> 고객 요청이 있어야함
2.6 Provider와 프록시
- 위 문제를 해결하기 위한 방법은 Provider와 프록시를 활용하는 것이다.
> Provider
private final ObjectProvider<MyLogger> myLoggerProvider; @RequestMapping("log-demo") @ResponseBody public String logDemo(HttpServletRequest request) { //.. MyLogger myLogger = myLoggerProvider.getObject(); //요청시점에 얻을 수 있음 //.. }
- ObjectProvider 덕분에 호출시점까지 빈의 생성을 지연할 수 있다.
- getObject() 호출 시점에서는 요청이 진행중이므로 request scope빈이 정상 처리됨
- getObject를 컨트롤러와 서비스 객체에서 각각 따로 호출해도 같은 HTTP요청이면 같은 스프링 빈이 반환됨
> 프록시
@Component @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) public class MyLogger { //.. }
- proxyMode를 사용해보자 (인터페이스면 INTERFACES선택)
- 이렇게 하면, 의존관계 주입 시점에 MyLogger에 가짜 프록시 클래스가 주입된다.
- CGLIB(바이트 코드 조작 라이브러리)가 MYLogger를 상속받은 가짜 프록시 객체 만들어서 주입
- 스프링 컨테이너에서 myLooger라는 이름으로 진짜 대신 가짜가 등록된다.
- 가짜 프록시 객체는 요청이 오면 그떄 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
- 클라이언트가 myLooger.log()를 호출-> 가짜 프록시 객체 메서드 호출
가짜 프록시가 진짜 메서드 호출
다형성을 통해 클라이언트는 원본인지 아닌지 구분할 수 없음
*Provider와 프록시의 핵심은 진짜 객체 조회가 필요한 시점까지 지연처리 한다는 것
단지 어노테이션 설정만으로 이게 가능한 것이 다형성과 DI컨테이너의 큰 장점이다.
* 마치 싱글톤 사용하는 것 같지만 다르게 동작 주의하고, 특별한 경우에만 사용하자
'Web > Spring' 카테고리의 다른 글
SpringMVC (2) - 서블릿 (0) 2023.10.02 스프링 MVC (1) 웹 애플리케이션 이해 (0) 2023.09.04 스프링 핵심 원리 (4) - 컴포넌트 스캔 (0) 2023.08.18 스프링 핵심 원리 (3) - 싱글톤 컨테이너 (0) 2023.08.17 스프링 핵심 원리 (2) - 스프링 컨테이너와 스프링 빈 (0) 2023.08.17