빈 생명주기 콜백과 빈 스코프

현민·2022년 1월 24일
0

스프링 공부

목록 보기
9/9

인프런 김영한님의 스프링 강의를 듣고 정리한 내용입니다. 출처


빈 생명주기 콜백

데이터베이스 커넥션 풀이나 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 하려면, 객체의 초기화와 종료 작업이 필요하다.

스프링 빈은 객체를 생성하고 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용 할 수 있는 준비가 완료된다. 따라서 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야 한다.

스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공한다. 또한 스프링은 스프링 컨테이너가 종료되기 전에 소멸 콜백을 준다. 따라서 안전하게 종료 작업을 진행 할 수 있다.

스프링 빈의 이벤트 라이프사이클은 다음과 같다.
스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸전 콜백 -> 스프링 종료

초기화 콜백 : 빈이 생성되고 빈의 의존관계 주입이 완료된 후 호출
소멸전 콜백 : 빈이 소멸되기 직전에 호출

  • 객체의 생성과 초기화를 분리하자.
    생성자는 필수 정보를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다. 반면에 초기화는 이렇게 생성된 값을 활용해서 외부 커넥션을 연결하는등 무거운 동작을 수행한다. 따라서 객체를 생성하는 부분과 초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋다.

빈 생명주기 콜백 방법

스프링은 크게 3가지 방법으로 빈 생명주기 콜백을 지원한다.

  • 인터페이스 (InitializingBean, DisposableBean)
public class NetworkClient implements InitializingBean, DisposableBean {
    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);
    }
    @Override
    public void afterPropertiesSet() throws Exception {
        connect();
        call("초기화 연결 메시지");
    }
    @Override
    public void destroy() throws Exception {
        disConnect();
    }
}

InitializingBeanafterpropertiesSet() 메서드로 초기화를 지원한다.
DisposableBeandestroy() 메서드로 소멸을 지원한다.

이 방식은 초기화, 소멸 메서드의 이름을 변경할 수없고, 내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없기 때문에 거의 사용하지 않는다.

  • 빈 등록 초기화, 소멸 메서드 지정
@Bean(initMethod = "init", destroyMethod = "close")

설정 정보에 위 코드처럼 초기화, 소멸 메서드를 지정해줄 수있다.
메서드 이름을 자유롭게 줄 수 있고, 스프링 빈이 스프링 코드에 의존하지 않고, 코드를 고칠 수 없는 외부 라이브러리에도 적용가능하다.

또한 @Bean의 destroyMethod 속성에는 기본값이 (inferred)로 등록되어 있고, 이 기능은 close, shutdown의 이름을 가진 메서드를 자동으로 호출해준다. 따라서 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 동작한다.

  • 애노테이션 @PostConstruct, @preDestory
    @PostConstruct
    public void init() {
        connect();
        call("초기화 연결 메세지");
    }

    @PreDestroy
    public void close() {
        disconnect();
    }

최신 스프링에서 가장 권장하는 방법으로, 애노테이션 하나만 붙이면 되기때문에 매우 편리하다.
유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다. 외부 라이브러리를 초기화,종료 해야하면 @Bean의 기능을 사용하자.

빈 스코프

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

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

  • 싱글톤 : 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프

  • 프로토타입 : 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프

  • 웹 관련 스코프
    request : 웹 요청이 들어오고 나갈 때 까지 유지되는 스코프
    session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
    application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
    websocket : 웹 소켓과 동일한 범위로 유지되는 스코프

빈 스코프는 다음과 같이 지정할 수 있다.

// 컴포넌트 스캔 자동 등록
@Scope("prototype")
@Component
public class HelloBean {}

//수동 등록
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
 return new HelloBean();
}

프로토타입 스코프

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

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

프로토타입 스코프를 싱글톤 빈과 함께 사용시 문제점

스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환한다. 하지만 싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않으므로 주의해야 한다.

위 그림처럼 싱글톤 빈에서 프로토타입 빈을 사용하게되면 문제가 생기게된다.

싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에 프로토타입 빈이 싱글톤 빈과 함께 계속 유지되는 문제가 생긴다.

하지만 이렇게 프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라 사용할 때 마다 새로 생성해서 사용하는 것을 원할 것이다.

Provider로 문제 해결

의존관계를 외부에서 주입(DI) 받는게 아니라 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL) 이라고 한다.

지금 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 DL정도의 기능만 제공하는 무언가가 있으면 된다.

  • ObjectFactory, ObjectProvider
	@Autowired
	private ObjectProvider<PrototypeBean> prototypeBeanProvider;

    public int logic() {
		PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
		prototypeBean.addCount();
		return prototypeBean.getCount();
	}

prototypeBeanProvider.getObject()를 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.

ObjectProvidergetObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)

ObjectFactory : 기능이 단순하고 스프링에 의존함.
ObjectProvider: ObjectFactory를 상속. 옵션, 스프림 처리등 편의 기능이 많음. 스프링에 의존함.

  • JSR-330 Provider
	@Autowired
	private Provider<PrototypeBean> provider;

    public int logic() {
		PrototypeBean prototypeBean = provider.get();
		prototypeBean.addCount();
		return prototypeBean.getCount();
	}

javax.inject.Provider라는 JSR-330 자바 표준을 이용하는 방법이다.
사용하려면 javax.inject:javax.inject:1 라이브러리를 gradle에 추가해야한다. 자바 표준이므로 스프링이 아닌 다른 컨테이너에도 사용할 수 있다.

웹 스코프

웹 스코프는 웹 환경에서만 동작하고, 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리하기때문에 종료 메서드가 호출된다.

request 스코프

request 스코프는 위 그림처럼 HTTP request 요청 당 각각 할당된다.

동시에 여러 HTTP 요청이 오면 어떤 요청이 남긴 로그인지 구분하기 어렵고 이럴때 사용하기 좋은 것이 request 스코프이다.

@Component
@Scope(value = "request")
public class MyLogger {
    private String uuid;
    private String requestURL;

    public void log(String message) {
        System.out.println("[" + this.uuid + "]" + "[" + this.requestURL + "] " + message);
    }
    
    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    @PostConstruct
    public void init() {
        this.uuid = UUID.randomUUID().toString();
        System.out.println("[" + this.uuid + "] request scope bean create: " + this);
    }

    @PreDestroy
    public void destroy() {
        System.out.println("[" + this.uuid + "] request scope bean close: " + this);
    }
}

@Scope(value = "request")를 사용해서 request 스코프로 지정하고, 생성되는 시점에 자동으로 초기화 메서드를 사용해 uuid를 생성해서 저장해둔다.
이 빈은 HTTP 요청당 하나씩 생성되므로 uuid를 저장해두면 다른 HTTP 요청과 구분할 수 있다.

@Controller
@RequiredArgsConstructor
public class LogDemoController {
    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> myLoggerObjectProvider;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        MyLogger myLogger = myLoggerObjectProvider.getObject();
        String requestURL = request.getRequestURL().toString();
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        logDemoService.logic("test id");
        return "OK";
    }
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final ObjectProvider<MyLogger> myLoggerObjectProvider;

    public void logic(String id) {
        MyLogger myLogger = myLoggerObjectProvider.getObject();
        myLogger.log("service id = " + id);
    }
}

스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 웹 요청이 들어올 때 생성되기 때문에 MyLogger 빈에 Provider를 사용해서 ObjectProvider.getObject()를 호출하는 시점까지 빈의 생성을 지연 할 수 있다.

스코프와 프록시

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}

Provider를 사용하지 않고 위 문제를 해결할 수 있는 방법이다.
proxyMode = ScopedProxyMode.TARGET_CLASS를 추가해 MyLogger의 가짜 프록시 클래스를 만들어주고 HTTP request 와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수있다.

이렇게 하면 provider를 사용하지 않아도 의존관계 주입시 CGLIB라는 라이브러리를 통해 MyLogger를 상속받은 가짜 프록시 객체를 만들어 주입하게 된다.

프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 있다. 하지만 싱글톤과 다르게 동작하기 때문에 주의해서 사용해야한다.

0개의 댓글