Study/Spring

[Spring] 싱글톤 스코프에서 프로토타입 스코프 사용 시 문제점과 해결 방법

lsh2613 2023. 8. 1. 23:13

먼저 싱글톤과 프로토타입의 차이점에 대해 알아보자

싱글톤 VS 프로토 타입

싱글톤 

  • 스프링 컨테이너  생성 시 빈 객체가 같이 생성되며 컨테이너 종료 시 같이 소멸한다.
  • 객체 인스턴스가 한 번만 생성되고 각 클라이언트에서 같은 객체를 공유하여 사용한다

프로토타입

  • 스프링 컨테이너가 종료되고 같이 소멸하지 않고, 클라이언트에서 더 이상 사용되지 않으면 가비지 컬렉터에 의해 수거된다.
  • 클라이언트에서 호출 될 때마다 새로운 객체를 반환한다.

 

프로토 타입 설정과 테스트

package hello.core.scope;

import org.junit.jupiter.api.Test;
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.AssertionsForClassTypes.assertThat;

public class PrototypeTest {
    @Test
    public void prototypeBeanFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        System.out.println("prototypeBean1 = " + prototypeBean1);
        System.out.println("prototypeBean2 = " + prototypeBean2);
        assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
        ac.close(); //종료
    }
    @Scope("prototype")
    static class PrototypeBean {
        @PostConstruct
        public void init() {
            System.out.println("PrototypeBean.init");
        }
        @PreDestroy
        public void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    } }

코드 풀이

@Scope를 통해 프로토 타입을 지정할 수 있다.

해당 프로토타입의 빈을 getBean을 통해 꺼냈을 때 서로 다른 객체임을 확인할 수 있다.

 

문제가 발생하는 코드

package hello.core.scope;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
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.AssertionsForClassTypes.assertThat;

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(2);
    }
    @Scope("singleton")
    static class ClientBean {
        private final PrototypeBean prototypeBean;
        @Autowired
        public ClientBean(PrototypeBean prototypeBean) {
            this.prototypeBean = prototypeBean;
        }
        public int logic() {
            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");
        }
    }
}

 

코드 풀이

싱글톤인 ClientBean에서 PrototypeBean을 의존관계 주입을 받아 사용하고 있다.

테스트 코드에서는 분명 ClientBean을 두 번 호출하여 각각 로직을 수행하였을 때 count값을 증가하였지만 count값이 공유되고 있음을 확인할 수 있다.

 

그 이유는 싱글톤인 ClientBean에서 생성 시점에 PrototypeBean에 대한 의존관계가 이미 이루어졌기 때문에 그렇다.

 

prototype은 호출 시 새로운 객체를 사용하기 위해 사용하는 건데 위 문제를 해결하는 방법은 뭐가 있을까?

 

해결 방법

1. 스프링 프레임워크에서 지원하느 ObjectProvider
2. 자바 표준에서 지원하는 javax.inject.Provider
  2.1 스프링 3.0 이상에서는 jakarta.inject.Provider

1. ObjectProvider

@Autowired
  private ObjectProvider<PrototypeBean> prototypeBeanProvider;
  public int logic() {
      PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
      prototypeBean.addCount();
      int count = prototypeBean.getCount();
      return count;
}

2. Provider

@Autowired
  private Provider<PrototypeBean> provider;
  public int logic() {
      PrototypeBean prototypeBean = provider.get();
      prototypeBean.addCount();
      int count = prototypeBean.getCount();
      return count;
}

 

DL (Dependency Lookup)

이처럼 외부에서 의존관계를 주입하는 것이 아니라 직접 필요한 의존관계를 찾는 것을 말한다.