Web/Spring

스프링 핵심 원리 (5) - 빈 생명주기

now0204 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컨테이너의 큰 장점이다.

 

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