[spring] 빈스코프 : 프로토타입 스코프 (스프링 기본편 by 김영한)

su_y2on·2022년 2월 3일
0

Spring

목록 보기
25/30
post-thumbnail

빈스코프 : 프로토타입 스코프


빈 스코프는 빈이 존재할 수 있는 범위를 뜻합니다 이때 범위는 시간이라고 보시면 됩니다! 아래와 같이 총 3가지 종류가 있습니다

  • 싱글톤 : 이제 까지 주로 다뤘던 빈들이 여기에 속합니다. 스프링 컨테이너의 시작과 끝을 함께하며 가장 범위가 넓은 스프링 빈입니다
  • 프로토타입: 프로토 타입 빈의 생성과 의존성 주입이 되면 그 뒤로는 스프링 컨테이너가 더 이상 관리하지 않습니다. 따라서 매우 짧은 범위를 갖습니다
  • 웹 관련
    - request: 웹 요청이 들어오고 나갈 때 까지 유지
    - session: 웹 세션이 생성되고 종료될 때 까지 유지
    - application: 웹의 서블릿 컨텍스트와 같은 범위로 유지




스코프를 지정해주는 방법컴포넌트 스캔이라면 @Component와 함께 @Scope를 사용하고 수동등록방식이라면 @Bean과 함께 @Scope를 적어주면 됩니다.

@Scope("prototype")
@Component
public class HelloBean {}
@Scope("prototype")
  @Bean
  PrototypeBean HelloBean() {
      return new HelloBean();
  }

프로토타입 빈은 스프링 컨테이너에서 생성과 의존성주입 후에 관리를 하지 않기 때문에 모든 요청에서 매번 새로 스프링 빈을 생성해서 반환합니다.

따라서 스프링빈의 생명주기 때 활용했던 초기화 함수와 종료함수 중 종료함수는 호출되지 않습니다!!




테스트

이제 테스트로 직접확인해보도록 하겠습니다

싱글톤

먼저 비교를 위해 싱글톤 빈부터 테스트를 진행해보겠습니다. 출력은 예상과 같이 bean1, bean2가 같은 객체로 나오며 초기화, 종료 메서드가 모두 호출됩니다.

 
 @Test
 public void singletonBeanFind() {
 
 	AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
 
 	SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
 	SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
 
 	System.out.println("singletonBean1 = " + singletonBean1);
 	System.out.println("singletonBean2 = " + singletonBean2);
 
 	assertThat(singletonBean1).isSameAs(singletonBean2);
 	ac.close(); //종료 
}
@Scope("singleton")
static class SingletonBean {

	@PostConstruct
	public void init() {
		System.out.println("SingletonBean.init");
          }
          
     @PreDestroy
     public void destroy() {
              System.out.println("SingletonBean.destroy");
          }
}

출력값

SingletonBean.init
singletonBean1 = hello.core.scope.PrototypeTest$SingletonBean@54504ecd
singletonBean2 = hello.core.scope.PrototypeTest$SingletonBean@54504ecd 
org.springframework.context.annotation.AnnotationConfigApplicationContext -
Closing 
SingletonBean.destroy



프로토타입

이번에도 같은 방식으로 프로토타입을 테스트해보도록 하겠습니다. bean1과 bean2가 다른것을 알 수있습니다

@Test
public void prototypeBeanFind() {
	AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
    
	System.out.println("find prototypeBean1");
	PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class); 		
    System.out.println("find prototypeBean2");
	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");
          }
          
}

출력값

getBean을 할 때 초기화가 진행되지만 ac.close를 해도 Closing이라는 스프링 컨테이너의 종료만 출력될 뿐 prototype bean의 종료메서드는 호출되지 않았습니다.

find prototypeBean1
PrototypeBean.init
find prototypeBean2
PrototypeBean.init
prototypeBean1 = hello.core.scope.PrototypeTest$PrototypeBean@13d4992d
prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@302f7971
org.springframework.context.annotation.AnnotationConfigApplicationContext -
Closing




프로토타입빈 + 싱글톤타입빈

그런데 이런 프로토타입빈이 싱글톤과 함께 쓰이면 문제가 생길 수 있습니다!
만약 싱글톤 타입빈을 통해 프로토타입빈을 호출하는 상황이라면 프로토타입의 스코프가 예상과 다르게 요청할 때마다 생성하지 않습니다

아래의 테스트로 확인해보록 하겠습니다. 먼저 clientBean이라는 싱글톤 빈을 먼저 생성하고 그 빈의 logic이라는 함수를 수행해서 프로토타입 빈안에 count를 올려주도록 하겠습니다! 유저와 프로토타입빈 사이에 clientbean이 꼈다고 생각하면됩니다

@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);
        
    }

싱글 톤 빈으로 사용할 clientBean부터 만들어주도록 하겠습니다. singleton은 default값이기 때문에 생략해도됩니다. 이 빈에서 프로토타입 빈을 의존관계주입 받습니다

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;
		} 
}

그뒤로 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");
        }
        
}

결과는 assertion에 쓴 그대로 요청1일때 count가 1 요청2일때 count가 2가 됩니다. 결국 새로운 프로토타입빈이 생성된 것이 아니라는 겁니다. 프로토타입빈이 생성되고 초기화 후에 관리가 안되는 것은 맞지만 그것이 싱글 톤 안에 있기 때문에 요청2도 똑같은 clientBean에 요청이 되고 그 안에 있는 프로토타입빈은 같은 것이기 때문입니다.





해결 : DL(Dependency Lookup)


1. 스프링 컨테이너에 매번 요청하기

이 문제를 해결하는 가장 간단한 방법은 의존관계를 주입받는게 아니라 매번 스프링 컨테이너에 요청을 해서 가져다 쓰는 것입니다. clinetBean 안에서 ac를 만들어주고 이를 스프링 컨테이너에서 주입받은 뒤에 프로토타입 빈을 필요할 때마다 요청해서 사용합니다.

static class ClientBean {
          @Autowired
          private ApplicationContext ac;
          
          public int logic() {
              PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
              prototypeBean.addCount();
              int count = prototypeBean.getCount();
              
              return count;
		  } 
}
 

이렇게 하면 매번 새로운 프로토타입빈이 logic을 호출할 때마다 생성됩니다! 이렇게 직접 필요한 의존관계를 찾아오는 것을 DL이라고 합니다. 그런데 이방법의 문제는 스프링 컨테이너에 의존적이며 단위테스트도 어려워집니다

좀더 가벼운 DL기능을 제공하는 무언가가 있으면 좋을 것같습니다..🥲


2. ObjectProvider

그런 것이 바로 objectProvider입니다. 컨테이너에서 대신 빈을 가져다주는 역할을 합니다. ObjectProvider에 타입을 생성할 프로토타입빈을 넣어서 객체를 생성해줍니다. 그리고 이를 이용해 getObject()로 가져오면됩니다! 여기서 등록해 준 적도 없는 ObjectProvider빈을 @Autowired로 받을 수 있는 것은 스프링 컨테이너에 애초에 들어가 있는 것이기 때문입니다.

@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;

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



3. JSR-330 Provider

ObejectProvider는 매우 간편하지만 여전히 스프링에 의존적입니다. 그래서 마지막 방법은 자바표준을 사용하는 방법입니다.

쓰기전에 라이브러리를 gradle에 추가해줘야합니다!

implementation 'javax.inject:javax.inject:1'

이제 clientBean에 logic함수에 적용해보도록 하겠습니다. 2번 방법과 비슷하지만 get이라는 간단한 함수로 빈을 가져다 쓸수 있습니다.

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

이는 스프링뿐만 아니라 다른 컨테이너에서도 사용가능합니다 (스프링이 아닌 다른 컨테이너를 쓸일은 물론 많이 없습니다만..ㅎㅎ)




위에서 말한 DL기술들은 프로토타입빈을 사용할 때만 한정적으로 사용하는 기술이 아닌 DL이 필요한 모든곳에서 사용하면됩니다. 프로토타입빈은 요청마다 매번 새로운 빈을 만들어줘야할 때 사용하면 되지만 싱글톤에 비해서는 잘 사용할일이 없긴합니다🐥

0개의 댓글