본문 바로가기
WEB/Spring

[Spring] 스프링 빈 스코프(Scope) 싱글톤, 프로토 타입 - 1/2

by 정권이 내 2022. 6. 19.

File:Spring Framework Logo 2018.svg - Wikimedia Commons

 

 

[Spring] 스프링 빈 스코프(Scope) 싱글톤, 프로토 타입

이전 포스팅에서 스프링 빈의 생명주기와 초기화, 소멸 콜백 메소드에 대해서 알아보았습니다. 스프링 빈은 기본적으로 스프링 컨테이너가 종료될때까지 유지되는데 스프링 빈의 기본 스코프타입이 싱글톤이기 때문입니다.

 

스코프(Scpoe)는 스프링 빈이 존재할수 있는 범위를 뜻합니다.

  • 싱글톤 타입 : 스프링 컨테이너의 시작 ~ 종료시점까지 유지되는 기본적인 범위의 스코프
  • 프로토 타입 : 스프링 빈의 생성과 의존관계 주입시점까지만 스프링 컨테이너에서 관리된다.

 

프로토 타입 스코프

프로토 타입 스코프는 다음과 같은 특징이 있습니다.

  • 스프링 빈을 가져올때마다 싱글톤 타입 빈과 다르게 계속 생성되고 초기화 콜백 메서드도 매번 수행합니다.
  • 스프링 컨테이너가 종료되도 소멸 콜백메서드를 수행하지 않습니다.

 

싱글톤타입과 프로토타입을 각각 테스트하여 어떤 차이점이 있는지 알아보겠습니다.

 

싱글톤 타입 스코프 테스트

public class SingletonTest {

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

Assertions.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 = com.example.springdemostudy.scope.SingletonTest$SingletonBean@5fd62371
singletonBean2 = com.example.springdemostudy.scope.SingletonTest$SingletonBean@5fd62371
16:44:10.158 [Test worker] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@447a020, started on Fri May 27 16:44:10 KST 2022
SingletonBean.destroy

 

두개의 SingletonTest 객체가 스프링 빈으로 관리되기 때문에 동일한 값을 보여주고 초기화, 소멸 콜백 메서드도 정상적으로 호출 되는것을 확인할수 있습니다.

 

프로토 타입 스코프 테스트

public class PrototypeTest {

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

Assertions.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");
}
}
}

 

<결과>

find prototypeBean1
PrototypeBean.init
find prototypeBean2
PrototypeBean.init
prototypeBean1 = com.example.springdemostudy.scope.PrototypeTest$PrototypeBean@372ea2bc
prototypeBean2 = com.example.springdemostudy.scope.PrototypeTest$PrototypeBean@4cc76301
16:48:55.265 [Test worker] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@447a020, started on Fri May 27 16:48:55 KST 2022

 

PrototypeBean 객체를 가져올때 마다 초기화 콜백 메서드인 init() 함수를 호출하고 스프링 빈을 새로 생성하므로 서로 다른 값을 나타내는것을 확인할수 있습니다. 그리고 스프링 컨테이너가 종료되도 소멸 콜백 메서드인 destroy 메서드가 호출되지 않고 있습니다.

 

단, 프로토 타입 스프링 빈을 종료하려면 수동으로 소멸 콜백 메서드를 호출하는 방법이 있습니다.

prototypeBean1.destroy();
prototypeBean2.destroy();

 

 

프로토 타입과 싱글톤 타입을 같이 사용할때 문제점

스프링 컨테이너에 프로토타입의 스프링 빈을 요청하면 항상 새로운 객체 인스턴스를 반환해주지만 싱글톤 타입과 함께 사용하면 의도와 다르게 동작할수 있으므로 주의해야 합니다.

 

먼저 프로토타입만 사용했을때의 예제 코드입니다.

프로토 타입만 사용

public class SingletonWithPrototypeTest {

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

@Scope("prototype")
@Getter
static class PrototypeBean {
private int count = 0;

public void addCount() {
count++;
}

@PostConstruct
public void init() {
System.out.println("PrototypeBean.init" + this);
}

@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}

 

위 테스트 코드를 실행하면 오류없이 정상적으로 실행되는것을 확인할수 있습니다. prototypeBean1, prototypeBean2 객체는 프로토 타입이기 때문에 스프링 컨테이너에서 객체 인스턴스를 받아올때마다 새로 생성되기 때문에 getCount 메서드의 반환값이 모두 1로 나오게 되는것입니다.

 

이번엔 싱글톤 타입 빈에서 프로토 타입 빈을 주입받는 경우를 예제 코드로 확인해보겠습니다.

 

싱글톤 빈에서 프로토타입 빈 사용

public class SingletonWithPrototypeTest {

@Test
void singletonClientUsePrototype() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class, ClientBean.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;

// prototypeBean 은 생성시점에 주입되고 ClientBean이 관리하게 된다.
@Autowired
public ClientBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}

public int logic() {
prototypeBean.addCount();
return prototypeBean.getCount();
}
}

@Scope("prototype")
@Getter
static class PrototypeBean {
private int count = 0;

public void addCount() {
count++;
}

@PostConstruct
public void init() {
System.out.println("PrototypeBean.init" + this);
}

@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}

 

테스트 메서드를 수행하면 정상으로 나오는데 의문점은 clientBean1, clientBean2 각 객체를 생성하여 logic 메서드를 수행할때 싱글톤 타입인 ClientBean 객체에서 프로토 타입인 PrototypeBean 빈을 새로 생성하여 addCount() 메서드를 수행하면 count 값이 각각 1이 나와야할것 같지만 count는 공유되는 값처럼 2가 나오게 됩니다.

 

왜냐하면 최초에 ClientBean 클래스에 의존관계 주입시 PrototypeBean 객체를 스프링빈에 등록했을때 PrototypeBean은 스프링 컨테이너의 권한의 영역에서 벗어나 ClientBean의 소유가 되었기 때문에 ClientBean 객체를 새로 생성하더라도 싱글톤 타입이라 스프링빈은 공유가 되기 때문에 PrototypeBean은 ClientBean 빈을 따라가게 되는것입니다.

 

이것이 싱글톤타입 빈과 프로토타입 빈을 같이 사용했을때 문제가 되는 부분입니다. 프로토 타입을 사용한다는것은 객체를 사용할때마다 새로 생성하는것을 목적으로 하기 때문입니다.

 

물론 아래와 같은 방법으로 해결할수도 있지만 좋은 방법은 아닙니다.

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

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

 

이 방법은 의존관계를 외부에서 주입받는 DI(Dependency Injection)이 아니라 필요한 의존관계를 직접 찾는 DL(Dependency Lookup) 이라고 합니다. 하지만 이 방법은 애플리케이션 컨텍스트 전체를 주입받아야 하고 스프링 컨테이너에 종속적인 코드가 되기때문에 좋은 방법이 아닙니다.

 

프로토 타입과 싱글톤 타입을 같이 사용하는 방법

1. ObjectProvider

ObjectProvider는 ObjectFactory를 상속받아 편의성 기능을 추가한 클래스이며 지정한 스프링 빈을 스프링 컨테이너에서 찾아서 반환해주는 DL 서비스를 제공합니다.

 

@Scope("singleton")
static class ClientBean {

@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanObjectProvider;

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

ObjectProvider의 getObject() 메서드에서 새로운 PrototypeBean 빈을 생성하여 Client 객체마다 각각의 PrototypeBean을 사용할수 있게 해줍니다.

 

ObjectProvider의 특징은 다음과 같습니다.

  • ObjectFactory 상속받음
  • 옵션, 스트림 처리등 편의성 기능이 많다.
  • 별도의 라이브러리가 필요없지만 스프링에 의존적이다.

 

2. JSR-330 Provider

javax.inject.Provider 라는 JSR-330 자바 표준을 사용하는 방법입니다. 이 방법을 사용하려면 javax.inject:javax.inject:1 라이브러리를 gradle에 추가해야 합니다.

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

 

@Scope("singleton")
static class ClientBean {

@Autowired
private Provider<PrototypeBean> prototypeBeanObjectProvider;

public int logic() {
PrototypeBean prototypeBean = prototypeBeanObjectProvider.get();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}

Provider의 get 메서드를 통해 새로운 프로토타입 빈이 생성됩니다. JSR-330 Provider는 자바 표준이면서 기능이 단순하므로 단위테스트를 만들거나 mock코드를 만들기 쉽습니다.

 

JSR-330 Provider의 특징은 다음과 같습니다.

  • get() 메서드 하나로 기능이 매우 단순하다.
  • 별도의 라이브러리 추가가 필요하다.
  • 자바표준이므로 스프링에 의존하지 않는다.

 

프로토타입 빈은 언제 사용하는가??

프로토타입 빈은 사용할때마다 의존관계 주입이 완료된 새로운 객체가 필요할때 사용하는데 실무에서는 대부분의 경우 싱글톤 빈으로 해결할수 있기 때문에 사용하는 경우는 많지 않습니다.

 

하지만 사용해야 하는 경우가 생긴다면 스프링 방식의 ObjectProvider와 자바표준의 JSR-330 Provider 방식중 선택해야 하는데 애플리케이션의 개발 환경이 스프링 의존적이라면 ObjectProvider를 사용하고 스프링이 아닌 다른 환경에서도 사용해야 한다면 JSR-330 Provider 방식을 사용하면 됩니다.

 

 

 

 

테스트 소스(GitHub)

 

GitHub - rlatjd1f/spring-demo-study: spring study

spring study. Contribute to rlatjd1f/spring-demo-study development by creating an account on GitHub.

github.com

출처 : 인프런 - 우아한 형제들 기술이사 김영한의 스프링 완전 정복 (스프링 핵심원리 - 기본 편)

반응형

댓글