티스토리 뷰

Spring

[Spring] 3. 스프링 빈

mandykr 2022. 4. 13. 16:53

목차

1. 빈 생명주기 콜백

2. 빈 생명주기 콜백 사용 방법

3. 빈 스코프

4. 웹 스코프

 

 

 

1. 빈 생명주기 콜백

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

객체의 초기화와 종료 작업이 필요하다.

 

예제로 이 NetworkClient 는 애플리케이션 시작 시점에 connect() 를 호출해서 연결을 맺어두어야 하고, 

애플리케이션이 종료되면 disConnect() 를 호출해서 연결을 끊어야 한다.

public class NetworkClient {

    private String url;
    
    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
        connect();
        call("초기화 연결 메시지");
    }
    
    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);
    }
}

 

객체의 생성과 초기화를 분리

생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다.

반면에 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는등 무거운 동작을 수행한다.

@Configuration
class LifeCycleConfig {

    @Bean
    public NetworkClient networkClient() {
        NetworkClient networkClient = new NetworkClient("http://hello-spring.dev");
        return networkClient;
    }
}

NetworkClient를 스프링 빈으로 등록할 때

위와 같이 생성자 안에서 무거운 초기화 작업을 함께 하는 것 보다는

객체를 생성하는 부분과 초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋다. 

NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");

물론 초기화 작업이 내부 값들만 약간 변경하는 정도로 단순한 경우에는 생성자에서 한번에 다 처리하는게 더 나을 수 있다.

 

빈 생명주기 콜백

객체의 생성과 초기화 부분을 분리하였을 때 스프링 빈의 라이프 사이클은 (생성자 주입 이외)

객체 생성 -> 의존관계 주입 순서이기 때문에 객체의 초기화 작업이 모두 완료되기 전,

객체 생성 단계(NetworkClient 생성자 호출)에서는 url 정보를 알 수 없다.

따라서 객체의 초기화가 완료된 시점을 알아야 url 정보가 초기화된 NetworkClient의 connect() 를 호출할 수 있다.

 

스프링은 의존관계 주입이 완료되면

스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공한다. 

또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 

따라서 안전하게 종료 작업을 진행할 수 있다.

 

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

스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸전 콜백 -> 스프링 종료

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

 

2. 빈 생명주기 콜백 사용 방법

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

  • 인터페이스(InitializingBean, DisposableBean)
  • 설정 정보에 초기화 메서드, 종료 메서드 지정
  • @PostConstruct, @PreDestroy 애노테이션 지원

 

 

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

InitializingBean : afterPropertiesSet() 메서드로 초기화를 지원한다.

DisposableBean : destroy() 메서드로 소멸을 지원한다

public class NetworkClient implements InitializingBean, DisposableBean {
    //...   
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("NetworkClient.afterPropertiesSet");
        connect();
        call("초기화 연결 메시지");
    }
    
    @Override
    public void destroy() throws Exception {
        System.out.println("NetworkClient.destroy");
        disConnect();
    }  
}

[ 결과 ]

객체 초기화가 완료된 시점에 afterPropertiesSet()가 호출되었고

스프링 컨테이너의 종료가 호출되자 destroy() 소멸 메서드가 호출되었다.

 

단점

  • 이 인터페이스는 스프링 전용 인터페이스다. 해당 코드가 스프링 전용 인터페이스에 의존한다.
  • 초기화, 소멸 메서드의 이름을 변경할 수 없다.
  • 내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.

인터페이스를 사용하는 초기화, 종료 방법은 스프링 초창기에 나온 방법들이고, 

지금은 다음의 더 나은 방법들이 있어서 거의 사용하지 않는다

 

2) 설정 정보에 초기화 메서드, 종료 메서드 지정

설정 정보에 @Bean(initMethod = "init", destroyMethod = "close") 처럼 초기화, 소멸 메서드를 지정할 수 있다.

@Configuration
static class LifeCycleConfig {

    @Bean(initMethod = "init", destroyMethod = "close")
    public NetworkClient networkClient() {
        NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl("http://hello-spring.dev");
        return networkClient;
    }
}
public class NetworkClient {
	//...
    public void init() {
        // 의존관계 주입후
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메세지");

    }

    public void close() {
        // 빈이 종료될 때
        System.out.println("NetworkClient.close");
        disconnect();
    }
}

특징

  • 메서드 이름을 자유롭게 줄 수 있다.
  • 스프링 빈이 스프링 코드에 의존하지 않는다.
  • 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료메서드를 적용할 수 있다.

종료 메서드 추론

@Bean의 destroyMethod 는 기본값이 (inferred) (추론)으로 등록되어 있다.

이 추론 기능은 close , shutdown 라는 이름의 메서드를 자동으로 호출해준다. 

이름 그대로 종료 메서드를 추론해서 호출해준다.

 

3) @PostConstruct, @PreDestroy 애노테이션 지원

@PostConstruct , @PreDestroy 이 두 애노테이션을 사용하면 가장 편리하게 초기화와 종료를 실행할 수 있다.

public class NetworkClient {
	//...
    @PostConstruct
    public void init() {
        // 의존관계 주입후
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메세지");

    }

    @PreDestroy
    public void close() {
        // 빈이 종료될 때
        System.out.println("NetworkClient.close");
        disconnect();
    }
}

특징

  • 최신 스프링에서 가장 권장하는 방법이다.
  • 패키지를 보면 javax.annotation.PostConstruct 이다. 스프링에 종속적인 기술이 아니라 JSR-250 라는 자바 표준이다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다.
  • 컴포넌트 스캔과 잘 어울린다.

 

단점

유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다. 

외부 라이브러리를 초기화, 종료 해야 하면 @Bean의 기능을 사용하자.

 

정리

@PostConstruct, @PreDestroy 애노테이션을 사용하자
코드를 고칠 수 없는 외부 라이브러리를 초기화, 종료해야 하면 @Bean 의 initMethod , destroyMethod 를 사용하자.

 


 

3. 빈 스코프

빈 스코프는 스프링 빈이 존재할 수 있는 범위를 뜻한다.

스프링 빈은 기본적으로 싱글톤 스코프로 생성된다.

 

빈 스코프 종류

싱글톤

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

 

프로토타입

스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고

더는 관리하지 않는 매우 짧은 범위의 스코프이다

 

웹 관련 스코프

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

 

1) 싱글톤

싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다.

1. 싱글톤 스코프의 빈을 스프링 컨테이너에 요청한다.

2. 스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환한다.

3. 이후에 스프링 컨테이너에 같은 요청이 와도 같은 객체 인스턴스의 스프링 빈을 반환한다.

 

[ 예제 ]

class SingletonTest {

  @Test
  void singletonBeanFind() {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);

    SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
    SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
    System.out.println("singletonBean1 = " + singletonBean1);
    System.out.println("singletonBean2 = " + singletonBean2);

    assertThat(singletonBean1).isSameAs(singletonBean2);

    ac.close();

  }

  static class SingletonBean {

    @PostConstruct
    public void init() {
      System.out.println("SingletonBean.init");
    }

    @PreDestroy
    public void destroy() {
      System.out.println("SingletonBean.destroy");
    }
  }

}

  • 빈 초기화 메서드를 실행
  • 같은 인스턴스의 빈을 조회
  • 종료 메서드까지 정상 호출

 

참고

new AnnotationConfigApplicationContext(SingletonBean.class);

ApplicationContext 를 생성할 때 파라미터로 빈으로 등록할 클래스를 전달하면

@Component 필요 없이 컨포넌트 스캔 대상이 된다.

 

2) 프로토타입 스코프

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

1. 프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.

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

3. 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환한다.

4. 이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환한다.

 

[ 예제 ]

class PrototypeTest {

  @Test
  void prototypeBeanFind() {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
    PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
    PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
    System.out.println("prototypeBean1 = " + prototypeBean1);
    System.out.println("prototypeBean2 = " + prototypeBean2);
    
    assertThat(prototypeBean1).isNotSameAs(prototypeBean2);

    ac.close();

  }

  @Scope("prototype")
  static class PrototypeBean {

    @PostConstruct
    void init() {
      System.out.println("PrototypeBean.init");
    }

    @PreDestroy
    void destroy() {
      System.out.println("SingletonBean.destroy");
    }

  }

}

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


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

싱글톤 빈(ClientBean)에서 프로토타입 스코프 빈(PrototypeBean)을 참조해 사용하는 예제이다.

class SingletonWithPrototypeTest1 {

  @Test
  void singletonClientUsePrototype() {
    ApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
    ClientBean clientBean1 = ac.getBean(ClientBean.class);
    int count1 = clientBean1.logic();
    assertThat(count1).isEqualTo(1);

    ClientBean clientBean2 = ac.getBean(ClientBean.class);
    int count2 = clientBean2.logic();
    assertThat(count2).isEqualTo(2);
  }

  static class ClientBean {
    private final PrototypeBean prototypeBean; // 생성시점에 주입

    ClientBean(PrototypeBean prototypeBean) {
      this.prototypeBean = prototypeBean;
    }

    public int logic() {
      prototypeBean.addCount();
      return prototypeBean.getCount();
    }
  }

  @Scope("prototype")
  static class PrototypeBean {
    private int count = 0;

    public void addCount() {
      count++;
    }

    public int getCount() {
      return count;
    }

    @PostConstruct
    public void init() {
      System.out.println("PrototypeBean.init " + this);
    }

    @PreDestroy
    public void destroy() {
      System.out.println("PrototypeBean.destroy " + this);
    }

  }
}

clientBean 은 싱글톤이므로, 보통 스프링 컨테이너 생성 시점에 함께 생성되고, 의존관계 주입도 발생한다.

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

 

클라이언트 A는 clientBean 을 스프링 컨테이너에 요청해서 받는다.싱글톤이므로 항상 같은 clientBean 이 반환된다.

  • 3. 클라이언트 A는 clientBean.logic() 을 호출한다.
  • 4. clientBean 은 prototypeBean의 addCount() 를 호출해서 프로토타입 빈의 count를 증가한다.
    count값이 1이 된다

클라이언트 B는 clientBean 을 스프링 컨테이너에 요청해서 받는다.

싱글톤이므로 항상 같은 clientBean 이 반환된다.

clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다.

주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지,

사용 할 때마다 새로 생성되는 것이 아니다!

  • 5. 클라이언트 B는 clientBean.logic() 을 호출한다.
  • 6. clientBean 은 prototypeBean의 addCount() 를 호출해서 프로토타입 빈의 count를 증가한다.
    원래 count 값이 1이었으므로 2가 된다.

 

[ 문제점 ]

싱글톤 빈이 프로토타입 빈을 사용할 때마다 프로토타입 빈을 새로 생성해서 사용하고자 했지만

싱글톤 빈은 생성 시점에만 의존관계 주입을 받고 의존관계 주입 시점에만 프로토타입 빈이 새로 생성되어

싱글톤 빈과 함께 계속 유지되는 문제가 발생한다.

 

4) 프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결

Dependency Lookup(DL)

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

싱글톤 빈이 프로토타입 빈을 사용할 때마다 프로토타입 빈을 새로 생성하려면

DL 방식을 사용해 스프링 컨테이너에서 프로토타입 빈을 꺼내오면 된다.

 

(1) ObjectFactory, ObjectProvider

static class ClientBean {
    @Autowired
    private ObjectProvider<PrototypeBean> prototypeBeanProvider;

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

스프링의 애클리케이션 컨텍스트를 주입받아 PrototypeBean을 받아올 수 있지만

ObjectFactory, ObjectProvider를 사용하면 지정한 프로토타입 빈만을 컨테이너에서 찾아주는 DL 정도의 기능만 사용할 수 있다.

  • ObjectFactory: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존
  • ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존

(2) JSR-330 Provider

static class ClientBean {
    @Autowired
    private Provider<PrototypeBean> provider;
    public int logic() {
        PrototypeBean prototypeBean = provider.get();
        prototypeBean.addCount();
        int count = prototypeBean.getCount();
        return count;
    }
}

스프링에 의존하지 않고 DL을 사용하려면 javax.inject.Provider 라는 JSR-330 자바 표준을 사용할 수 있다.

이 방법을 사용하려면 javax.inject:javax.inject:1 라이브러리를 gradle에 추가해야 한다.

implementation 'javax.inject:javax.inject:1'

 

정리

실무에서 웹 애플리케이션을 개발해보면, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.
ObjectProvider , JSR330 Provider 등은 프로토타입 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있다.

 

ObjectProvider vs JSR-330 Provider

스프링을 사용하다 보면 이 기능 뿐만 아니라 다른 기능들도 자바 표준과 스프링이 제공하는 기능이 겹칠때가 많이 있다. 대부분 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에, 특별히 다른 컨테이너를 사용할 일이 없다면, 스프링이 제공하는 기능을 사용하면 된다.

 


4. 웹 스코프

웹 스코프는 웹 환경에서만 동작한다.

웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다.

따라서 종료 메서드가 호출된다.

 

웹 스코프 종류

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

 

request 스코프

웹 스코프는 웹 환경에서만 동작하므로 web 환경이 동작하도록 라이브러리를 추가해야 한다.

implementation 'org.springframework.boot:spring-boot-starter-web'
@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 + "] request scope bean create:" + this);
    }

    @PreDestroy
    public void close() {
        System.out.println("[" + uuid + "] request scope bean close:" + this);
    }
}
@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 id) {
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log("service id = " + id);
    }
}

MyLogger 클래스를 request 스코프로 지정했다.

이 빈은 HTTP 요청 하나씩 생성되고 HTTP 요청이 끝나는 시점에 소멸된다.

 

LogDemoController와 LogDemoService는 싱글톤 빈이고 애플리케이션이 실행되는 시점에

싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 고객의 요청이 와야 생성된다.

 

ObjectProvider를 이용해서 ObjectProvider.getObject() 를 호출하는 시점까지 request scope 빈을 애플리케이션 컨텍스트에서 조회하는 처리를 지연할 수 있다.

ObjectProvider.getObject() 를 호출하시는 시점에는 HTTP 요청이 진행중이므로 request scope 빈의 생성이 정상 처리된다.

 

request 스코프 빈은 HTTP 요청마다 하나의 인스턴스를 생성되고 관리된다.

따라서 같은 HTTP요청(같은 UUID)이면 같은 스프링 빈이 반환된다.

 

스코프와 프록시

MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다.

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d

@Scope 의 proxyMode = ScopedProxyMode.TARGET_CLASS) 를 설정하면

스프링 컨테이너는 CGLIB 라는 바이트코 를 조작하는 라이브러리를 사용해서,

MyLogger를 상속받은 가짜 프록시 객체 생성한다.

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

정리

  • Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다.
  • 단지 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 
  • 이것이 바로 다형성과 DI 컨테이너가 가진 큰 강점이다.
  • 꼭 웹 스코프가 아니어도 프록시는 사용할 수 있다.

주의점

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

 

 

 

 

 

 

 

 

 

출처

스프링 핵심 원리 - 기본편(김영한)

 

 

 

728x90

'Spring' 카테고리의 다른 글

[Spring] 예외 처리  (0) 2022.10.21
[Spring] 트랜잭션  (0) 2022.10.19
[Spring] 2. 컴포넌트 스캔과 의존관계 자동 주입  (0) 2022.03.21
[Spring] 1. 스프링 컨테이너  (0) 2022.03.04