티스토리 뷰

Spring

[Spring] Bean & Bean Scope

MAENCO 2021. 8. 16. 22:58
반응형

데이터베이스나 커넥션 풀, 네트워크 소켓처럼 애플리케이션이 시작하는 시점에 연결을 미리 해야 하는 상황이 있다

이때 종료시점에 맞춰 연결을 모두 종료하려면 객체의 초기화와 종료 작업이 필요하다고 한다

초기화 같은 작업은 빈 객체를 생성하고 의존관계의 주입이 끝난 다음에야 필요한 데이터를 사용할 수 있도록 준비가 된다

즉 개발자가 언제 의존관계가 주입이 되는지를 알아야 한다

 

Bean 생명주기

의존관계를 알기 위해서는 우선 빈의 생명주기를 알아야 한다

스프링 빈은 대략적으로 아래와 같은 생명주기를 가지고 있다

(초기화 콜백: 빈이 생성되고 빈의 의존관계 주입이 완료된 후의 호출)

(소멸 전 콜백: 빈이 소멸되기 직전의 호출)

 

이때 생성자 주입의 경우 스프링 빈이 생성됨과 동시에 의존관계가 주입이 되며(파라미터 값 등의 값을 받아오기 위하여)

수정자와 필드 주입의 경우 스프링 빈이 생성 된 후에 의존관계가 주입된다

 

여기서 핵심은 객체의 생성과 초기화를 분리한다는 것이다

 

생성자 주입을 예로 들면 파라미터 값의 필수 정보를 받은 생성자는 객체를 만드는 역할을 한다

반면에 초기화는 이렇게 생성된 값들을 가지고 외부 커넥션을 연결하거나 하는 등의 다른 역할을 수행하게 된다

SRP(단일 책임 원칙)에 따라 객체 생성은 생성자가 초기화는 다른 메서드가 하는 방식으로 역할을 분리해야 유지보수 관점에서도 좋다

 

스프링은 크게 아래와 같이 빈의 생명주기 콜백을 지원한다

 

1. 인터페이스

InitializingBean & DisposableBean의 인터페이스를 사용함

(지금은 사용하지 않기 때문에 자세한 내용은 skip)

 

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

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

public void close() {
   System.out.println("NetworkClient.close");
   disConnect();
}


// 설정 정보
@Configuration
static class LifeCycleConfig {
      
   @Bean(initMethod = "init", destroyMethod = "close")
   public NetworkClient networkClient() {
       NetworkClient networkClient = new NetworkClient();
       networkClient.setUrl("http://hello-spring.dev");
       return networkClient;
  }
}

말 그대로 설정 정보에서 시작과 종료 메서드를 지정해주는 것이다

이때 재밌는 것이 하나 있는데 destoryMethod의 기본값은 inferred(추론)으로 등록이 되어 있다

이유는 대부분이 라이브러리들이 종료 메서드의 이름을 close 혹은 shutdown으로 사용하는데

이를 추론하여서(close 혹은 shutdown) 종료 메서드를 찾아주는 것이다 즉 값을 적지 않아도 잘 사용한다

 

3. 어노테이션 (@PostConstruct & @PreDestroy)

어노테이션 또한 말 그대로 어노테이션을 직접 붙여 시작과 종료를 지정해주는 것이다

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

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

이는 최신 스프링에서 가장 권장하는 방법이며, 어노테이션 하나만 붙이면 되기 때문에 아주 아주 간편하다

이 어노테이션 같은 경우는 자바 표준 기술이기 때문에 다른 컨테이너에서도 작동하며 컴포넌트 스캔과 잘 어울린다

하지만 이도 단점이 있다 바로 코드를 고칠 수 없는 외부 라이브러리에 적용하지 못한다는 것인데

이때는 2번에서 설명한 @Bean으로 대체하여 사용하면 문제를 해결할 수 있다

 

Bean 스코프

자바에서 변수등의 사용범위를 스코프라고 부른다, 스프링의 빈 또한 스코프가 있다

빈의 스코프는 아래와 같이 수동 혹은 자동으로 등록할 수 있다

// 자동 등록
@Scope("prototype")
@Component
public class Exam{
}

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

대부분 싱글톤으로 빈을 관리하는 스프링의 경우 컨테이너의 시작과 함께 생성되어 종료될 때까지 유지되는 싱글톤 스코프와

이와 반대인 프로토 타입 스코프가 존재한다

 

Prototype Scope(프로토타입 스코프)

스프링 컨테이너에 빈을 조회하면 싱글톤 빈 같은 경우 항상 같은 객체를 반환할 수 있게 보장한다

프로토타입은 이와 정반대로 조회할 때마다 새로운 인스턴스 객체를 반환하는 것을 보장한다

프로토타입 빈의 경우 매번 새로 생성하기 때문에 스프링 컨테이너는

프로토타입 빈의 생성과 의존관계 주입 그리고 초기화까지만 관여하게 된다

이 말인즉슨 프로트 타입은 조회한 클라리언트가 관리해야 하며 @PreDestory 같은 종료 메서드가 자동으로 호출되지 않는다

만약 프로토타입 빈을 종료하려면 아래와 같이 직접 호출하여야 한다

prototypeBean.destroy();

 

Provider

스프링 컨테이너가 거의 모든 빈을 싱글톤으로 관리한다

그래서 대부분 싱글톤 빈으로 프로토 타입 빈을 호출하게 되는데

이때 문제가 하나 생긴다

 

클라이언트가 요청을 하면 프로토타입을 생성하고 숫자를 증가시키는 로직을 호출한다는 상황을 가정해보자

이때 프로토타입이라면 항상 새로운 객체를 반환해야 하기 때문에 몇 번 호출이 되던 0 -> 1로 카운팅이 되어야 할 것이다

하지만 싱글톤 빈은 항상 같은 객체를 반환하기 때문에 위의 SingletonBean이

프로토타입 빈을 호출할 경우 우리의 기대와는 다르게 호출되는 만큼 숫자가 누적해서 증가한다

이는 SingletonBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 상태이기 때문이다

즉 주입 시점에 컨테이너에 요청을 하여 프로토타입이 생성이 된 것이지 사용할 때마다 새로 생성되는 것이 아니라는 것이다

하지만 이렇게 되면 프로토타입을 사용하는 이유가 없다 (그냥 싱글톤 빈을 사용하지..)

 

이를 해결해주는 것이 바로 Provider의 역할이다

DL이라고도 표현하는 Provider는 Denependecy Lookup의 약자로 의존관계 탐색을 도와주는 기능을 한다

 

ObjectProvider

스프링 표준의 ObjectProvider를 사용하기 위한 예제로 아래와 같이 생성자 주입을 사용하여 만들었다

private final ObjectProvider<PrototypeBean> prototypeBeanProvider;

@Autowired
public ClientBean(ObjectProvider<PrototypeBean> prototypeBeanProvider) {
    this.prototypeBeanProvider = prototypeBeanProvider;
}

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

이렇게 ObjectProvider(DL)을 사용하여 의존성 주입을 하게 되면

프로토타입이 아래와 같이 새로운 인스턴스 객체를 반환해준다

(생략)$PrototypeBean@432038ec
(생략)$PrototypeBean@1f010bf0

 

JSR-330 Provider

자바 표준의 Provider인 JSR-330 Provicer 또한 같은 기능을 제공한다

즉 DL역할을 하며 항상 새로운 인스턴스 객체를 반환하도록 해준다

이 Provider 같은 경우는 라이브러리를 추가해주어야 사용할 수 있다 (Gradle에 추가하려면 아래와 같이 추가하면 된다)

javax.inject:javax.inject:1 // build.gradle에 추가

코드 작성 시에도 조금의 차이가 있다

private final Provider<PrototypeBean> prototypeBeanProvider;

@Autowired
public ClientBean(Provider<PrototypeBean> prototypeBeanProvider) {
   this.prototypeBeanProvider = prototypeBeanProvider;
}

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

JSR-330 Provider도 똑같이 새로운 인스턴스 객체를 생성 하는 것을 볼 수 있다

(생략)$PrototypeBean@31add175
(생략)$PrototypeBean@1b955cac

이렇게 싱글톤 빈에서 프로토타입 빈을 요청하더라도 문제없이

항상 새로운 인스턴스 객체를 반환해주게끔 하는 DL을 알아보았다

ObjectProvider / JSR-330 Provider 같은 경우는 꼭 프로토타입이 아닌 DL이 필요한 경우 언제든지 사용할 수 있다고 한다

 

그렇다면 이 둘 중 무엇을 사용해야 하나?

사실 이러한 고민은 DL뿐만 아니라 굉장히 다양한 영역에서 고민하게 된다

예를 들어 스프링이 제공하는 @Autowired와 자바 표준인 @Injection의 경우 똑같은 기능을 제공한다

 

정답은 모르겠지만 대부분의 경우

스프링 말고 다른 컨테이너에서도 사용해야 한다면 자바 표준을 사용하고

그 외에는 스프링 표준을 사용한다고 한다(대부분 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에)

 

그렇다면 프로토타입 빈은 왜 필요한가?

사실 대부분의 경우 싱글톤 빈만으로도 해결이 가능하기 때문에

사용하는 경우는 거의 없다고 하지만

혹시라도 매번 사용할 때마다 새로운 객체가 필요할 때 프로토타입 빈을 사용하면 된다

 

핵심정리

데이터 커넥션 풀이나 네트워크 소켓처럼 애플리케이션이 시작하는 시점에 미리 연락을 해야 하는 경우가 있다

이런 경우 애플리케이션이 종료에 맞춰 객체도 종료해야 하기 때문에 객체가 어느 시점에 초기화와 종료가 되는지 알아야 한다

 

이를 알 수 있는 것이 빈의 생명주기이다(생명주기의 설명은 위에)

객체의 생성과 초기화에서의 핵심은 이 둘의 역할을 분리하는 것이다

 

대부분의 경우 @PostConstruct와 @PreDestroy를 사용하여

초기화와 종료 시점을 지정한다

하지만 이는 싱글톤에서 둘 다 지정하며

싱글톤 빈과 정반대의 개념인 프로토타입의 경우 종료 메서드를 클라이언트가 직접 호출하여야 한다

 

그 이유는 프로토타입 빈의 경우 항상 새로운 인스턴스 객체를 반환해주는 것을 보장하는데

스프링 컨테이너가 생성 , 의존관계 주입 그리고 초기화까지만 관여하기 때문에 종료 메서드를 자동으로 호출해주지 않기 때문이다

 

대부분 싱글톤으로 관리하는 스프링 컨테이너의 경우 프로토타입 빈을 사용할 때는 DL(Dependency Lookup)인 Provider로

의존관계를 탐색하여야 새로운 인스턴스 객체를 반환하는 것을 보장한다

 

 

반응형
댓글