본문 바로가기
Spring|Spring-boot

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

by oncerun 2021. 12. 18.
반응형

스프링 컨테이너가 관리하는 모든 빈은 기본 값으로 싱글톤 스코프를 가지고 관리된다. 그 와 별개로 클라이언트의 요청마다 스프링 컨테이너가 빈 생성, 의존관계 주입, 초기화 메서드까지 실행까지만 책임지는 빈이 존재하는데 

이때 해당 빈은 프로토 타입 스코프를 갖는다고 말한다.

 

 

우선 스프링이 지원하는 다양한 스코프의 종류를 알아보자.

 

  • 싱글톤 : 스프링의 기본 스코프 방식으로, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 긴 라이프사이클을 가지는 빈 스코프이다.
  • 프로토타입 : 스프링 컨테이너는 빈의 생성과 의존관계 주입까지만 책임진다.
  • request : Spring Web에서 사용되는 스코프로 웹 요청이 들어오고 나갈 때까지만 유지되는 스코프이다.
  • session : 웹 세션이 생성되고 종료될 때까지 유지되는 스코프이다.
  • application : 웹의 서블릿 콘텍스트와 같은 범위로 유지되는 스코프이다.

 

 

테스트 시나리오는 다음과 같다.

 

클라이언트 A, 클라이언트 B가 프로토타입 빈을 의존관계로 가지고 있는 싱글톤 빈을 요청한다.

 

클라이언트들은 프로토타입 빈은 매번 새로운 객체를 응답해 준다고 하여 상태를 관리하는 필드에 데이터 수정을 요청하고, 그 응답으로 프로토타입을 의존관계로 가지고 있는 싱글톤은 해당 로직을 실행 후 결과를 클라이언트에게 반환한다. 

 

 

1. 프로토타입 빈 

    @Scope("prototype")
    static class PrototypeBean {

        private int count;

        public PrototypeBean() {
            this.count = 0;
        }


        public void addCount(){
            ++count;
        }

        public int getCount(){
            return count;
        }
    }

 

 

프로토 타입 빈은 count라는 상태를 가지고 있으며 객체 생성 시 그 값을 0으로 초기화한다.

(사실 객체 생성과 초기화하는 로직은 별도로 나누는 것을 추천하지만 간단한 초기값 세팅은 생성자에서 하는 것도 나쁘지 않은 것 같다.)

 

2. 싱글톤 빈

    @Scope("singleton")
    @RequiredArgsConstructor
    static class Singleton{

        private final PrototypeBean prototypeBean;


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

    }

싱글톤 빈은 클라이언트의 요청을 addCountLogic() 메서드로 처리하는데 의존관계를 가진 프로토타입 빈에게 그 요청을 전달해 프로토타입 count상태를 응답해준다.

 

3. TestMain

 @Test
    @DisplayName("프로토타입은 요청마다 it's new?")
    void testMain(){
        // 빈등록
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class, SingletonWithPrototypeTest.SingletonBean.class);

        //클라이언트 2명의 요청
        SingletonWithPrototypeTest.SingletonBean singletonBean1 = ac.getBean(SingletonWithPrototypeTest.SingletonBean.class);
        SingletonWithPrototypeTest.SingletonBean singletonBean2 = ac.getBean(SingletonWithPrototypeTest.SingletonBean.class);

        // 로직실행
        int result1 = singletonBean1.addCountLogic();
        int result2 = singletonBean2.addCountLogic();
        //결과
        //1. 싱글톤이 맞는지 확인
        assertThat(singletonBean1).isSameAs(singletonBean2);

        //2. result1 값이 모두 1인지 확인
        assertThat(result1).isEqualTo(result2);

    }

그 값은 예상과 다르게 2이다. 이 결과를 예상하지 못했다면 스프링이 싱글톤을 생성하는 과정과 프로토타입의 값이 무엇으로 들어가는지 생각해보아야 한다.

 

프로토타입은 스프링 DI 컨테이너의 요청마다 새로 생성하는 빈 객체이다.  그와 반대로 싱글톤은 단 하나의 객체만을 관리한다. 그렇기 때문에 싱글톤 내부에 존재하는 프로토타입은 싱글톤 객체 생성 후 의존관계 주입 시 새로 만들어져서 주입되긴 하지만 그 참조값은 싱글톤 객체에 저장되어버려 요청마다 새로 생기지 않는다.

 

해결 방안

 

싱글톤 빈에서 그러면 매번 요청마다 프로토타입을 새로 생성한 후 주입해 사용하지는 못하는 것일까?

우리는 의존관계를 스프링 컨테이너가 설정해주는 것을 DI라고 했다면,  직접 필요한 의존관계를 탐색하는 것을 Dependency LookUp이라고 한다. 

 

DL을 위해 우리가 스프링 컨테이너를 싱글톤에 의존관계를 설정한 후 getBean()를 매번 호출해줘서 새로운 프로토타입 객체를 설정해 줄 수 도 있다.

 

하지만 이런 방법은 스프링 컨테이너에 해당 객체가 종속되고, 단위 테스트를 할 수 없는 상황이 되어버린다.

 

그래서 스프링에선 ObjectProvider, ObjectFactory를 제공한다.

ObjectFactory를 상속받은 ObjectProvider는  ObjectFactory가 getObject() 메서드만 존재하는 것과 달리 추가적인 편의 메서드를 지원한다.

 

@Scope("singleton")
    @RequiredArgsConstructor
    static class SingletonBean{

        private final ObjectProvider<PrototypeBean> prototypeBeanProvider;

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

    }

다음과 같이 코드를 변경하고 테스트 결과를 확인하자.

2번의 요청에 각 새로운 Prototype 객체가 생성되어 주입되는 것을 확인할 수 있다.

 

스프링이 지원하는 기능을 사용하지만 매우 단순히 대리자에게 요청을 전달하는 것으로도 단위 테스트나, Mock테스트를 작성하기가 매우 쉬워진다.

 

스프링에 의존하는 코드가 싫다면 다음과 같은 방법을 사용하자.

 

JSR-330 Provider는 자바 표준으로 javax.inject: javax.injext:1 라이브러리를 추가해서 사용할 수 있다.

    @Scope("singleton")
    @RequiredArgsConstructor
    static class SingletonBean{

        private final Provider<PrototypeBean> prototypeBeanProvider;

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

    }

 provider의 get()을 호출하면 스프링 컨테이너에서 해당 빈을 찾아서 반환한다. 자바 표준이라는 점과 기능이 매우 단순해 단위 테스트와 mock코드를 만들기 쉽다. 또한 필요한 DL 정도의 기능만을 제공한다.

 

Provider 기능은 보통 빈들의 순환참조를 풀고 , 지연 로딩,  객체를 매번 주입받을 때, Optional인 경우에 사용한다. 

 

JSR-330 Provider를 사용할지 스프링에서 제공하는 ObjectProvider를 사용할지는 상황에 따라 알맞게 적용하면 된다. 자바 표준을 사용하여 다른 컨테이너에서도 동작하는 코드를 만들지, 스프링에서 제공하는 기능만으로 충분한지 등등 각각의 상황에 맞게 구현하는 것을 추천하는 것 같다.

 

사실 프로토타입 빈을 사용할 일은 리소스를 많이 사용한다는 점에서 매우 겪기 힘든 경험이고 대부분 싱글톤 스코프로 비지니스 문제를 대부분 해결 가능하지만, 이 개념은 스프링을 이해하는데 큰 도움이 되기 때문에 한번 정리했다. 

 

 

 

 

 

 

반응형

'Spring|Spring-boot' 카테고리의 다른 글

뷰 리졸버  (0) 2021.12.25
핸들러 매핑과 핸들러 어댑터  (0) 2021.12.25
Spring BeanDefinition  (0) 2021.12.15
Validation  (0) 2021.06.10
Spring Transaction(2)  (0) 2021.04.03

댓글