[Spring] Bean Scope

강민승·2023년 9월 7일
0

spring

목록 보기
9/17

빈 스코프(Bean Scope)란?

빈 스코프는 말 그대로 빈이 존재할 수 있는 범위를 뜻한다. 스프링은 다음과 같은 다양한 스코프를 지원한다.

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

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

웹 관련 스코프

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

싱글톤 스코프

싱글톤 스코프 요청

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

프로토타입 스코프

프로토타입 빈 요청


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

프로토타입의 빈을 스프링 컨테이너에 요청하면 스프링 컨테이너는 프로토타입의 빈을 생성하고, 필요한 의존관계를 주입한다.
싱글톤 빈은 컨테이너 생성 시점에 같이 생성되고 초기화되지만, 프로토타입 빈은 스프링 컨테이너에서 빈을 조회할 때 생성되고 초기화 메서드도 실행된다.

class PrototypeTest {
   @Test
   void prototypeBeanFind() {
       ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
       System.out.println("find prototypeBean1");
       final PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
       System.out.println("find prototypeBean2");
       final PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
       assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
       ac.close();
   }

   @Scope("prototype")
   static class PrototypeBean {
       @PostConstruct
       public void init() {
           System.out.println("SingletonBean.init");
       }

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

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

싱글톤 빈, 프로토타입 빈을 함께 쓸 때 문제점

프로토타입 스코프의 빈은 해당 타입의 빈이 스프링 컨테이너에 요청될 때마다 생성된다. 그런데 싱글톤 스코프의 빈이 프로토타입의 빈을 주입 받는다면, 싱글톤 스코프의 빈이 생성되고 의존 관계가 주입되는 시점에만 프로토타입 빈이 조회될 것이고, 이후에는 계속 같은 빈이 사용될 것이다. 대부분의 경우, 개발자는 이를 의도하지 않았을 것이다. 애초에 매번 새로운 객체가 필요하기 때문에 프로토 타입으로 선언했을 것이기 때문이다.

따라서 아래와 같은 테스트 코드를 실행한다면, 테스트는 실패할 것이다.

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

       final ClientBean clientBean2 = ac.getBean(ClientBean.class);
       final int count2 = clientBean2.logic();
       assertThat(count2).isEqualTo(1);

       ac.close();
   }

   @Scope("singleton")
   static class ClientBean {
       private final PrototypeBean prototypeBean; // 생성 시점에 주입

       public ClientBean(final 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");
       }
   }
}


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

가장 간단한 방법은, 프로토타입의 의존 관계를 주입받지 않고 ApplicationContext에 매번 빈을 아래와 같이 요청하는 것이다. 하지만 이렇게 되면, 스프링에 종속적인 코드가 되고 단위테스트도 어려워 진다.

여기서 말하는 DL(Dependency Lookup)

의존관계를 외부에서 주입받는 걸 Dependency Injection(DI)이라 불렀다면, 직접 필요한 의존관계를 찾는 것은 Dependency Lookup(DL)이라고 부른다. ObjectFactory와 ObjectProvider가 스프링에서 DL 기능을 제공한다.

  • ObjectFactory, ObjectProvider

    ObjectProvider: 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공한다.
    ObjectFactory: ObjectProvider의 구 버전 클래스이다.
    스프링 컨테이너에서만 사용 가능하다는 단점이 있다.
@Scope("singleton")
static class ClientBean {
    @Autowired
    private ApplicationContext ac;

    public int logic() {
        final PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
        prototypeBean.addCount();
        return prototypeBean.getCount();
    }
}
 

1) ObjectFactory, ObjectProvider
ObjectProvider는 지정한 빈을 컨테이너에서 대신 찾아주는 DL(Dependency Lookup) 서비스를 제공한다. 원래는 ObjectFactory만 있었는데, 여기에 편의 기능을 추가해서 ObjectProvider가 만들어졌다. 아래와 같이 사용할 수 있다.

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

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

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

특징

1) 스프링이 제공하는 기능을 사용하긴 하지만, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기 쉬워진다.

2) 딱 필요한 DL(Dependency Lookup) 기능만 제공한다.

2) JSR-330 Provider
이 방법은 javax.inject.Provider 라는 JSR-330 자바 표준을 사용하는 방법이다. 이 방법을 사용하기 위해선 javax.inject:javax.inject:1 라이브러리를 gradle에 별도로 추가해야 한다. 아래와 같이 사용할 수 있다.

@Scope("singleton")
static class ClientBean {
    private final Provider<PrototypeBean> prototypeBeanProvider;

    ClientBean(final Provider<PrototypeBean> prototypeBeanProvider) {
        this.prototypeBeanProvider = prototypeBeanProvider;
    }

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

특징

1) 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.

2) 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기 쉬워진다.

3) 딱 필요한 DL(Dependency Lookup) 기능만 제공한다.

4) 별도의 라이브러리가 필요하다.

profile
Step by Step goes a long way. 꾸준하게 성장하는 개발자 강민승입니다.

0개의 댓글