FrameWork/Spring&Spring-boot

Spring 기본 36 - 프로토타입 스코프(싱글톤 빈과 함께 사용시 Provider로 문제 해결)

Surge100 2023. 11. 30. 11:26

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

싱글톤 빈과 프로토타입 빈을 함께 사용할 때, 어떻게 하면 사용할 때 마다 항상 새로운 프로토타입 빈을 생성할 수 있을까?

 

스프링 컨테이너에 요청

가장 간단한 방법은 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청하는 것이다.

package com.hello.core.scope;

import lombok.RequiredArgsConstructor;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import static org.assertj.core.api.Assertions.*;

public class SingletonWithPrototypeTest1 {

    @Test
    void prototypeFind(){
        AnnotationConfigApplicationContext ac =
                new AnnotationConfigApplicationContext(PrototypeBean.class);

        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        prototypeBean1.addCount();

        assertThat(prototypeBean1.getCount()).isEqualTo(1);

        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        prototypeBean2.addCount();

        assertThat(prototypeBean2.getCount()).isEqualTo(1);
    }

    @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  ApplicationContext ac;

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

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

 

핵심코드

@Scope("singleton")
static class ClientBean{
    private  ApplicationContext ac;

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

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

 

  • 실행해보면 `ac.getBean()`을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
  • 의존관계를 외부에서 주입(DI)받는게 아니라 이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL) 의존관계 조회(탐색)이라고 한다.
  • 그런데 이렇게 스프링 애플리테이션 컨텍스트 전체를 주입받게 되며, 스프링 컨테이너에 너무 종속적이 코드가 되고, 단위 테스트도 어려워진다.
  • 지금 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 딱! DL 정도의 기능남 제공하는 무언가가 있으면 된다.

 

ObjectFactory, ObjectProvider

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

static class ClientBean{

    @Autowired
    private ObjectProvider<PrototypeBean> prototypeBeanProvider;

    public int logic(){
        PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
        prototypeBean.addCount();
        return prototypeBean.getCount();
    }
}
  • 실행해보면 `prototypeBeanProvider.getObject()`을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
  • `ObjectProvider`의 `getObject()`를 호출하면 내부에서 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.(*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();
}

 

@Scope("singleton")
static class ClientBean{

    @Autowired
    private Provider<PrototypeBean> prototypeBeanProvider;

    public int logic(){
        PrototypeBean prototypeBean = prototypeBeanProvider.get();
        prototypeBean.addCount();
        return prototypeBean.getCount();
    }
}
  • 실행해보면 `provider.get()`을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
  • `provider`의 `get()`을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.(*DL*)
  • 자바 표준이고, 기능이 단순하므로 단위테스트를 만들거나 mock코드를 만들기는 훨씬 쉬워진다.
  • `Provider`는 딱 필요한 DL 정도의 기능만 제공한다.

 

특징

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

 

정리

  • 그러면 프로토타입 빈을 언제 사용할까? 매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용하면 된다. 그런데 실무에서 웹 어플리케이션을 개발해보면, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.
    • 지연(lazy)해서 가져오거나 optional로 객체 인스턴스를 가져 올 때
    •  A가 B를 의존하고 B가A를 의존할 때 (순환참조,breaking circular dependencies)
  • `ObjectProvider`,`JSR330 Provider`등은 프로토타입 뿐만 아니라 DL이 핑요한 경우는 언제든지 사용할 수 있다.

 

 

참고
스프링이 제공하는 메서드에`@Lookup`애노테이션을 사용하는 방법도 있지만, 고려해야 할 사항이 많다.

 

 

 

[출처 - 스프링 핵심 원리 - 기본편]

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보세요! 📢

www.inflearn.com