티스토리 뷰

빈 스코프란?

빈 스코프의 빈(Bean)이란 스프링 컨테이너에서 관리하는 자바 객체를 의미합니다.

그리고 스코프는 이름 그대로 범위라는 뜻으로 빈 스코프는 빈이 존재할 수 있는 범위를 말합니다.

 

스프링을 공부해 보신 분들은 알겠지만, 기본적으로 스프링 컨테이너에서 스프링 빈이 싱글톤(스코프)으로 생성되고 관리되고 스프링 컨테이너와 생명주기를 같이 했기 때문에 신경 쓸 필요가 없었습니다.

 

하지만, 빈 스코프를 어떻게 설정하느냐에 따라 스프링 빈의 생성과 소멸을 클라이언트에서 관리해줘야 하는 경우도 생길 수 있고, 다양한 요구사항에 맞는 스코프를 지정해서 사용할 수 있습니다.

 

스프링(Spring)은 다음과 같은 다양한 스코프를 지원합니다.

  1. 싱글톤 : 디폴트(기본) 스코프입니다. 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프입니다.
  2. 프로토타입 : 스프링 컨테이너는 프로토타입 빈의 생성과 의존 관계 주입까지만 관여하고, 더는 관리하지 않는 매우 짧은 범위의 스코프입니다. 따라서 종료 메서드는 호출되지 않습니다.
  3. 웹 관련 스코프
    • request : 웹 요청이 들어오고 나갈 때까지 유지되는 스코프입니다.
    • session : 웹 세션이 생성되고 종료될 때까지 유지되는 스코프입니다.
    • application : 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프입니다.
    • websocket : 웹 소켓과 동일한 생명주기를 가지는 스코프입니다.

 

빈 스코프는 아래와 같이 @Scope 애노테이션을 통해 지정할 수 있습니다.

//자동 빈 등록시
@Scope("prototype")
@Component
public class NetworkClient {...}

//수동 빈 등록시
@Configuration
class LifeCycleConfig {

    @Scope("prototype")
    @Bean
    public NetworkClient networkClient() {
    	NetworkClient networkClient = new NetworkClient();
        return networkClient;
    }
}

 


싱글톤 스코프(Singleton Scope)

스프링 컨테이너는 기본적으로 싱글톤 스코프를 사용합니다.

싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스를 반환합니다.

싱글톤 스코프를 설정하는 방법은 scope(범위)를 명시적으로 지정하지 않으면 됩니다.

//명시적으로 지정하거나 지정하지 않으면 싱글톤 스코프로 관리
@Component
public class SingletonBean {...}

//명시적으로 지정하거나 지정하지 않으면 싱글톤 스코프로 관리
@Component
@Scope("singleton")
public class SingletonBean {...}

 

 

싱글톤과 빈에 대해 아직 지식이 부족하시다면 아래 링크를 통해 공부해 보시길 바랍니다.

 

[Spring] 싱글톤 패턴이란? 그리고 스프링에서의 싱글톤

싱글톤 패턴(Singleton Pattern)싱글톤 패턴(Singleton Pattern)은 객체지향 프로그래밍에서 자주 사용되는 디자인패턴입니다. 소프트웨어 디자인 패턴 중 하나인 싱글톤 패턴의 핵심은 클래스의 인스턴

developshrimp.com

 

 

 

프로토타입 스코프(Prototype Scope)

프로토타입 스코프의 빈을 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환합니다.

 

프로토타입의 빈을 스프링 컨테이너에 요청하면 스프링 컨테이너는 프로토타입의 빈을 생성하고, 필요한 의존관계를 주입합니다. 싱글톤 빈은 컨테이너 생성 시점에 같이 생성되고 초기화되지만, 프로토타입 빈은 스프링 컨테이너에서 빈을 조회할 때 생성되고 초기화 메서드도 실행됩니다.

 

스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리합니다.

클라이언트에게 빈을 반환한 이후에는 생성된 프로토타입 빈을 관리하지 않습니다.

 

프로토타입 빈을 관리할 책임을 컨테이너가 가지는게 아닌 클라이언트에게 주는 것이죠

따라서 @PreDestory와 같은 종료 콜백 메서드가 호출되지 않습니다.

 

[Spring] 빈 생명주기 콜백 - 꼬리에 꼬리를 물고 완벽 이해하기

빈 생명주기 콜백프로그램을 작성하면서 우리는 데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모

developshrimp.com

 

그렇기 때문에 클라이언트가 소멸 메서드(@PreDestory) 같은 것을 자체적으로 관리해주어야 합니다.

 

 

그렇다면 프로토타입 스코프는 언제 사용이 될까요?

 

1. 상태를 가진(Stateful) 빈 사용 시

쉽게 이해하기 위해 온라인 쇼핑 애플리케이션을 예로 들어보겠습니다.

 

각 사용자는 쇼핑 카트를 관리하는(장바구니) 빈이 필요한 경우, 해당 빈은 사용자마다 다른 상태(구매 항목 목록)를 유지해야 합니다. 여기에서 싱글톤 스코프를 사용한다면 모든 사용자가 같은 쇼핑 카트 인스턴스를 공유하게 되어 문제가 발생할 수 있겠죠

 

이럴 때 프로토타입(Prototype) 스코프를 사용한다면 각 요청마다 새로운 쇼핑 카트 인스턴스가 생성되어 각 사용자의 상태를 독립적으로 유지할 수 있습니다.

 

 

2. 멀티스레드 환경에서 스레드 안정성(Thread Safety) 보장 필요시

쉬운 이해를 위해 공용 주방을 사용하는 기숙사로 예를 들어보겠습니다.

 

기숙사에 여러 사람이 살고 있고, 모두 하나의 공용 주방을 사용해야 한다고 생각해 봅시다.

주방에는 하나의 조리 도구 세트만 있어서, 여러 사람이 동시에 요리를 하려고 할 때 기다려야 합니다.

만약 각 사람이 요리할 때마다 자신만의 조리 도구 세트를 받는다면 충돌 없이 더 효율적으로 요리할 수 있을 겁니다.

 

애플리케이션에서 각 스레드(기숙사 사용자)가 독립적인 작업을 수행하면서 빈의 인스턴스(조리 도구 세트)를 사용해야 하는 경우(예: 배치 작업 처리), 싱글톤 빈을 사용하면 여러 스레드가 동일 인스턴스를 공유하게 되어 스레드 안정성 문제가 발생할 수 있습니다. 프로토타입(Prototype) 스코프를 사용하면 각 스레드마다 새로운 빈 인스턴스가 할당되어, 이러한 문제를 해결할 수 있습니다

 

3. 리소스 집약적 인스턴스의 사용 제어

쉬운 이해를 위해 도서관의 참고 서적을 예를 들어보겠습니다.

 

도서관에서 매우 유용하지만 제한된 수량만 있는 참고 서적을 생각해 봅시다.

이러한 서적은 많은 사람들이 사용하고 싶어 하지만, 동시에 많은 사람들이 사용할 경우에 서적이 손상될 수 있고, 필요할 때 사용할 수 없게 될 위험이 있습니다. 따라서 도서관에서는 이 서적을 필요할 때만 특정 인원에게 제한적으로 대여하여, 서적의 상태를 잘 유지하고 공정하게 접근할 수 있도록 관리하여 줍니다.

 

스프링으로 비유하자면 스프링에서 프로토타입 스코프를 사용하는 것은 리소스가 많이 필요한 빈(참고 서적)을 필요할 때만 생성하는 것과 유사합니다. 이 방법을 통해 리소스를 과도하게 사용하는 것을 방지하고, 빈의 생명주기를 효과적으로 관리하여 리소스를 최적화할 수 있습니다. 각 사용자(또는 작업)가 빈을 요청할 때마다 새로운 인스턴스가 생성되어 사용 후에는 자원이 해제되므로 리소스의 낭비를 줄이고 성능을 개선할 수 있습니다.

 

 

프로토타입 스코프의 문제점 - 싱글톤 빈과 함께 사용

프로토타입 스코프는 싱글톤 스코프와 함께 사용할 때 주의해야 할 점이 있습니다.

싱글톤 스코프의 빈이 프로토타입 빈을 주입받으면 싱글톤의 프로토타입 빈은 매번 바뀌지 않고 같은 빈이 사용됩니다.

 

싱글톤(Singleton) 빈은 ApplicationContext(스프링 컨테이너)가 처음 애플리케이션을 구동할 때 빈을 만들어주고 빈을 주입해서 앱이 종료될 때까지 계속 사용되기 때문에 싱글톤(Singleton) 빈 안에 있는 프로토타입(Prototype) 빈도 처음 주입된 채로 그대로 사용되기 때문입니다.

 

간단하게 정리하자면, 싱글톤 빈 안에 프로토타입 빈을 사용하게 된다면 그냥 일반 객체(빈)를 사용하는 것과 다름없습니다.

그렇다면 프로토타입 스코프를 이용하려 한 개발자의 의도한 대로 동작하지 않는 문제점이 생길 것 입니다.

 

이와 같은 문제를 어떻게 하면 해결 할 수 있을까요?

 

해결방법 1. 프록시 모드(proxy mode) 이용

@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ProtoType { ... }

 

프로토타입으로 사용할 빈에 @Scope 애노테이션에 proxyMode 속성 값으로 ScopedProxyMode.TARGET_CLASS를 넣어주면 됩니다.

만약에 해당 빈이 클래스가 아닌 인터페이스라면 ScopedProxyMode.INTERFACES로 사용할 수 있습니다.

 

이 방법은 스프링 컨테이너가 빈을 처음에 생성할 때 프로토타입 빈을 주입받는게 아니라 프로토타입 클래스를 상속받은 프록시(proxy) 클래스를 만들어서 빈으로 등록하고 프록시(proxy) 클래스에서 내부적으로 매번 새로운 프로토타입 빈을 사용하게 됩니다. 

 

아래와 같이 프로토타입 빈의 클래스 정보를 확인해보면 CGLIB(프록시)가 출력되는 것을 볼 수 있습니다.

@ComponentScan
public class PrototypeTest {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Prototype.class);
        Prototype prototype = ac.getBean(Prototype.class);

        System.out.println("prototype : " + prototype.getClass());
    }
}

 

 

 

해결방법 2. ObjectFactory, ObjectProvider 사용

가장 간단한 방법으로 싱글톤 빈이 프로토타입을 사용할 때마다 스프링 컨테이너에 새로 요청하는 것입니다.

@Component
public class Singleton {

    @Autowired
    ObjectProvider<ProtoType> protoTypeProvider;
    
    public ProtoType getProtoType() {
        PrototypeBean prototypeBean = protoTypeProvider.getObject();
        ...
    }

 

ObjectFactory : 지정한 빈을 컨테이너에서 대신 찾아주는 DL(Dependency Lookup) 서비스를 제공해줍니다. 아주 단순하게 getObject 메서드 하나만 제공하는 인터페이스이고, 별도의 라이브러리도 필요 없습니다. 스프링에 의존합니다.

 

ObjectProvider : ObjectFactory에 편의기능들(Optional, Stream...)을 추가해서 만들어진 인터페이스입니다.

별도의 라이브러리는 필요 없고 스프링에 의존합니다.

 

위의 코드처럼 getObect 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환합니다.(DL)

 

이 방법의 장점으로는 스프링이 제공하는 기능을 사용하긴 하지만(스프링 의존) 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기 쉬워집니다.

 

 

해결방법 3. JSR-330 Provider

이 방법은 jakarta.inject.Provider 라는 JSR-330 자바 표준을 사용하는 방법입니다.

이 방법을 사용하기 위해서는 jakarta.inject:jakarta.inject:1 라이브러리를 별도로 추가해 주어야 합니다.

라이브러리 추가후 아래 코드와 같이 사용할 수 있습니다.

@Component
public class Singleton {

    @Autowired
    Provider<ProtoType> protoTypeProvider;
    
    public ProtoType getProtoType() {
        PrototypeBean prototypeBean = protoTypeProvider.get();
        ...
    }

 

이 방법의 특징으로는 get 메서드를 통해 스프링 컨테이너 내부에 해당 빈을 찾아서 반환합니다.

자바 표준이기 때문에 스프링이 아닌 다른 컨테이너에서도 사용할 수 있므며, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기 쉬워집니다.

 

 

웹 스코프(Web Scope)

  • request : 웹 요청이 들어오고 나갈 때까지 유지되는 스코프입니다.
  • session : 웹 세션이 생성되고 종료될 때까지 유지되는 스코프입니다.
  • application : 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프입니다.
  • websocket : 웹 소켓과 동일한 생명주기를 가지는 스코프입니다.

웹 스코프는 웹 환경에서만 동작합니다.

웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료 시점까지 관리하므로 종료 메서드가 호출됩니다.

 

 

스프링 빈 등록 시 웹 스코프를 그대로 주입받으면 오류가 발생합니다.

싱글톤 빈은 스프링 컨테이너 생성 시 함께 생성되어서 라이프 사이클을 같이하지만, 웹 스코프(여기서는 request 스코프)의 경우 HTTP 요청이 올 때 새로 생성되고 응답하면 사라지기 때문에, 싱글톤 빈이 생성되는 시점에는 아직 생성되지 않습니다. 따라서 의존관계 주입이 불가능 한것이죠

 

좀더 쉽게 풀어 쓰자면, 애플리케이션이 실행되고 스프링 빈들이 컴포넌트 스캔이 되며 등록 및 의존관계 주입이 되는데, 여기서 웹 스코프(request 스코프)는 웹 요청이 들어와야(클라이언트의 요청이 와야 생성) 스코프가 생성되기 때문에 스프링 실행시점에 생성할 수 없습니다. 그렇기에 에러가 발생하게 됩니다.

 

<한줄 정리>

앱 실행 후 요청 와야 웹 스코프 생성하는데, 앱 실행 할 때 생성이 안되어 있으므로 스프링에서 에러 발생

 

 

이럴 때, Provider 혹은 프록시를 사용하면 문제를 해결 할 수 있습니다.

 

 

첫번째로 Provider를 사용하면 getObject() 혹은 get() 메서드 호출하는 시점까지 웹 스코프(request scope) 빈의 생성을 지연할 수 있습니다.

 

두번째로는 프록시를 사용하여 해결 할 수 있는데, @Scope 애노테이션의 proxyMode를 설정하면 스프링 컨테이너는 CGLIB 이라는 바이트코드 조작 라이브러리를 사용해 웹 스코프 빈을 상속받은 가짜 프록시 객체를 생성합니다.

스프링 컨테이너에는 이 프록시 객체가 등록이 되고 의존관계에서도 프록시 객체가 주입됩니다.

 

Provider 와 프록시의 사용방법은 위에 "프로토타입 빈의 문제점' 의 해결방법에 적혀있는 것과 동일합니다.

 


 

출처(참고)

인프런 김영한님의 스프링 핵심 원리 - 기본편 강의

https://mslim8803.tistory.com/70

https://catsbi.oopy.io/b2de2693-fd8c-46e3-908a-188b3dd961f3

https://0soo.tistory.com/225#%ED%--%--%EB%A-%-C%ED%--%A-%ED%--%--%EC%-E%--%---Prototype-%--%EC%-A%A-%EC%BD%--%ED%--%--

+ 이해 쉬운 예제를 제공하는 챗GPT 4.0