4 분 소요


빈 스코프란?

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


스프링이 지원하는 스코프

  • 싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
  • 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프
  • 웹 관련 스코프
    • request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프
    • session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
    • application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프

빈 스코프 지정

컴포넌트 스캔 자동 등록

@Scope("prototype")
@Component
public class HelloBean {}

수동 등록

@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
    return new HelloBean();
}


프로토타입 스코프

싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환하지만 프로토타입 스코프를 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환해준다.

싱글톤 빈 요청

singleton

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

프로토타입 빈 요청

prototype

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

prototype1

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

정리

핵심은 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다는 것. 클라이언트에 빈을 반환 후 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않으며, 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있다. 그래서 @preDestroy같은 종료 메서드가 호출되지 않는다.


프로토타입 빈의 특징 정리

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


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

프로토타입 빈 직접 요청

스프링 컨테이너에 프로토타입 빈 직접 요청1
prototype2

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

스프링 컨테이너에 프로토타입 빈 직접 요청2
prototype3

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

싱글톤 빈에서 프로토타입 빈 사용

clientBean이라는 싱글톤 빈이 의존관계 주입을 통해서 프로토타입 빈을 주입받아서 사용하는 예시

싱글톤에서 프로토타입 빈 사용1
clientBean1

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

싱글톤에서 프로토타입 빈 사용2
clientBean2

  • 클라이언트A는 clientBean을 스프링 컨테이너에 요청해서 받는다. 싱글톤이므로 항상 같은 clientBean이 반환된다.
  • 4.클라이언트A는 clientBean.logic()을 호출한다.
  • 5.clientBean은 prototypeBean의 addCount()를 호출해서 프로토타입 빈의 count++
  • 결과적으로 count = 1

싱글톤에서 프로토타입 빈 사용3
clientBean3

  • 클라이언트B는 clientBean을 스프링 컨테이너에 요청해서 받는다. 싱글톤이므로 항상 같은 clientBean이 반환된다.
  • clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다. 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지, 사용 할 때마다 새로 생성되는 것이 아니다.
  • 6.클라이언트B는 clientBean.logic()을 호출한다.
  • 7.clientBean은 prototypeBean의 addcount()를 호출해서 프로토타입 빈의 count++
  • 결과적으로 count = 2

[참고] logic()

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

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

[참고]
여러 빈에서 같은 프로토타입 빈을 주입 받으면, 주입 받는 시점에 각각 새로운 프로토타입 빈이 생성
clientA -> prototypeBean@x01
clientB -> prototypeBean@x02


Provider로 문제 해결

싱글톤 빈과 프로토타입 빈을 함께 사용할 때 새로운 프로토타입 빈을 생성하는 방법

스프링 컨테이너에 요청

static class ClientBean {
    @Autowired
    private ApplicationContext ac;

    public int logic() {
        PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
        prototypeBean.addCount();
        int count = prototypeBean.getCount();
        return count;
    }
}
  • 간단한 방법은 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청.
  • 의존관계를 외부에서 주입(DI)받는게 아닌 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL) 의존관계 조회(탐색)라한다.
  • 하지만 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다.
  • DL정도의 기능만 제공하기만 하면 된다!(ObjectFactory, ObjectProvider)

ObjectFactory, ObjectProvider

지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 ObjectProvider이다. 과거에는 ObjectFactory가 있었는데, 여기에 편의 기능을 추가해서 ObjectProvider가 만들어졌다.

@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;

public int logic() {
    PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}
  • ObjectProvidergetObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.(DL)
  • 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.
  • ObjectProvider는 필요한 DL정도의 기능만 제공한다.

특징

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

JSR-330 Provider

javax.inject.Provider라는 JSR-330 자바 표준을 사용하는 방법. 이 방법을 사용하려면 javax.inject:javax.inject:1라이브러리를 gradle에 추가해야 한다.

package javax.inject;

public interface Provider<T> {
    T get();
}
@Autowired
private Provider<PrototypeBean> provider;

public int logic() {
    PrototypeBean prototypeBean = provider.get();
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}
  • providerget()을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.(DL)
  • 자바 표준이고, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기 쉬워진다.
  • provider는 필요한 DL 정도의 기능만 제공한다.

특징

  • get()메서드 하나로 기능이 매우 단순하다.
  • 별도의 라이브러리가 필요하다.
  • 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.


정리

  • 매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 프로토타입 빈을 사용하면 된다.
  • 하지만, 실무에서는 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.
  • ObjectProvider, JSR-330 Provider 등은 프로토타입 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있다.

[참고] ObjectProvider는 DL을 위한 편의 기능을 많이 제공해주고 스프링 외에 별도의 의존관계 추가가 필요 없기 때문에 편리하기 때문에 사용하면 된다. 만약, 코드를 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야 한다면 JSR-330 Provider를 사용해야 한다.

스프링을 사용하다 보면 자바 표준과 스프링이 제공하는 기능이 겹칠때가 많은데, 대부분 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에 특별히 다른 컨테이너를 사용할 일이 없다면, 스프링이 제공하는 기능을 사용!


<출처 : 인프런 - 스프링 핵심 원리 기본편(김영한)>

댓글남기기