본문 바로가기

Spring

Spring- 빈 스코프(싱글톤, 프로토타입)

 싱글톤 스코프

 디폴트 타입의 스코프로서 빈이 등록시 하나의 객체만 존재하며 클라이언트들은 하나의 빈을 공유하며 사용한다.

스프링 컨테이너 시작과 종료까지 유지되는 가장 넓은 범위의 스코프

 프로토타입 스코프

 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.

 클라이언트가 스프링 컨테이너에 프로토타입 빈을 요청하면 요청시마다 새로운 프로토타입 빈을 생성하여 반환한다.

즉, 스프링은 의존관계 주입까지만 프로토타입 빈을 관리하고 그 후부터는 클라이언트가 빈의 관리 책임을 가진다.

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.Assertions.assertThat;

/**
 * 싱글톤은 빈을 등록함과 동시에 객체가 생성됨. 그 후 DI 및 초기화가 진행된다.
 * 하나의 객체를 여러 클라이언트가 공유해 사용하고, 관리 책임은 스프링이 가진다.
 * 프로토타입 스코프 빈은 클라이언트가 요청시 마다 스프링 컨테이너가 새로운 프로토타입 빈을 생성하고 DI 및 초기화가 진행됨
 * 그 후 생성된 빈은 클라이언트에게 반환되고 반환된 빈은 스프링이 아닌 클라이언트가 관리함
 * 즉 프로토타입 스코프 빈의 관리 책임은 클라이언트가 가진다.
 */

public class PrototypeTest {

    @Test
    void prototypeTest(){
        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();
        // 프로토타입 스코프는 객체 생성과 의존관계 주입, 초기화만 스프링에서 관리하고
        // 그 후에는 스프링 대신 클라이언트가 관리함.
        // 프로토타입 스코프는 스프링에서 관리하는 것이 아니라서 destroy 메소드 호출 안 됨

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

    @Scope("prototype")  // prototype 스코프 등록
    static class PrototypeBean{
        @PostConstruct
        public void init(){
            System.out.println("SingletonBean.init");
        }

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


prototypeBean1 = hello.core.scope.PrototypeTest$PrototypeBean@63f259c3
prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@26ceffa8

 프로토타입 빈을 호출시마다 새로운 객체가 반환되기 때문에 prototypeBean1과 prototypeBean2는 서로 다른 객체이다.

또한 생성되어 반환된 객체는 스프링에서 관리하지 않기 때문에 ac.close(); 해도  각 객체들의 destroy()메소드가 실행되지 않는다.

굳이 각 객체들의 destroy() 메소드를 호출하고 싶다면 테스트 코드에서 destroy() 메소드를 직접 호출하면 된다.

public class PrototypeTest {

    @Test
    void prototypeTest(){
		
        .....
        
        prototypeBean1.destroy();
        prototypeBean2.destroy();
    }
    
    @Scope("prototype") 
    static class PrototypeBean{
        ....
    }
}

//각 프로토타입 빈들의 destroy() 메소드 호출됨
SingletonBean.destroy hello.core.scope.PrototypeTest$PrototypeBean@63f259c3
SingletonBean.destroy hello.core.scope.PrototypeTest$PrototypeBean@26ceffa8

 

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

  이번에는 "clientBean" 이라는 싱글톤 빈이 의존관계 주입을 통해서 프로토타입 빈을 주입받아서 사용하는 예를 보자.

package hello.core.scope;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.assertj.core.api.Assert;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.*;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import static org.assertj.core.api.Assertions.assertThat;

/**  <예시>
 * 상황: 싱글톤 빈에 프로토타입빈을 의존관계로 주입 후, 클라이언트들이 싱글톤 빈을 호출.
 *
 * 싱글톤 빈: clientBean
 * 프로토타입 빈: prototypeBean@x01 (참조값은 내가 임의로 지은 거임. 참조값 모름)
 *
 * 1. clientBean이 prototypeBean을 의존관계로 주입받는 경우
 *   스프링 컨테이너는 prototypeBean 객체를 생성해서 clientBean에 반환, 주입한다.
 * 2. 클라이언트A가 싱글톤 빈을 컨테이너에 요청해 받는다. 
 *   이때 클라이언트는 prototypeBean@x01이 주입된 clientBean을 받는다.
 * 3. prototypeBean 빈의 스코프는 프로토타입이지만 이는 다른 클라이언트 B,C가 clientBean을 요청시마다 
 *   새로이 생성되는 것이 아닌 싱글톤 빈에 이미 의존관계 주입이 끝났기 때문에 클라이언트 B,C는 
 *   prototypeBean@x01이 주입된 clientBean을 반환받는다.
 * 4. 즉 addCount()를 진행할 경우, 주입된 prototypeBean@x01이 비록 프로토타입 빈이라고 해도 
 *   동일한 객체가 호출되어 count의 값이 계속 증가된 체로 유지된다.
 */
public class SingletonWithPrototypeTest1 {

    @Test
    void prototypeFind(){
        AnnotationConfigApplicationContext ac =
                new AnnotationConfigApplicationContext(ClientBean.class,PrototypeBean.class);

        ClientBean clientBean = ac.getBean(ClientBean.class);
        PrototypeBean prototypeBean1 = clientBean.getPrototypeBean();
        System.out.println("prototypeBean1.count: " + prototypeBean1.getCount());
        prototypeBean1.addCount();
        System.out.println("prototypeBean1.count: " + prototypeBean1.getCount());


        PrototypeBean prototypeBean2 = clientBean.getPrototypeBean();
        System.out.println("prototypeBean2.count: " + prototypeBean2.getCount());

        // prototypeBean1 == prototypeBean2
        assertThat(prototypeBean1).isSameAs(prototypeBean2);
    }


    @Component
    @RequiredArgsConstructor
    static class ClientBean{
        private final PrototypeBean prototypeBean;

        public PrototypeBean getPrototypeBean() {
            return prototypeBean;
        }
    }

    @Scope("prototype")
    @Component
    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");
        }
    }
}

prototypeBean1.count: 0
prototypeBean1.count: 1
prototypeBean2.count: 1  // 동일한 프로토타입 빈에서 호출되어 값이 0이 아닌 1

 싱글톤 내부에 주입된 프로토타입 빈은 싱글톤 빈을 호출할 때마다 새로운 객체가 생성되어 주입되는 것이 아니라 이미 의존관계 주입이 끝난 시점에 주입된 객체 하나만 사용되므로 더 이상 프로토타입 빈 객체가 새롭게 생성되지 않는다.

 그러나 일반적으로 프로토타입 빈을 사용하는 이유는 호출 시 마다 새로운 객체를 생성하여 사용하기 위함이므로

이는 프로토타입 빈의 사용목적에 반하게된다. 그래서 싱글톤 내부에 프로토타입 빈을 주입한 경우에도 싱글톤 빈을 호출 시 마다 새로운 프로토타입 빈 객체를 사용하기 위해서는 다음과 같은 방법이 사용된다.

 ObjectProvider

  싱글톤 빈에서 프로토타입을 사용할 때, 싱글톤 빈이 호출될 때 마다 새로운 프로토타입 빈이 호출되게 하기 위해선 DL을 사용하면 된다. 그리고 이러한 DL 서비스를 제공하는 것이 "ObjectProvider"이다.

static class ClientBean {

    @Autowired private ObjectProvider<PrototypeBean> prototypeBeanProvider;
    //ObjectProvider는 의존관계 조회를 해줌

    public int logic() {
        
        PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
        //메소드 호출 시 마다 PrototypeBean 객체가 조회되고 반환되는 과정에서 계속해 새로운 객체 생성.
        prototypeBean.addCount();
        int count = prototypeBean.getCount();
        return count;
    }
}

  clientBean(싱글톤)을 보면 ObjectProvider 객체에 PrototypeBean이 들어가는 것을 볼 수 있다. 그리고 logic( ) 메소드는 getObject() 메소드를 통해서 prototypeBean을 조회하여 반환한다. 결국 logic() 메소드에서 prototypeBean은 메소드 호출 시마다 계속 새롭게 생성이 되게 된다.  

  - DL(의존 관계 탐색)

 참고로 의존관계를 외부에서 주입받는 것이 아니라 스프링 컨테이너에서 필요한 의존관계를 찾는 것 DL(Defendency Lookup) 의존 관계 탐색이라고 한다.

 

 Provider

 ObjectProvider가 스프링에서 지원하는 기능이라면 Provider는 자바 표준으로 지원하는 기능이다. ObjectProvider가 다양한 기능을 지원하는 반면 Provider는 get() 메소드로 DL 하나만을 지원한다는 차이가 있다. 따라서 스프링과 많은 기능을 사용한다면 ObjectProvider를 스프링이 아닌 다른 컨테이너를 사용한다면 Provider를 사용하면 된다.

static class ClientBean {

    @Autowired
    private Provider<PrototypeBean> prototypeBeanProvider;
    // ObjectProvider의 자바 표준.ver = Provider
    // 대신 기능이 좀 후달림

    public int logic() {

        PrototypeBean prototypeBean = prototypeBeanProvider.get(); 
        prototypeBean.addCount();
        int count = prototypeBean.getCount();
        return count;
    }
}