들어가며

  • 이전 포스트에서 빈 스코프의 개념과 프로토타입 빈에 대해 알아보았다. 그리고 프로토타입 빈이 싱글톤 빈과 같이 쓰일때 생길 수 있는 문제에 대해서도 알아보았다.
  • 이번 포스트에서는 해당 문제에 대해 짤막히 리마인드 하고, 해당 문제를 해결할 수 있는 방법에 대해 알아본다.
 

Spring Framework - 빈 스코프 - 빈 스코프와 프로토타입 스코프

들어가며 이번 포스트부터는 빈 스코프에 대해 알아보고 여러가지 스코프에 대해서 알아보도록 한다. 빈 스코프의 개념과 스코프의 종류 빈 스코프의 개념 지금까지 우리는 스프링 컨테이너가

sehun5515.tistory.com

 

 

 

싱글톤 빈, 프로토타입 빈 동시 사용시 발생하는 문제

  • 싱글톤 빈 내부에 프로토타입 빈을 주입하여 사용해야 하는 상황을 생각해 보자.
  • 이 경우에 스프링 컨테이너가 실행되면 다음과 같은 과정을 통해 싱글톤 빈이 생성되게 된다.
    • 먼저 싱글톤 빈이 생성된다. 이 과정에서 프로토타입 빈을 주입받아야 하는데 아직 프로토타입 빈이 생성되지 않았다. 따라서 스프링 컨테이너에 프로토타입 빈을 요청하게 된다.
    • 프로토타입 빈은 그 정의에 의하여 새로운 빈 인스턴스를 만들어 싱글톤 빈에게 반환한다. 즉 싱글톤 빈에 새로운 프로토타입 빈 인스턴스를 주입한다.
  • 이후 클라이언트 객체에서 해당 싱글톤 빈이 필요할 경우 요청을 보내고, 스프링 컨테이너가 해당 싱글톤 빈을 클라이언트 객체에 주입한다(DI)
  • 문제는 다른 클라이언트가 다시 이 싱글톤 빈을 요청하는 경우이다.  이 클라이언트는 새로운 프로토타입 빈을 주입받은 싱글톤 빈을 원하지만 싱글톤 빈은 이미 생성될때 프로토타입 빈을 하나 주입받았기 때문에 또다시 새로운 프로토타입 빈을 받지 않는다.
  • 따라서 다른 클라이언트가 싱글톤 빈을 주입받을 때 그 내부에 있는 프로토타입 빈은 이전에 이미 주입받은 프로토타입 빈이라는 문제가 생긴다.

 

 

 

문제 해결 - Provider

  • 위 문제에서 우리가 원하는 동작은 싱글톤 빈을 주입받을 때 마다 새로운 프로토타입 빈이 생성되어 해당 싱글톤 빈에 주입되기를 원한다.
  • 가장 간단한 방법은 싱글톤 빈을 호출할 때 마다 새로운 프로토타입 빈을 생성해 받도록 컨테이너에 새로 요청하는 것이다. 코드로 간단히 살펴보자.
@Scope("prototype")
public class PrototypeBean{
    private int count = 0;
    
    public void addCount(){
        count++;
    }
    
    public int getCount(){
        return count;
    }
}
public class SingletonBean{
    private final ApplicationContext ac;
    
    @Autowired
    SingletonBean(ApplicationContext ac){
        this.ac = ac;
    }
    
    public int logic(){
        PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
        prototypeBean.addCount();
        
        return prototypeBean.getCount();
    }
}
public class Client{
    public void addCount(){
        AnnotationConfigApplicationContext ac = 
            new AnnotationConfigApplicationContext(SingletonBean.class, PrototypeBean.class);
        
        SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
        int prototypeBeanCount1 = singletonBean1.logic();
        
        SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
        int prototypeBeanCount2 = singletonBean2.logic();
        
        System.out.println(prototypeBeanCount1 == prototypeBeanCount2);
    }
}
  • SingletonBean 클래스의 logic 부분을 자세히 보자. ApplicationContext를 통해 스프링 컨테이너에 매번 프로토타입 빈을 요청하고 있다.
  • 이렇게 한다면 logic을 실행할 때 마다 새로운 프로토타입 빈 인스턴스가 반환될 것이고 해당 인스턴스의 count를 1 올리기 때문에 아까과 같은 문제가 발생하지 않는다.
  • 이렇게 의존관계를 외부에서 주입받는게 아닌 직접 getBean과 같이 찾는것을 의존관계 조회(Dependency Lookup)이라고 한다.
    • 문제는 위의 logic을 보면 ac를 주입받아서 프로토타입 빈을 찾고 있는데 이렇게 하면 ac 전체를 모두 가져오는 것이기 때문에 단위 테스트가 어려워진다. 그리고 스프링 컨테이너를 주입받는 것이기 때문에 스프링에 의존적인 코드가 된다.
  • 위의 logic은 ac.getBean()으로 정말 딱 자신이 필요한 빈만 가져올 수 있을 정도의 기능만 있으면 모두 대체가 가능하다. 즉 의존관계 조회 "만" 가능한 정도의 무언가가 있으면 logic 상에서 ac의 필요성이 사라진다.

ObjectFactory, ObjectProvider

  • 위의 의존관계 조회를 해주는 서비스를 제공하는것이 두 가지가 존재하는데 그것이 바로 ObjectFactory와 ObjectProvider이다.
  • 즉 다음과 같이 SingletonBean 코드를 만들 수 있다.
public class SingletonBean{
    private final ObjectProvider<PrototypeBean> objectProvider;
    
    @Autowired
    SingletonBean(ObjectProvider<PrototypeBean> obejctProvider){
        this.objectProvider = objectProvider;
    }
    
    public int logic(){
        PrototypeBean prototypeBean = objectProvider.getObject();
        prototypeBean.addCount();
        
        return prototypeBean.getCount();
    }
}
  • ObjectProvider의 getObject()는 스프링 컨테이너에서 해당 빈을 조회하여 반환하는 역할을 한다. 해당 객체 역시 스프링 프레임워크에 종속되어 있기 때문에 코드가 스프링에 의존하는 상황이 되버리지만, ApplicationContext 전체를 가져와서 찾는 것 보다는 훨씬 가볍다.
  • 위의 가벼운 장점으로 테스트를 수행하기가 쉬워지게 된다.
  • 원래 ObjectProvider의 이전 버전으로 ObjectFactoy가 존재하였으나 해당 객체에 몇 가지 편의 기능을 추가하여 새롭게 만든게 ObjectProvider이다.

JSR-330 Provider

  • 이 방법은 JSR-330이라는 자바 표준의 Provider를 사용하는 방법이다.
    • SpringBoot 3.0 이상부터는 jakarta.inject.Provider로 변경되었다.
  • 해당 방법을 사용하려면 gradle에 의존성을 추가해야 한다. 해당 방법을 사용한 SingletonBean 코드는 다음과 같다.
public class SingletonBean{
    private final Provider<PrototypeBean> ㅔrovider;
    
    @Autowired
    SingletonBean(Provider<PrototypeBean> provider){
        this.provider = provider;
    }
    
    public int logic(){
        PrototypeBean prototypeBean = provider.get();
        prototypeBean.addCount();
        
        return prototypeBean.getCount();
    }
}
  • 해당 방법은 자바 표준으로 지정되어 있는 방식이기 때문에 스프링 프레임워크에 독립적이다. 즉 스프링 프레임워크가 아닌 다른 프레임워크를 사용하게 되더라도 해당 코드가 동작하는것이 보장된다.
  • 또한 기능 역시 단순하기 때문에 가볍고 이는 스프링에 종속적이지 않은 순수 자바 테스트 코드를 만들기가 쉬워진다.
복사했습니다!