jhhan의 블로그

스프링 코어(17) - 프로토타입과 싱글톤을 같이 쓸 때 해결법 본문

Spring

스프링 코어(17) - 프로토타입과 싱글톤을 같이 쓸 때 해결법

jhhan000 2021. 11. 18. 00:05

오랜만에 다시 써봅니다.

(오랜만에 작성하는 블로그여서 저번에 뭘 썼는지도 잊었습니다...)

 

지난번 포스트에서는 프로토타입과 싱글톤 타입을 함께 쓸 때 나타날 수 있는 문제점에 대해 알아봤는데,

이번에는 이에 대한 해결법을 알아보겠습니다.

 

일단 지난번에 다뤘던 문제는

  • 싱글톤 빈 생성
  • 싱글톤 빈이 주입받을 때 프로토타입 빈이 주입
  • 그래서 프로토타입 빈이지만 싱글톤 처럼 작동

이런 문제가 있었습니다.

우리는 싱글톤 빈이 있더라도 프로토타입 빈의 성격을 가진 것을 사용하고 싶다는 것이죠.

 

그럼 가장 단순하게 생각하면 되죠!

바로 → 싱글톤 빈이 프로토타입 빈을 사용할 때마다 스프링 컨테이너에 새로 요청하는 것입니다.

@Test
void providerTest() {
    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);
}

static class ClientBean {
    @Autowired
    private ApplicationContext applicationContext;

    public int logic() {
      PrototypeBean prototypeBean = applicationContext.getBean(PrototypeBean.class);
      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"); }
  
}

이렇게 코드를 작성하면 됩니다.

여기서 중요하게 볼 부분은

  • PrototypeBean prototypeBean = applicationContext.getBean(PrototypeBean.class);

이 부분입니다.

항상 새로운 프로토타입 빈이 생성되는 것을 가능하게 해주는 코드입니다.

 

이렇게 직접 필요한 의존관계를 찾는 것Dependency Lookup(DL) - 의존관계 탐색(조회) 라고 합니다.

 

하지만 저런 형식의 DL은 일단 복잡합니다.

그래서 나중에 수정도 힘들고, 테스트를 하는데도 어려움을 겪을 수 있습니다.

그러므로 좀 더 간결하게 DL을 적용할 수 있는 방법을 찾아야 합니다.

 

그리고 이런 기능은 스프링에서 제공해주고 있습니다.

ObjectFactory와 ObjectProvider가 DL을 좀 더 간단하게 진행할 수 있게 합니다.

 

먼저 ObjectFactory를 사용해보겠습니다.

public class SingletonWithPrototypeTest1 {

    @Test
    public void singletonClientUsePrototype() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean bean1 = ac.getBean(ClientBean.class);
        int count1 = bean1.logic();
        System.out.println("count1 = " + count1);
        Assertions.assertThat(count1).isEqualTo(1);

        ClientBean bean2 = ac.getBean(ClientBean.class);
        int count2 = bean2.logic();
        System.out.println("count2 = " + count2);
        Assertions.assertThat(count2).isEqualTo(1);
    }

    @Scope("singleton")
    static class ClientBean {

        @Autowired
        private ObjectFactory<PrototypeBean> prototypeBeanFactory;
        
        public int logic() {
            PrototypeBean prototypeBean = prototypeBeanFactory.getObject();    // prototypeBean만 찾아주는 역할 수행
            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");
        }
    }
}

ClientBean클래스를 보면 ObjectFactory를 사용한 것을 볼 수 있습니다.

그리고 새로운 프로토타입 빈 생성을 위해 prototyeBeanFactory.getObject()를 사용한 것을 알 수 있습니다.

그럼 결과는... 당연히...

count1과 count2 모두 1이라는 값이 나오는 것을 확인할 수 있습니다.

 

다음으로 ObjectProvider를 사용해보겠습니다.

public class SingletonWithPrototypeTest1 {

    @Test
    public void singletonClientUsePrototype() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean bean1 = ac.getBean(ClientBean.class);
        int count1 = bean1.logic();
        System.out.println("count1 = " + count1);
        Assertions.assertThat(count1).isEqualTo(1);

        ClientBean bean2 = ac.getBean(ClientBean.class);
        int count2 = bean2.logic();
        System.out.println("count2 = " + count2);
        Assertions.assertThat(count2).isEqualTo(1);
    }

    @Scope("singleton")
    static class ClientBean {
        // ObjectFactory 나 ObjectProvider 나 아무거나 사용해도 된다.  ObjectProvider가 제공하는 기능이 더 많다.
        @Autowired
        private ObjectProvider<PrototypeBean> prototypeBeanProvider;

        public int logic() {
            PrototypeBean prototypeBean = prototypeBeanProvider.getObject();    // prototypeBean만 찾아주는 역할 수행
            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");
        }
    }
}

ObjectProvider 역시 getObject()를 사용해서 새로운 프로토타입 빈을 생성하는 것을 확인할  수 있습니다.

그럼 결과는 역시...

count1과 count2가 1이라는 값이 나오는 것을 확인할 수 있습니다.

 

이렇게 ObjectFactory와 ObjectProvider를 사용하면 딱 필요한 DL의 기능을 제공합니다.

 

그리고 ObjectFactory와 ObjectProvider의 관계를 알아보죠

예상은 하셨겠지만 상속관계에 있습니다.

ObjectFactory를 상속해서 ObjectProvider가 나왔습니다. 

  • ObjectFactory는 getObject()만을 제공하며 아주 단순합니다.
  • ObjectProvider는 getObject() 뿐만 아니라 다양한 기능을 제공합니다.
  • 그래서 단순한 기능만을 원할 때는 ObjectFactory
  • 다양한 기능을 원할 때는 ObjectProvider 를 사용하면 됩니다.

 

하지만 ObjectFactory, ObjectProvider는 큰 단점이 한 개 있는데...

바로 스프링에서만 사용할 수 있다는 것입니다.

스프링이 제공하는 것이기 때문에 스프링에 의존적입니다.

그래서 스프링을 사용하지 않으면서 DL 기능을 사용하고 싶을 때는 2가지를 사용하지 못합니다.

 

그럼 만약 스프링을 사용하지 않을 때 DL 기능을 사용하고 싶다면 어떻게 해야할까요?

이럴 때는 JSR-330 Provider를 사용합니다.

JSR-330 Provider는 자바가 제공하는 자바 표준입니다.

JSR-330 Provider를 사용하기 위해서는 라이브러리 추가를 진행해야 합니다.

build.gradle 파일에서 다음을 추가합니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'javax.inject:javax.inject:1'
    implementation 'junit:junit:4.12'
    implementation 'junit:junit:4.12'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

바로 implementaion 'javax.inject:javax.inject:1' 을 추가하면 됩니다.

그리고 추가를 한다면 오른쪽 상단에

이런 아이콘이 뜨는 것을 볼 수 있을텐데 눌러서 적용하면 됩니다.

그러면 라이브러리 추가가 완료됩니다!

 

그러면 코드도 바꿔봅니다.

public class SingletonWithPrototypeTest1 {

    @Test
    public void singletonClientUsePrototype() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean bean1 = ac.getBean(ClientBean.class);
        int count1 = bean1.logic();
        System.out.println("count1 = " + count1);
        Assertions.assertThat(count1).isEqualTo(1);

        ClientBean bean2 = ac.getBean(ClientBean.class);
        int count2 = bean2.logic();
        System.out.println("count2 = " + count2);
        Assertions.assertThat(count2).isEqualTo(1);
    }

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

        public int logic() {
            PrototypeBean prototypeBean = prototypeBeanProvider2.get();
            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");
        }
    }
}
  • get() 메서드를 통해 새로운 프로토타입 빈이 생성되는 것을 알 수 있습니다.
  • Provider는 오로지 get() 메서드 하나만 제공해서 기능이 매우 단순합니다.
  • 자바 표준이기 때문에 스프링 환경이 아니더라도 사용이 가능합니다.
  • 라이브러리 추가를 해줘야하는 단점이 있다고 할 수 있습니다.

 

이렇게 DL을 가능하게 해주는 2가지에 대해 알아봤습니다. (ObjectFactory & ObjectProvider 와 JSR-330 Provider)

그리고 프로토타입 빈 생성과 관련되지 않더라도 DL을 사용해야 하는 경우에는

이 3가지를 기억해서 사용하면 됩니다.

 

 

여기서 의문이 생길 수 있습니다.

그러면 DL 기능을 사용하기 위해 무엇을 사용해야 할까요?

스프링이 아닌 환경을 고려해서 JSR-330 Provider를 적극적으로 사용하면 되는 걸까요?

아니면 편하게 ObjectProvider를 쓰면 될까요?

 

이것은 본인이 원하는대로 하면 된다고 합니다 ㅎㅎ

ObjectProvider 처럼 다양한 기능을 제공하는 것을 사용하고 싶다면 이것을 쓰면 된다고 합니다.

스프링 내에서는 확실히 동작하니 큰 문제는 없다고 합니다.

물론 스프링을 사용하지 않을 때는 무조건 JSR-330 Provider를 사용해야 합니다...

 

 

 

프로토타입 빈에 대해 다시 한번 생각해보겠습니다.

프로토타입 빈은 사용할 때마다 의존관계 주입이 완료된 새로운 객체가 필요한 경우에 사용하면 됩니다.

하지만 싱글톤 빈으로 대부분 해결이 가능하다고 하는군요 ㅎㅎ

프로토타입 빈이 있다는 것을 알아두기만 하면 될 것 같습니다.

 

 

 

 

그러면 이렇게 해서 프로토타입과 싱글톤 빈 같이 쓸 때 해결법

에 대한 포스트를 마치겠습니다.

 

 

 

 

 

 

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