[Spring] 빈의 Scope - 싱글톤과 프로토타입

2021. 4. 21. 01:57 Spring Framework/Spring Core

 

[Spring] 빈의 Scope - 싱글톤과 프로토타입

빈을 등록할 때 아무런 설정을 하지 않으면 기본적으로 빈은 싱글톤 scope을 갖는다.

싱글톤 scope이란 어플리케이션 전반에 걸쳐 해당 빈의 인스턴스를 오직 하나만 생성해서 사용하는 것이다.

 

1. Singleton Scope

Single, Proto 클래스를 새로 만들고 @Component를 붙여 빈으로 등록한다.

 

Single.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
 
@Component
public class Single {
 
    @Autowired
    private Proto proto;
 
    public Proto getProto() {
        return proto;
    }
}

 

Proto.java

import org.springframework.stereotype.Component;
 
@Component
public class Proto {
}

 

Single에 Proto를 주입한다.

 

AppRunner.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
 
@Component
public class AppRunner implements ApplicationRunner {
 
    @Autowired
    Single single;
 
    @Autowired
    Proto proto;
 
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println(proto);
        System.out.println(single.getProto());
    }
}

 

ApplicationRunner를 만들고 Single과 Proto를 주입받아 runner가 주입받은 Proto와 Single에서 가져온 Proto를 출력해보자.

 

실행 결과

출력 결과로 두 레퍼런스가 하나의 동일한 인스턴스인것을 확인할 수 있다.

Proto를 빈으로 등록할때 scope을 따로 설정해주지 않았으므로 싱글톤 scope을 갖고, 어플리케이션이 시작할때 생성되는 하나의 인스턴스를 쓰기 때문이다.

 

1) 싱글톤 빈 사용시 주의할 점

- 프로퍼티 공유

싱글톤 객체의 프로퍼티는 thread-safe하다고 보장할 수 없다.

멀티쓰레드 환경에서 싱글톤 객체의 프로퍼티는 여러 쓰레드에 의해 바뀔 수 있는데

가령 쓰레드 A에서 프로퍼티 값을 x로 바꾸고 출력하는 과정에서 쓰레드 B가 프로퍼티 값을 y로 바꾸면 쓰레드 A 입장에서는 예상치 못한 결과가 나올수도 있는것이다.

 

- Application 초기 구동 시 인스턴스 생성

싱글톤 빈은 모두 기본적으로 어플리케이션 구동 시 생성되므로 싱글톤 빈이 많을 수록 구동 시간이 증가할 수 있다.

 

2. Prototype Scope

빈의 scope 설정은 @Scope 애노테이션에 설정할 수 있다.

프로토타입 scope으로 설정하려면 @Scope("prototype")과 같이 문자열로 지정해준다.

 

Proto.java

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
 
@Component @Scope("prototype")
public class Proto {
}

 

Proto만 @Scope를 붙여 scope를 프로토타입으로 설정한다.

 

AppRunner.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
 
@Component
public class AppRunner implements ApplicationRunner {
 
    @Autowired
    ApplicationContext ctx;
 
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("Proto:");
        System.out.println(ctx.getBean(Proto.class));
        System.out.println(ctx.getBean(Proto.class));
        System.out.println(ctx.getBean(Proto.class));
 
        System.out.println("Single:");
        System.out.println(ctx.getBean(Single.class));
        System.out.println(ctx.getBean(Single.class));
        System.out.println(ctx.getBean(Single.class));
    }
}

 

Runner에서 ApplicationContext를 주입받고 Proto와 Single 빈을 세번씩 출력해보자.

 

실행 결과

Proto는 모두 다른 인스턴스이고 Single은 모두 같은 인스턴스를 가리킨다.

 

프로토타입 scope는 싱글톤 scope과 달리 IoC에서 빈을 받아올때마다 매번 인스턴스를 새로 생성한다.

 

이렇게 빈의 scope를 간단하게 관리해줄 수 있는 것이 spring의 장점이다.

그러나 프로토타입 빈과 싱글톤 빈을 섞어서 사용하는 것은 문제가 발생할 수 있다.

 

3. 프로토타입 빈과 싱글톤 빈을 섞어서 사용

1) 프로토타입 빈에서 싱글톤 빈을 참조하는 경우

이 경우는 아무 문제가 없다.

아래와 같이 프로토타입 빈 Proto에서 싱글톤 빈 Single을 사용한다고 가정해보자.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
 
@Component @Scope("prototype")
public class Proto {
    
    @Autowired
    Single single;
}

 

프로토타입 빈의 인스턴스는 계속 생성되고 주입받는 싱글톤 빈은 계속 동일한 하나의 인스턴스일것이다.

이러한 결과는 개발자의 기대, 의도에서 벗어나지 않는다.

 

2) 싱글톤 빈에서 프로토타입 빈을 참조하는 경우

이 경우는 문제가 발생할 수 있다.

싱글톤 빈의 인스턴스는 단 한번만 생성되고 그 때 프로토타입 빈의 주입도 이미 완료된다.

그렇기 때문에 싱글톤 빈을 사용할때 주입받은 프로토타입 빈이 변경(업데이트) 되지 않는다.

 

AppRunner.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
 
@Component
public class AppRunner implements ApplicationRunner {
 
    @Autowired
    ApplicationContext ctx;
 
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("Proto:");
        System.out.println(ctx.getBean(Proto.class));
        System.out.println(ctx.getBean(Proto.class));
        System.out.println(ctx.getBean(Proto.class));
 
        System.out.println("Single:");
        System.out.println(ctx.getBean(Single.class));
        System.out.println(ctx.getBean(Single.class));
        System.out.println(ctx.getBean(Single.class));
 
        System.out.println("Proto by Single:");
        System.out.println(ctx.getBean(Single.class).getProto());
        System.out.println(ctx.getBean(Single.class).getProto());
        System.out.println(ctx.getBean(Single.class).getProto());
    }
}

 

싱글톤 빈인 Single을 통해 프로토타입 빈 Proto를 가져와서 출력해보자.

 

실행 결과

위와 같이 싱글톤 빈 Single 안의 Proto는 프로토타입인데도 불구하고 주입되고나서 인스턴스가 새로 생성되지 않는다.

이렇게 싱글톤 빈에서 프로토타입 빈을 참조할때는 다음 방법으로 인스턴스가 업데이트되도록 할 수 있다.

 

4. 업데이트 문제 해결 방법

1) 방법 1 - proxyMode 설정

프로토타입 빈의 @Scope 애노테이션에 proxyMode 속성을 설정해준다.

 

Proto.java

import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
 
@Component @Scope(scopeName = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class Proto {
}

 

proxyMode 속성은 설정하지 않으면 기본값은 scopedProxyMode.DEFAULT이다.

DEFAULT는 proxy를 사용하지 않는다는 옵션이고 proxy를 사용하려면 클래스인 경우 TARGET_CLASS, 인터페이스일 경우 INTERFACES로 설정한다.

 

  • scopedProxyMode.DEFAULT : Proxy를 사용하지 않음
  • scopedProxyMode.TARGET_CLASS : Proxy를 사용함(클래스)
  • scopedProxyMode.INTERFACES : Proxy를 사용함(인터페이스)

실행 결과

 

Proxy를 쓴다는 것은 대상 빈을 proxy로 감싼다는 것이다.

 

그렇게 함으로써 해당 빈을 사용하는 빈들이 프록시로 감싼 빈을 사용하게 한다.

 

싱글톤 빈이 프로토타입 빈을 사용할때 proxy로 감싸야하는 이유는, 프로토타입 빈을 직접 참조하면 인스턴스를 새로 생성해줄 여지가 없기 때문이다.

즉 매번 인스턴스를 새로 생성해줄 수 있는 프록시로 감싸주는 것이다.

 

참고로 원래 JDK 안에 있는 dynamic proxy는 인터페이스의 프록시만 만들 수 있기 때문에 클래스의 프록시는 써드 파티 라이브러리를 사용한다.

예제 코드와 같이 ScopedProxyMode.TARGET_CLASS라고 설정하는 것은 타겟 클래스의 proxy를 만들라고 알려주는 것이다.

 

결과적으로 실제 인스턴스를 감싸는 프록시 인스턴스가 생성되고, 이 프록시 인스턴스가 빈으로 등록된다.

따라서 실질적으로 주입되는 빈은 프록시 빈이다.

프록시 빈도 원본 프로토타입 빈을 상속해서 만들기 때문에 타입은 동일하다.

타입이 같으므로 문제없이 주입될 수 있다.

 

2) 방법 2 - ObjectProvider 사용

이 방법은 빈 설정이 아닌 코드를 수정해서 해결하는 방법이다.

위에서 설정한 proxyMode를 원복하고 Single.java를 다음과 같이 수정한다.

 

Single.java

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
 
@Component
public class Single {
 
    @Autowired
    private ObjectProvider<Proto> proto;
 
    public Proto getProto() {
        return proto.getIfAvailable();
    }
}

 

프로토타입 빈을 주입받는 필드의 타입을 ObjectProvider로 감싸고 getter에서 getIfAvailable()로 리턴하도록 한다.

 

실행 결과

 

이 방법은 코드 수정이 필요하며 spring 코드가 들어가기 때문에 자바 객체가 POJO 스타일에서 벗어나게 된다는 특징이 있다.

 

어떤 방법을 사용하든, 이렇게 Scope이 넓은 빈(싱글톤 빈)에서 scope이 짧은 빈(프로토타입 빈)을 주입받을 때는 주의가 필요하다는 점을 반드시 알아두자.

 

References

인프런 - 백기선님의 스프링 프레임워크 핵심 기술

 

출처 : atoz-develop.tistory.com/entry/Spring-%EB%B9%88%EC%9D%98-Scope-%EC%8B%B1%EA%B8%80%ED%86%A4%EA%B3%BC-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85?category=869243