[Spring] 9-4. 프로토타입 스코프 - Provider로 문제해결

송광호·2024년 1월 19일

[Spring]

목록 보기
40/41
post-thumbnail

Spring 시리즈는 혼자 공부하며 기록으로 남기고, 만약 잘못 학습 한 지식이 있다면 공유하며 피드백을 받고자 작성합니다.
스프링에 대해 깊게 공부해보고자 인프런의 김영한 강사님께서 강의를 진행하시는 (스프링 핵심 원리 - 기본편) 강의를 수강하며 정리하는 글입니다.
혹여나 글을 읽으시며 잘못 설명된 부분이 있다면 지적 부탁드리겠습니다.


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

  • 앞 게시글에서 싱글톤 빈과 프로토타입 스코프를 같이 사용하면 어떤 문제가 발생하는지 알아보았다.
  • 이 문제를 해결하기 위해 몇가지 방법이 있으니 차례대로 살펴보자

스프링 컨테이너에 직접 요청

테스트 코드

public class SingletonWithPrototypeTest1 {
    @Test
    void singletonClientUsePrototype() {
        AnnotationConfigApplicationContext 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(1);
    }

    @Scope("singleton")
    static class ClientBean {

        private final ApplicationContext ac;

        public ClientBean(ApplicationContext ac) {
            this.ac = ac;
        }

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

    @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");
        }
    }
}
  • 그냥 로직 호출할 때마다 프로토타입 빈을 생성하는 방법이다. (무식한 방법)
  • 실행해보면 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.

실행 결과

PrototypeBean.init hello.core.scope.SingletonWithPrototypeTest1$PrototypeBean@1cf2fed4
PrototypeBean.init hello.core.scope.SingletonWithPrototypeTest1$PrototypeBean@329a1243
  • 이렇게 의존관계를 외부에서 주입(DI) 받는게 아니라 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL) 의존관계 조회(탐색) 이라고 한다.
  • 이렇게 스프링의 ApplicationContext 전체를 주입받게 되면 스프링 컨테이너에 너무 종속적이게 되고 나중에 단위 테스트를 짜기 어려워진다.
  • 사실 지금 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 딱 찾아주는 기능만 있으면 된다.
  • 이미 스프링에서는 모든게 준비가 되어있다. 스프링 굉장해! 엄청나!

ObjectFactory, ObjectProvider

    @Scope("singleton")
    static class ClientBean {

        private final ObjectProvider<PrototypeBean> prototypeBeanProvider;

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

        public int logic() {
            PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }
  • 단순히 찾아서 제공해주는 것 provider를 사용하면 된다.
  • .getObject() 메서드를 호출하면 그때서야 스프링 컨테이너에서 프로토타입 빈을 찾아서 반환해주는 것이다.

그래서 ObjectFactory 는 뭔데?

  • 위의 코드에서 ObjectProviderObjectFactory 로 바꾸어도 동작한다.

ObjectFactory 인터페이스

@FunctionalInterface
public interface ObjectFactory<T> {

	T getObject() throws BeansException;

}
  • getObject() 메서드 하나만 구현되어있다.

ObjectProvider 인터페이스

public interface ObjectProvider<T> extends ObjectFactory<T>, Iterable<T> {...}
  • ObjectProvider 인터페이스를 살펴보면 ObjectFactory를 이미 상속받고있다.
  • ObjectFactory 에서 편의기능 몇가지를 더 추가해 만든 것이 ObjectProvider

특징

  • ObjectFactory : 기능이 단순, 별도의 라이브러리가 존재하지 않음, 스프링에 의존적이다
  • ObjectProvider : ObjectFactory 상속했기 때문에 옵션이나 스트림처리 같은 몇가지 편의 기능이 많다.

스프링에 의존하지 않는 새로운 기술 JSR-330 Provider

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

스프링부트 3.0 이상
jakarta.inject:jakarta.inject-api:2.0.1 라이브러리 추가

코드 수정

@Scope("singleton")
    static class ClientBean {
		//gradle jakarta.inject:jakarta.inject-api:2.0.1 추가
        private final Provider<PrototypeBean> prototypeBeanProvider;

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

        public int logic() {
            PrototypeBean prototypeBean = prototypeBeanProvider.get(); //get으로 변경
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }
  • 실행해보면 항상 새로운 프로토타입 빈이 생성되는걸 확인할 수 있다.
  • 자바 표준이고, 기능이 단순해서 단위테스트나 Mock으로 사용하기 편하다.
  • Provider는 지금 딱 필요한 DL 정도의 기능만 제공한다.

특징

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

정리

  • 그래서 프로토타입 빈을 언제 사용하는데?
    매번 사용할 때 마다 의존관계 주입이 완료된 객체 인스턴스가 필요하면 그때 사용하면 된다.
    사실 실무에서 애플리케이션을 막상 개발하다 보면 싱글톤 빈으로 대부분의 문제가 해결이 되기 때문에, 프로토타입 빈을 사용하는 경우는 드물다.
  • ObjectProvider, JSR-330 Provider 같은 경우 프로토타입 뿐만아니라 DL이 필요한 경우라면 언제든지 사용가능하다.

참고: 스프링이 제공하는 메서드에 @Lookup 애노테이션을 사용하는 방법도 존재하지만, 이전 방법들로 충분하고 고려해야할 내용도 많아서 거의 쓰지 않는다.

참고: 그럼 ObjectProvider를 사용할지 자바표준인 JSR-330 Provider를 사용할지 고민이 될 것인데, 편한건 스프링이 제공하는 ObjectProvider이다. 정말 만약(그럴 일은 거의 없겠지만!) 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야한다면 자바 표준을 사용해야한다.

  • 스프링을 쓰다보면 스프링이 제공하는 기능을 사용할지 자바 표준을 사용할지 겹칠때가 많은데 대부분 스프링이 제공해 주는 기능이 좋기때문에 다른 컨테이너를 사용할 일이 없다면 그냥 스프링이 제공하는 기능을 사용하면 된다.

0개의 댓글