-> 블로그 이전

[Spring - 기본] 빈 스코프

2022. 5. 19. 19:03Language`/Spring

빈 스코프

말 그대로 "빈이 존재할 수 있는 범위"를 뜻한다

 

지금까지는 스프링 빈이 "스프링 컨테이너 시작 ~ 종료까지 유지"된다고 하였다

이것은 스프링 빈이 "싱글톤"일 경우에만 해당된다

 

Singleton

스프링 컨테이너에서는 기본적으로 스프링 빈 객체들을 "싱글톤"으로 관리한다

싱글톤 빈은 스프링 컨테이너 시작 ~ 종료까지 유지되는 가장 넓은 범위의 스코프이다

따라서 싱글톤 빈의 생명주기는 스프링 컨테이너의 생명주기와 동일하다고 볼 수 있다

 

Prototype

프로토타입 빈은 스프링 빈과 전혀 다른 타입이다

스프링 컨테이너는 "프로토타입 빈의 {생성, DI, 초기화}"까지만 관리해주고 그 이후부터는 전혀 관여하지 않는다

  • 따라서 스프링 컨테이너가 종료된다고 해도 "종료 메소드"가 호출되지 않는다

Prototype Scope

싱글톤 빈을 조회하면 스프링 컨테이너는 항상 동일한 인스턴스의 스프링 빈을 return해준다

  • 싱글톤 빈은 스프링 컨테이너에 단 하나만 존재하기 때문

프로토타입 빈을 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 return해준다

  싱글톤 빈 프로토타입 빈
초기화 메소드 스프링 컨테이너에 의해서 싱글톤 빈이 생성되고, DI가 완료된 후 초기화 메소드 호출 스프링 컨테이너가 프로토타입 빈을 조회함에 따라서 프로토타입 빈이 생성되고 DI되고 그 이후에 초기화 메소드 호출
소멸 메소드 스프링 컨테이너가 종료될때 호출 스프링 컨테이너가 관리를 안해주기 때문에 스프링 컨테이너가 종료되어도 호출 X

 

프로토타입 빈의 특징은 다음과 같다

1. 스프링 컨테이너에 요청할 때마다 새로 생성
2. 스프링 컨테이너는 {생성 / DI / 초기화}까지만 관여
3. 종료 메소드가 호출되지 않는다 :: 직접 수동으로 호출해야 한다
4. 프로토타입을 return 받은 Client가 관리해줘야 한다

 

싱글톤 빈 테스트

@Scope("singleton")
public class SingletonBean {
    public SingletonBean(){
        System.out.println("(싱글톤 빈) 객체 생성(생성자 호출) >> " + this);
    }

    @PostConstruct
    public void init(){
        System.out.println("(싱글톤 빈) 초기화 콜백 메소드 호출 >> " + this);
    }

    @PreDestroy
    public void destroy(){
        System.out.println("(싱글톤 빈) 소멸 콜백 메소드 호출 >> " + this);
    }
}
public class SingletonTest {
    @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);

        Assertions.assertThat(singletonBean1).isSameAs(singletonBean2);

        ac.close();
    }
}

스프링 컨테이너로부터 싱글톤 빈을 조회하면 항상 동일한 인스턴스를 return해주는 것을 확인할 수 있다

 

프로토타입 빈 테스트

@Scope("prototype")
public class PrototypeBean {
    public PrototypeBean(){
        System.out.println("(프로토타입 빈) 객체 생성(생성자 호출) >> " + this);
    }

    @PostConstruct
    public void init(){
        System.out.println("(프로토타입 빈) 초기화 콜백 메소드 호출 >> " + this);
    }

    @PreDestroy
    public void destroy(){
        System.out.println("(프로토타입 빈) 소멸 콜백 메소드 호출 >> " + this);
    }
}
public class PrototypeTest {
    @Test
    public void prototypeBeanFind(){
        AnnotationConfigApplicationContext ac =
                new AnnotationConfigApplicationContext(PrototypeBean.class);
        /*
        AnnotationConfigApplicationContext에 파라미터로 넣어버리면 해당 클래스는 "컴포넌트 스캔" 대상으로 처리가 됨
        >> 그냥 스프링 빈으로 등록 시켜줌
         */
        System.out.println("## Generate prototypeBean1 ##");
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        System.out.println("## Generate prototypeBean2 ##");
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);

        System.out.println("prototypeBean1 = " + prototypeBean1);
        System.out.println("prototypeBean2 = " + prototypeBean2);

        Assertions.assertThat(prototypeBean1).isNotSameAs(prototypeBean2);

        ac.close();
    }
}

스프링 컨테이너로부터 프로토타입 빈을 조회하면 "항상 새로운 프로토타입 빈 인스턴스"가 생성되는 것을 확인할 수 있다

항상 새로운 인스턴스를 return하기 때문에 각 프로토타입은 서로 다르게 출력되는 것을 확인할 수 있다

 

그리고 결론적으로 프로토타입 빈은 "종료 메소드"를 호출하지 않는 것도 확인할 수 있다

 

이렇게 싱글톤 빈과 프로토타입 빈은 생성 & 관리되는 관점이 다른데 만약 서로 같이쓴다면 어떤 문제가 발생할까?

싱글톤 + 프로토타입

(1) 각 Client는 싱글톤 빈을 요청

@Scope("singleton")
public class ClientSingletonBean {
    private int count;

    public ClientSingletonBean(){
        System.out.println("(싱글톤 빈) 객체 생성 >> " + this);
    }

    public void addCount(){
        count++;
    }

    public int getCount(){
        return count;
    }

    @PostConstruct
    public void init(){
        System.out.println("(싱글톤 빈) 초기화 콜백 메소드 호출 >> " + this);
    }

    @PreDestroy
    public void destroy(){
        System.out.println("(싱글톤 빈) 소멸 콜백 메소드 호출 >> " + this);
    }
}
public class SingletonWithPrototypeTest1 {
    @Test
    @DisplayName("클라이언트는 싱글톤 빈을 호출해서 사용")
    void ClientCallSingleton(){
        AnnotationConfigApplicationContext ac =
                new AnnotationConfigApplicationContext(ClientSingletonBean.class);

        ClientSingletonBean clientA = ac.getBean(ClientSingletonBean.class);
        clientA.addCount();
        System.out.println("clientA - count : " + clientA.getCount());
        Assertions.assertThat(clientA.getCount()).isEqualTo(1);

        ClientSingletonBean clientB = ac.getBean(ClientSingletonBean.class);
        clientB.addCount();
        System.out.println("clientB - count : " + clientB.getCount());
        Assertions.assertThat(clientB.getCount()).isEqualTo(2);

        ac.close();
    }
}

동일한 싱글톤 빈을 공유하기 때문에 앞에서 발생한 계산결과에 대해서 유지하는 것을 볼 수 있다

 

(2) 각 Client는 프로토타입 빈을 요청

@Scope("prototype")
public class ClientPrototypeBean {
    private int count;

    public ClientPrototypeBean(){
        System.out.println("(프로토타입 빈) 객체 생성 >> " + this);
    }

    public void addCount(){
        count++;
    }

    public int getCount(){
        return count;
    }

    @PostConstruct
    public void init(){
        System.out.println("(프로토타입 빈) 초기화 콜백 메소드 호출 >> " + this);
    }

    @PreDestroy
    public void destroy(){
        System.out.println("(프로토타입 빈) 소멸 콜백 메소드 호출 >> " + this);
    }
}
public class SingletonWithPrototypeTest2 {
    @Test
    @DisplayName("클라이언트는 프로토타입 빈을 호출해서 사용")
    void ClientCallPrototype(){
        AnnotationConfigApplicationContext ac =
                new AnnotationConfigApplicationContext(ClientPrototypeBean.class);

        ClientPrototypeBean clientA = ac.getBean(ClientPrototypeBean.class);
        clientA.addCount();
        System.out.println("clientA - count : " + clientA.getCount());
        Assertions.assertThat(clientA.getCount()).isEqualTo(1);

        ClientPrototypeBean clientB = ac.getBean(ClientPrototypeBean.class);
        clientB.addCount();
        System.out.println("clientB - count : " + clientB.getCount());
        Assertions.assertThat(clientB.getCount()).isEqualTo(1);

        ac.close();
    }
}

프로토타입 빈은 조회할때마다 새로 생성되기 때문에 생성되는 프로토타입 빈은 서로 다르고 각자의 필드를 각자만 유지하는 것을 볼 수 있다

 

(3) 프로토타입 빈을 포함하는 싱글톤 빈을 요청

@Scope("singleton")
@RequiredArgsConstructor
public class ClientSingletonWithPrototypeBean {
    private final ClientPrototypeBean clientPrototypeBean; // 생성자 DI

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

    @PostConstruct
    public void init(){
        System.out.println("(싱글톤 빈) 초기화 콜백 메소드 호출 >> " + this);
    }

    @PreDestroy
    public void destroy(){
        System.out.println("(싱글톤 빈) 소멸 콜백 메소드 호출 >> " + this);
    }
}
public class SingletonWithPrototypeTest3 {

    @Configuration
    static class TestConfig{
        @Bean
        public ClientPrototypeBean clientPrototypeBean(){
            return new ClientPrototypeBean();
        }
    }

    @Test
    @DisplayName("클라이언트는 프로토타입 빈을 DI 받은 싱글톤 빈을 호출해서 사용")
    void ClientCallSingtonWithPrototype(){
        AnnotationConfigApplicationContext ac =
                new AnnotationConfigApplicationContext(ClientSingletonWithPrototypeBean.class, TestConfig.class);

        ClientSingletonWithPrototypeBean clientA = ac.getBean(ClientSingletonWithPrototypeBean.class);
        int clientA_count = clientA.logic();
        System.out.println("clientA.getCount() : " + clientA_count);
        Assertions.assertThat(clientA_count).isEqualTo(1);

        ClientSingletonWithPrototypeBean clientB = ac.getBean(ClientSingletonWithPrototypeBean.class);
        int clientB_count = clientB.logic();
        System.out.println("clientB.getCount() : " + clientB_count);
        Assertions.assertThat(clientB_count).isEqualTo(1);

        ac.close();
    }
}

싱글톤 빈은 생성시점에만 DI를 받게 된다

프로토타입은 조회할때마다 새로 생성되기는 하지만, 위에서는 이미 과거에 DI된 프로토타입 빈이 싱글톤 빈과 함께 유지되는 문제점을 확인할 수 있다

 

우리가 원하는 것은 프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라, 사용할때마다 새로 생성해서 사용하는 것을 원하고 있다

 


Provider

물론 싱글톤 빈과 프로토타입 빈을 함께 사용할 때 매번 프로토타입을 새로 생성하고 싶다면 가장 간단하게 싱글톤 빈이 프로토타입 빈을 사용할 때마다 스프링 컨테이너에 새로 요청하면 된다

  • 의존관계를 주입받는게 아니라, 직접 필요한 의존관계를 찾는 동작을 "Dependency Lookup(DL)"이라고 한다

 

하지만 이런 가장 간단한 방법은 여러 문제점이 존재한다

1. 스프링 컨테이너에 종속적인 코드가 된다
2. 단위 테스트가 어려워진다

 

Spring에서는 "Provider"라는 기능을 통해서 DL 서비스를 굉장히 편리하게 제공해주고 있다

 

(1) ObjectFactory / ObjectProvider

ObjectFactory는 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공해준다

ObjectProvider는 ObjectFactory에 부가적 편의기능을 추가해준 인터페이스이다

"getObject()"를 통해서 <T>에 지정한 빈을 찾아서 return해준다

 

@Scope("singleton")
@RequiredArgsConstructor
public class ClientSingletonWithPrototypeBean {
    private final ObjectProvider<ClientPrototypeBean> objectProvider;

    public int logic(){
        ClientPrototypeBean clientPrototypeBean = objectProvider.getObject();
        clientPrototypeBean.addCount();
        return clientPrototypeBean.getCount();
    }

    @PostConstruct
    public void init(){
        System.out.println("(싱글톤 빈) 초기화 콜백 메소드 호출 >> " + this);
    }

    @PreDestroy
    public void destroy(){
        System.out.println("(싱글톤 빈) 소멸 콜백 메소드 호출 >> " + this);
    }
}
public class SingletonWithPrototypeTest3 {
    @Test
    @DisplayName("클라이언트는 프로토타입 빈을 DI 받은 싱글톤 빈을 호출해서 사용")
    void ClientCallSingtonWithPrototype(){
        AnnotationConfigApplicationContext ac =
                new AnnotationConfigApplicationContext(
                        ClientSingletonWithPrototypeBean.class, 
                        ClientPrototypeBean.class
                );

        ClientSingletonWithPrototypeBean clientA = ac.getBean(ClientSingletonWithPrototypeBean.class);
        int clientA_count = clientA.logic();
        System.out.println("clientA.getCount() : " + clientA_count);
        Assertions.assertThat(clientA_count).isEqualTo(1);

        ClientSingletonWithPrototypeBean clientB = ac.getBean(ClientSingletonWithPrototypeBean.class);
        int clientB_count = clientB.logic();
        System.out.println("clientB.getCount() : " + clientB_count);
        Assertions.assertThat(clientB_count).isEqualTo(1);

        ac.close();
    }
}

Provider를 통해서 프로토타입 빈을 매번 새로 생성되는 것을 위의 결과를 통해서 확인할 수 있다

 

 

(2) JSR-330 Provider

javax.inject로써 자바 표준임을 확인할 수 있다

 

그리고 JSR-330 Provider를 사용하기 위해서는 build.gradle에 "javax.inject:javax.inject:1"을 추가해줘야 한다

 

그리고 굉장히 간단하게 내부에서 get()하나만 활용하면 된다

 

1. 자바 표준 + 기능이 단순하기 때문에 단위 테스트를 만들거나 mock 코드를 만들기 훨씬 쉬워진다
2. get() 하나만 활용하면 되기 때문에 매우 단순하다
3. 별도의 라이브러리가 필요하다
4. 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용이 가능하다
@Scope("singleton")
@RequiredArgsConstructor
public class ClientSingletonWithPrototypeBean {
    private final Provider<ClientPrototypeBean> objectProvider;

    public int logic(){
        ClientPrototypeBean clientPrototypeBean = objectProvider.get();
        clientPrototypeBean.addCount();
        return clientPrototypeBean.getCount();
    }

    @PostConstruct
    public void init(){
        System.out.println("(싱글톤 빈) 초기화 콜백 메소드 호출 >> " + this);
    }

    @PreDestroy
    public void destroy(){
        System.out.println("(싱글톤 빈) 소멸 콜백 메소드 호출 >> " + this);
    }
}
public class SingletonWithPrototypeTest3 {
    @Test
    @DisplayName("클라이언트는 프로토타입 빈을 DI 받은 싱글톤 빈을 호출해서 사용")
    void ClientCallSingtonWithPrototype(){
        AnnotationConfigApplicationContext ac =
                new AnnotationConfigApplicationContext(
                        ClientSingletonWithPrototypeBean.class,
                        ClientPrototypeBean.class
                );

        ClientSingletonWithPrototypeBean clientA = ac.getBean(ClientSingletonWithPrototypeBean.class);
        int clientA_count = clientA.logic();
        System.out.println("clientA.getCount() : " + clientA_count);
        Assertions.assertThat(clientA_count).isEqualTo(1);

        ClientSingletonWithPrototypeBean clientB = ac.getBean(ClientSingletonWithPrototypeBean.class);
        int clientB_count = clientB.logic();
        System.out.println("clientB.getCount() : " + clientB_count);
        Assertions.assertThat(clientB_count).isEqualTo(1);

        ac.close();
    }
}