ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링 핵심 원리 (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컨테이너의 큰 장점이다.

     

    * 마치 싱글톤 사용하는 것 같지만 다르게 동작 주의하고, 특별한 경우에만 사용하자 

Designed by Tistory.