본문 바로가기
카테고리 없음

230221 TIL

by hbIncoding 2023. 2. 21.

1. 빈 생명주기 콜백

  1) 빈 생명 주기 콜백

  • 스프링 빈은 객체를 생성 후 의존관계를 주입한 뒤 사용할 준비가 완료된다.
  • 해당 빈에서 초기화 작업들을 해주고 싶다면 이런 의존관계가 모두 지입된 다음 호출해야 한다.
  • 개발자 입장에서 의존관계가 모두 주입이 완료되는 시점을 알기 위해서 스프링에서는 스프링 빈이 의존관계 주입이 완료되면 콜백 메서드를 통해 초기화 시점을 알려주는 기능을 제공한다.
  • 더하여 스프링 컨테이너의 소멸 직전 소멸 콜백을 주어서 스프링 컨테이너가 종료되기 전 로직을 수행할 수 있다.

  2) 스프링 빈의 이벤트 라이프 사이클

  • 초기화 콜백 : 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
  • 소멸전 콜백 : 빈이 소멸되기 직전 호출
  • 객체의 생성과 초기화를 분리하자 : 생성자는 필수 정보를 받아 메모리 할당 후 객체를 생성하는 책임을 가진다. 그리고 초기화는 이렇게 생성된 값들을 활용해 기타 무거운 작업들을 수행한다. 기렇기에 생성과 초기화를 묶는 것은 SRP적으로도 과한 책익을 가지게 되기에 명확하게 두 부분을 나누는 것이 유지보수관점 및 OOP 적으로도 좋다. 
  • 싱글톤 빈들은 스프링 컨테이너가 종료될 때 같이 종료되기에 소멸전 콜백이 스프링 컨테이너 종료 직전 호출되지만, 생명주기가 더 짧은 빈들도 있는데 이 빈들은 컨테이너와 무관하게 소멸전 콜백이 일어난다.
  • 그럼 스프링은 빈 생명주기 콜백을 어떻게 관리하는가?
    • 인터페이스(InitializingBean, DisposableBean)
    • 설정 정보에 초기화 메서드, 종료 메서드 지정(에너테이션 속성 설정)
    • @PostConstrucotr, @PreDestroy 사용

  3) 인터페이스 (InitializingBean , DisposableBean)

package hello.core.lifecycle;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

public class NetworkClient implements InitializingBean, DisposableBean {

    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출 , url=" + url);
        connect();
        call("초기화 연결 메세지");
    }

    public void call(String msg) {
        System.out.println("call= " + url + " message= " + msg);
    }

    public void connect() {
        System.out.println("connect= " + url);
    }

    public void disconnect() {
        System.out.println("close= " + url);
    }

    public void setUrl(String url) {
        this.url = url;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        connect();
        call("초기화 연결 메세지");
    }

    @Override
    public void destroy() throws Exception {
        disconnect();
    }
}
  • 테스트 실행 결과

  • InitializiingBean은 afterPropertiesSet() 메서드로 초기화를 지원한다.
  • DisposableBean은 destry()메서드로 소멸을 지원한다.
  • 초기화가 메서드 의존관계 주입 완료 이후 호출되었기에 Null이 아닌 제대로 된 url이 출력된다.
  • 스프링 컨테이너가 종료되는 시점에서 disconnect도 호출되는 것을 확인 할 수 있다.
  • 해당 인터페이스 두 개를 구현하면서 두 메서드만 오버라이딩 해서 구현하면 되는 점은 편하지만, 이 인터페이스는 스프링 전용 인터페이스이기 때문에 해당 인터페이스에 의존해게 된다.
  • 그리고 두 인터페이스에서 제공하는 메서드 오버라이딩이기 때문에 이름을 변경할 수 없고 외부 라이브러리에는 적용할수도 없다.

  4) 빈 등록 초기화, 소멸 메서드

  • 스프링 빈 등록 애노테이션@Bean에 속성으로 초기화, 소멸 메서드를 지정할 수 있다.
    • @Bean(initMethod = "초기화메서드명", destryMethod="소멸메서드명")
  • NetworkClient > 초기화 메서드 init과 소멸 메서드 close를 구현해주었다.
public class NetworkClient{

    ...

    public void init() {
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메세지");
    }

    public void close() {
        System.out.println("NetworkClient.close");
        disconnect();
    }
}
  • LifeCycleConfig > 설정 정보 클래스에서 Bean으로 등록되는 부분에 속성으로 initMethod와 소멸 메서드를 설정
@Configuration
static class LifeCycleConfig {
    @Bean(initMethod = "init", destroyMethod = "close")
    public NetworkClient networkClient() {
        NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl("http://hello-spring.dev");

        return networkClient;
    }
}
  • 실행 결과
    • 인터페이스를 사용하는 것과는 다르게 이름도 init, close로 개발자가 임의로 지정할 수 있고, 스프링 빈이 스프링 코드(인터페이스)에 의존하지 않는다. 그리고 코드가 아닌 설정 정보를 이용하기 때문에 외부 라이브러리를 사용할지라도 적용이 가능하다.

  • 종료 메서드 추론
    • @Bean애노테이션의 소멸메서드를 지정하는 속성인 destroyMethod는 추론기능이 있다.
    • 관례적으로 대부분 라이브러리에서는 close, shutdown 이라는 이름으로 소멸 메서드를 사용하기때문에, @Bean 의 destroyMethod는 default 값으로 (inferred)(추론)으로 등록되어 있는데, 이 기능은 close, shutdown이라는 이름의 메서드를 자동으로 호출해준다.
    • 내가 스프링 빈을 등록할때 종료 메서드를 해당 관례를 따르는 close나 shutdown으로 지정한다면 따로 속성에 destroyMethod에 지정해주지 않아도, 자동으로 찾아서 동작한다.
    • 만약, 추론 기능을 사용하기 싫을경우 공백("")을 지정하면 된다.

5) @PostConstruct, @PreDestroy

  • 가장 편하고 최슨 스프링에서 권장하는 방법
  • 초기화 메서드에 @PostConstruct를 지정하면 스프링 빈 등록시 초기화 메서드로 수행
  • 소멸에서드에 @PreDestroy를 지정하면 소멸 메서드로 지정
  • 애노테이션 하나만 붙히면 되기에 상당히 편하다.
  • 해당 애노테이션의 import 패키지를 보면 javax.annotation.PostConstruct 이다. 즉 스프링에 종속적인 기술이 아닌 JSR-250 자바 표준이다.
  • 컴포넌트 스캔과 잘 어울린다.
  • 하지만, 외부 라이브러리에는 적용하지 못하기에(변경 불가로 인해) 이런 경우에는 @Bean의 기능(속성을 부여)을 이용하면 된다.
public class NetworkClient{
    ...

    @PostConstruct
    public void init() {
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메세지");
    }

    @PreDestroy
    public void close() {
        System.out.println("NetworkClient.close");
        disconnect();
    }
}

 

2. 빈 스코프

  1) 빈 스코프란?

  • 지금까지 우리는 스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료될 때
    까지 유지된다고 학습했다. 이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. 스코프는
    번역 그대로 빈이 존재할 수 있는 범위를 뜻한다.
  • 싱글톤 : 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
  • 프로토타입 : 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는
    매우 짧은 범위의 스코프이다.
  • 웹관련 스코프
    • request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프이다.
    • session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프이다.
    • application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프이다.
  • 빈스코프 지정방법
1. 컴포넌트 스캔 자동 등록
@Scope("prototype")
@Component
public class HelloBean { }

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

  2) 프로토타입 스코프

  • 싱글톤 빈 요청
    1. 싱글톤 스코프의 빈을 스프링 컨테이너에 요청
    2. 스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환
    3. 이후에 스프링 컨테이너에 같은 요청이 와도 같은 객체 인스턴스의 스프링 빈을 반환

  • 프로토타입 빈 요청
    1. 프로토타입 스코프의 빈을 스프링 컨테이너에 요청
    2. 스프링 컨테이너는 이 시점에서 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.

  • 프로토타입 빈 요청2
    1. 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환한다.
    2. 이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성 후 반환

  • 프로토타입 빈의 특징
    • 스프링 컨테이너의 요청할 때 마다 새로 생성
    • 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입 그리고 초기화만 관여
    • 종료 메서드가 호출되지 않는다.
    • 그래서 프로토타입 빈은 프로토타입 빈을 조회한 클라이언트가 관리해야한다. 종료 메서드에 대한 호출도 클라이언트가 직접 해야한다.

  ㄱ. 싱글톤 빈과 함께 사용 시 문제점

  • 스프링 컨테이너에 프로토타입 빈 직접 요청 
    1. 클라이언트A는 스프링 컨테이너에 프로토타입 빈을 요청한다.
    2. 스프링 컨테이너는 프로토타입 빈을 새로 생성해서 반환(x01)한다. 해당 빈의 count 필드 값은 0이다.
    3. 클라이언트는 조회한 프로토타입 빈에 addCount() 를 호출하면서 count 필드를 +1 한다.
    4. 결과적으로 프로토타입 빈(x01)의 count는 1이 된다.

  • 스프링 컨테이너에 프로토타입 빈 직접 요청2
    1. 클라이언트B는 스프링 컨테이너에 프로토타입 빈을 요청한다.
    2. 스프링 컨테이너는 프로토타입 빈을 새로 생성해서 반환(x02)한다. 해당 빈의 count 필드 값은 0이다.
    3. 클라이언트는 조회한 프로토타입 빈에 addCount() 를 호출하면서 count 필드를 +1 한다.
    4. 결과적으로 프로토타입 빈(x02)의 count는 1이 된다.

  • 싱글톤 빈에서 프로토타입 빈 사용1
    1. clientBean 은 싱글톤이므로, 보통 스프링 컨테이너 생성 시점에 함께 생성되고, 의존관계 주입도 발생한다.
      • 1)clientBean 은 의존관계 자동 주입을 사용한다. 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청 한다.
      • 2)스프링 컨테이너는 프로토타입 빈을 생성해서 clientBean 에 반환한다. 프로토타입 빈의 count 필드 값은 0이다.
    2. 이제 clientBean 은 프로토타입 빈을 내부 필드에 보관한다. (정확히는 참조값을 보관한다.)

  • 싱글톤 빈에서 프로토타입 빈 사용2
    1. 클라이언트 A는 clientBean 을 스프링 컨테이너에 요청해서 받는다.싱글톤이므로 항상 같은 clientBean 이 반환된다.
      • 3)클라이언트 A는 clientBean.logic() 을 호출한다.
      • 4)clientBean  prototypeBean의 addCount() 를 호출해서 프로토타입 빈의 count를 증가한다.
    2. count값이 1이 된다.

  • 싱글톤 빈에서 프로토타입 빈 사용3
    1. 클라이언트 B는 clientBean 을 스프링 컨테이너에 요청해서 받는다.싱글톤이므로 항상 같은 clientBean 이 반환된다.
    2. 여기서 중요한 점이 있는데, clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다. 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지, 사용 할 때마다 새로 생성되는 것이 아니다!
      • 5)클라이언트 B는 clientBean.logic() 을 호출한다.
      • 6)clientBean 은 prototypeBean의 addCount() 를 호출해서 프로토타입 빈의 count를 증가한다.
    3. 원래 count 값이 1이었으므로 2가 된다.
  • 스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 된다.
  • 그런데 싱글톤 빈은 생성 시점에만 의존관계를 주입받기 때문에, 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지되는 것이 문제다.
    • 원하는 것은 프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라, 사용할 때 마다 새로 생성해서 사용하는 것을 원할 것이다.

 

 

 

 

  ㄴ. 싱글톤 빈과 함께 사용 시 Provider로 문제 해결 방법 3가지

  • 스프링 컨테이너에 요청
    • 가장 간단한 방법으로 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청하는 것
    • 아래 코드를 실행해 보면 ac.getBean()을 통해 항상 새로운 프로토타입 빈이 생성되는 것을 확인
    • 의존관계를 외부에서 주입(DI) 받는게 아니라 이렇게 직접 필요한 의존 관계를 찾는 것을 Dependency Lookup(DL) 의존관계 조회(탐색) 이라고 한다.
    •  그런데 이렇게 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다.
    • 지금 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 딱! DL 정도의 기능만 제공하는 무언가가 있으면 된다.
@Scope("singleton")
static class ClientBean {
    @Autowired
    AnnotationConfigApplicationContext applicationContext;

    public int logic() {
        PrototypeBean prototypeBean = applicationContext.getBean(PrototypeBean.class);
        prototypeBean.addCount();
        return prototypeBean.getCount();
    }
}
  • ObjectFactory, ObjectProvider
    • 지정한 빈 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 ObjectProvider이다.
    • 과거에는 ObjectFactory가 있었는데 여기에 편의기능이 추가되어 ObjectProvider가 되었다.
    • 실행해보면 prototypeBeanProvider.getObject() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
    • ObjectProvider  getObject() 를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)
    • 스프링이 제공하는 기능을 사용하지만, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는
      훨씬 쉬워진다.
    • ObjectProvider 는 지금 딱 필요한 DL 정도의 기능만 제공한다.
      • - > ObjectProvider의 핵심컨셉: 스프링 컨테이너에서 Bean을 대신 조회해주는 용도
    • 특징
      • ObjectFactory: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존
      • ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존
@Test
void singletonClientUsePrototype() {
    AnnotationConfigApplicationContext applicationContext =
            new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
    ClientBean clientBean1 = applicationContext.getBean(ClientBean.class);
    int count1 = clientBean1.logic();
    assertThat(count1).isEqualTo(1);

    ClientBean clientBean2 = applicationContext.getBean(ClientBean.class);
    int count2 = clientBean2.logic();
    assertThat(count2).isEqualTo(1);
}

@Scope("singleton")
static class ClientBean {
    @Autowired
    ObjectProvider<PrototypeBean> prototypeBeanProvider;

    public int logic() {
        PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
        prototypeBean.addCount();
        return prototypeBean.getCount();
    }
}
  • JSR-330 Provider
    • javax.inject.Provider 라는 JSR-330 자바 표준을 사용하는 방법
    • 이 방법을 사용하려면 javax.inject:javax.inject:1 라이브러리를 gradle에 추가해야 한다.
    • 실행해보면 provider.get() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
    • provider  get() 을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)
    • 자바 표준이고, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.
    • Provider 는 지금 딱 필요한 DL 정도의 기능만 제공한다.
    • 특징
      • get() 메서드 하나로 기능이 매우 단순하다.
      • 별도의 라이브러리가 필요하다.
      • 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
implementation 'javax.inject:javax.inject:1'
@Scope("singleton")
static class ClientBean {
    @Autowired
    Provider<PrototypeBean> prototypeBeanProvider;

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

ㄷ. 정리

  • 프로토타입 빈은 매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용하면 된다.
  • 하지만 실무에서 웹 애플리케이션을 개발해보면, 싱글톤빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드믈다. 

 3) 웹스코프

 ㄱ. 특징

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

 ㄴ. 종류

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

 ㄷ. HTTP request 요청 당 각각 할당되는 request 스코프

  • 클라이언트 A가 컨트롤러에 요청을 한다.
  • 컨트롤러가 Request scope와 관련된 객체를 조회 한다. (ex. myLogger)
  • 클라이언트 A의 전용 객체가 만들어진다.

 ㄹ. request 스코프 예제

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

다음과 같이 로그가 남도록 request 스코프를 활용해서 추가 기능을 개발해보자.

[d06b992f...] request scope bean create
[d06b992f...][http://localhost:8080/log-demo] controller test
[d06b992f...][http://localhost:8080/log-demo] service id = testId
[d06b992f...] request scope bean close
  • 기대하는 공통 포멧: [UUID][requestURL] {message}
  • UUID를 사용해서 HTTP 요청을 구분하자.
  • requestURL 정보도 추가로 넣어서 어떤 URL을 요청해서 남은 로그인지 확인하자.

예제 코드

@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 + "]" + "[" + requestUrl + "] request scope bean create : " + this);
    }

    @PreDestroy
    public void destroy() {
        System.out.println("[" + uuid + "]" + "[" + requestUrl + "] request scope bean close : " + this);
    }
}
  • 로그를 출력하기 위한 MyLogger 클래스이다.
  • @Scope(value = "request") 를 사용해서 request 스코프로 지정했다.
    → 이제 이 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸된다.
  • 이 빈이 생성되는 시점에 자동으로 @PostConstruct 초기화 메서드를 사용해서 uuid를 생성해서 저장해 둔다.
  • 이 빈은 HTTP 요청 당 하나씩 생성되므로, uuid를 저장해두면 다른 HTTP 요청과 구분할 수 있다.
  • 이 빈이 소멸되는 시점에 @PreDestroy 를 사용해서 종료 메시지를 남긴다.
  • requestURL 은 이 빈이 생성되는 시점에는 알 수 없으므로, 외부에서 setter로 입력 받는다.

LogDemoController

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;

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

        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}
  • 로거가 잘 작동하는지 확인하는 테스트용 컨트롤러다.
  • 여기서 HttpServletRequest를 통해서 요청 URL을 받았다.
  • requestURL 값: http://localhost:8080/log-demo
  • 이렇게 받은 requestURL 값을 myLogger에 저장해둔다. myLogger는 HTTP 요청 당 각각 구분되므로 다른 HTTP 요청 때문에 값이 섞이는 걱정은 하지 않아도 된다.
  • 컨트롤러에서 controller test라는 로그를 남긴다.

LogDemoService

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;

    public void logic(String testId) {
        myLogger.log("service id = " + testId);
    }
}
  • 비즈니스 로직이 있는 서비스 계층에서도 로그를 출력해보자.
  • 여기서 중요한점이 있다. request scope를 사용하지 않고 파라미터로 이 모든 정보를 서비스 계층에 넘긴다면, 파라미터가 많아서 지저분해진다. 더 문제는 requestURL 같은 웹과 관련된 정보가 웹과 관련없는 서비스 계층까지 넘어가게 된다. 웹과 관련된 부분은 컨트롤러까지만 사용해야 한다. 서비스 계층은 웹 기술에 종속되지 않고, 가급적 순수하게 유지하는 것이 유지보수 관점에서 좋다.
  • request scope의 MyLogger 덕분에 이런 부분을 파라미터로 넘기지 않고, MyLogger의 멤버변수에 저장해서 코드와 계층을 깔끔하게 유지할 수 있다.

실제는 기대와 다르게 애플리케이션 실행 시점에 오류 발생

Error creating bean with name 'myLogger': Scope 'request' is not active for the
current thread; consider defining a scoped proxy for this bean if you intend to
refer to it from a singleton;

스프링 애플리케이션을 실행 시키면 오류가 발생한다. 메시지 마지막에 싱글톤이라는 단어가 나오고...
스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 아직 생성되지 않는다.
이 빈은 실제 고객의 요청이 와야 생성할 수 있다!

  4) 스코프와 Provider

첫번째 해결방안은 앞서 배운 Provider를 사용하는 것이다.

간단히 ObjectProvider를 사용해보자.

@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 testId) {
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log("service id = " + testId);
    }
}

→ MyLogger를 주입 받는 것이 아닌, MyLogger를 찾을 수 있는 (Dependency Lookup) Object Provider가 주입 된다!

main() 메서드로 스프링을 실행하고, 웹 브라우저에 http://localhost:8080/log-demo 를 입력하자.
드디어 잘 작동하는 것을 확인할 수 있다.

[9ecb568d-f6c9-4956-96c0-e96d0fb535ae][null] request scope bean create : hello.core.common.MyLogger@ae3b68d
[9ecb568d-f6c9-4956-96c0-e96d0fb535ae][http://localhost:8080/log-demo] controller test
[9ecb568d-f6c9-4956-96c0-e96d0fb535ae][http://localhost:8080/log-demo] service id = testId
[9ecb568d-f6c9-4956-96c0-e96d0fb535ae][http://localhost:8080/log-demo] request scope bean close : hello.core.common.MyLogger@ae3b68d
  • ObjectProvider 덕분에 ObjectProvider.getObject() 를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다. → 스프링 컨테이너로의 빈의 요청을 지연
  • ObjectProvider.getObject() 를 호출하시는 시점에는 HTTP 요청이 진행중이므로 request scope 빈의 생성이 정상 처리된다.
  • ObjectProvider.getObject()  LogDemoController , LogDemoService 에서 각각 한번씩 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환된다!

  5) 스코프와 프록시

이번에는 프록시 방식을 사용해보자.

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
		...
}
  • 여기가 핵심이다. proxyMode = ScopedProxyMode.TARGET_CLASS 를 추가해주자.
    • 적용 대상이 인터페이스가 아닌 클래스면 TARGET_CLASS 를 선택
    • 적용 대상이 인터페이스면 INTERFACES 를 선택
  • 이렇게 하면 MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다.

웹 스코프와 프록시의 동작 원리

먼저 주입된 myLogger를 확인해보자.

System.out.println("myLogger = " + myLogger.getClass());

출력결과

myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d
  • CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.
  • @Scope  proxyMode = ScopedProxyMode.TARGET_CLASS) 를 설정하면 스프링 컨테이너는 CGLIB라는 바이트코드를 조작하는 라이브러리를 사용해서, MyLogger를 상속받은 가짜 프록시 객체를 생성한다.
  • 결과를 확인해보면 우리가 등록한 순수한 MyLogger 클래스가 아니라 MyLogger$$EnhancerBySpringCGLIB 이라는 클래스로 만들어진 객체가 대신 등록된 것을 확인할 수 있다.
  • 그리고 스프링 컨테이너에 "myLogger"라는 이름으로 진짜 대신에 이 가짜 프록시 객체를 등록한다.
  • ac.getBean("myLogger", MyLogger.class) 로 조회해도 프록시 객체가 조회되는 것을 확인할 수 있
    다.
  • 그래서 의존관계 주입도 이 가짜 프록시 객체가 주입된다.

가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.

  • 가짜 프록시 객체는 내부에 진짜 myLogger를 찾는 방법을 알고 있다.
  • 클라이언트가 myLogger.logic() 을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이다.
  • 가짜 프록시 객체는 request 스코프의 진짜 myLogger.logic() 를 호출한다.
  • 가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 원본인지 아닌지도 모르게, 동일하게 사용할 수 있다 → (다형성)

동작 정리

  • CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.
  • 이 가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다.
  • 가짜 프록시 객체는 실제 request scope와는 관계가 없다. 그냥 가짜이고, 내부에 단순한 위임 로직만 있고, 싱글톤 처럼 동작한다.

특징 정리

  • 프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 있
    다.
  • 사실 Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연 처리 한다는 점이다.
  • 단지 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 이것이 바로 다형성과 DI 컨테이너가 가진 큰 강점이다.
  • 꼭 웹 스코프가 아니어도 프록시는 사용할 수 있다.

주의점

  • 마치 싱글톤을 사용하는 것 같지만 다르게 동작하기 때문에 결국 주의해서 사용해야 한다.
  • 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자, 무분별하게 사용하면 유지보수하기 어려워진다.

언제 request scope를 사용하는가?

request scope는 강의에서 설명드린 것 처럼 대안이 있으면 가급적이면 사용하지 않는게 좋다.
그래도 비즈니스 상황에 따라서 이것을 사용하면 편리한 경우들이 있다.

request scope 자체가 HTTP 요청 정보의 내용을 편리하게 다룰 수 있기 때문에, 이 요청 정보를 공통화해서 객체로 만들어두고, 공통으로 로그 처리를 하거나 또는 외부 API를 호출할 때 요청서버에서 넘어온 정보를 함께 넘기거나 할 때 유용하게 사용할 수 있다.

예를 들어, 요청서버 → 현재서버 → 대상서버 구조인 경우, 요청서버에서 요청 id를 만들어 현재서버에 넘겼을 때 현재서버는 단순이 이 요청 id가 비즈니스 로직과는 전혀 상관이 없고, 로그용으로 필요하고, 또 대상 서버로 넘길 때 필요하다면 파라미터로 계속 가지고 다니기에는 부담스럽다. 이런 경우에 사용하면 비즈니스 로직을 전혀 손대지 않고, 공통 로그 처리, 외부 API에 파라미터 전달 등등 업무를 처리할 수 있다.

정리하면 request scope는 비즈니스 로직에 파라미터를 계속 들고다닐 필요 없이 공통 정보를 처리할 때 효과적이다.

 

3. 과제 진행

  1) LV3

  ㄱ. mappedby와 cascade 

  • 아래와 같이 게시글에 대하여 OneToMany에 옵션을 설정해 준다.
  • 단방향이 아닌 양방향 매핑을 해주고, 관계의 주인이 댓글이고 외래키가 댓글의 'blog'이다. 
    • mappedBy = "blog"에서 blog는 게시글쪽이 아닌 댓글쪽에서의 Column을 의미한다.
  • 그리도 cascade 옵션을 통해 삭제시 같이 삭제 되는 것을 알려준다.

 

4. 참조 

  1) 빈 생명주기 콜백 : https://catsbi.oopy.io/3a9e3492-f511-483d-bc65-183bb0c166b3

 

빈 생명주기 콜백

목차

catsbi.oopy.io

 2) Optional 쓰는 26까지 방법 : https://escapefromcoding.tistory.com/246

 

Optional을 제대로 사용하는 26가지 방법 ( 1 )

Item 1 : null을 Optional 변수에 할당하지 말 것 //잘못된 방법 public Optional fetchChart() { Optional emptyCart = null; ... } //옳은 방법 public Optional fetchCart() { Optional emptyCart = Optional.empty(); ... } Optional을 초기화하

escapefromcoding.tistory.com