본문 바로가기

Spring

스프링 핵심 원리 - 기본편 : 빈 스코프(1/2)

스프링 핵심 원리 - 기본편 : 빈 스코프(1/2)

- 빈 스코프란?

빈의 생명주기가 '빈이 생존해 있을 때 생성과 소멸 사이에서 무엇을 할 것인가' 에 대한 것이라면 빈 스코프는 '빈이 언제 생성되고 언제 소멸될 것인가'에 대한 것이다. 즉 빈이 존재할 수 있는 범위를 결정한다.

 

지금까지는 디폴트로 설정되어 있는 싱글톤 스코프만 다뤘다. 싱글톤 스코프는 컨테이너가 생성되며 같이 등록되고, 컨테이너가 종료될 때까지 유지되는 가장 넓은 범위로 존재하는 스코프이다. 그 이외에 프로토타입, 웹 관련 스코프에 대해 알아보려고 한다, 먼저 프로토타입 스코프이다.

 

 

- 프로토타입 스코프

빈으로 생성할 클래스 위에 @Scope를 추가하므로써 빈의 스코프를 지정해 줄 수 있다. 아래는 클래스를 프로토타입 스코프로 지정하는 코드이다.

@Scope("prototype")
static class PrototypeBean{
    @PostConstruct
    public void init() {
        System.out.println("PrototypeBean.init");
    }

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

 

프로토타입 스코프 빈은 싱글톤 빈과 달리 컨테이너에 존재하지 않는다. 그러다 클라이언트의 조회가 있으면 새로운 빈을 생성한다. 그 후 필요한 의존관계를 주입받고, 빈을 조회한 클라이언트에게 반환된다. 이렇게 반환된 프로토타입 스코프 빈들은 스프링 컨테이너에 의해 관리되지 않는다.

 

다른 클라이언트가 다시 프로토타입 스코프 빈을 조회하면, 컨테이너는 새로운 빈을 생성하여 이전과 같은 과정을 거쳐 클라이언트에게 반환한다. 이전 클라이언트에게 반환한 빈과 현재 클라이언트에게 반환한 빈은 서로 별개의 빈이다. 때문에 아래와 같은 테스트 코드가 성립될 수 있다.

@Test
void prototypeBeanFind() {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
    PrototypeBean singletonBean1 = ac.getBean(PrototypeBean.class);
    PrototypeBean singletonBean2 = ac.getBean(PrototypeBean.class);
    System.out.println("singletonBean1 = " + singletonBean1);
    System.out.println("singletonBean2 = " + singletonBean2);
    assertThat(singletonBean1).isNotSameAs(singletonBean2);
    ac.close();
}

singletonBean1과 singletonBean2이 다른 인스턴스를 참조하고 있기 때문에 != 비교를 하는 isNotSameAs 가 성립할 수 있는 것이다.

 

정리하면 프로토타입 스코프 빈은 조회할 때마다 새로 생성된다. 이렇게 생성된 빈은 컨테이너가 생성과 의존관계 주입까지만 관여하고, 이 후에 클라이언트에게 반환해준 후에는 관여하지 않는다. 때문에 컨테이너가 종료되어도 @PreDetroy와 같은 소멸메서드는 실행되지 않는다. 빈의 소멸은 클라이언트에 맡겨지는 것이다.

 

 

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

프로토타입 스코프의 빈이 어떻게 동작하는지 알게 되었다. 하지만 이 프로토타입 빈이 싱글톤 빈과 의존관계를 갖게되면 의도하지 않은 방향으로 동작할 수 있기 때문에 주의해야 한다.

 

프로토타입 스코프의 빈을 의존관계 주입받는 싱글톤 스코프의 빈을 상상해 보자.

 

프로토타입 스코프의 빈은 클라이언트에게 '조회' 될 때마다 새로 생성된다. 싱글톤 스코프의 빈은 컨테이너가 생성되면서 생성되고 컨테이너가 종료될 때 까지 단 하나로 존재한다. 싱글톤 빈이 생성될 때, 프로토타입에 의존하고 있으므로 의존관계 주입을 통해 프로토타입 스코프 빈을 반환받아 사용한다.

 

그렇다면, 클라이언트A, B가 각각 컨테이너에서 싱글톤 빈을 조회하면, 이 싱글톤 빈 안에 프로토타입 빈 인스턴스들은 같은놈들일까 다른 놈들일까?

 

너무 당연하지만 두 프로토타입 빈은 같은 객체이다. 위 과정에서 프로토타입 빈은 싱글톤 빈에 의해 단 한번 조회됐다.

스프링 컨테이너는 조회에 대해 프로토타입 빈을 생성하고 싱글톤 빈에게 반환해줬다. 그 다음에 컨테이너는 이 프로토타입빈을 관리하지 않는다. 이 프로토타입 빈에 대한 책임은 온전히 싱글톤 빈에게로 넘어갔다. 때문에 클라이언트가 싱글톤 빈을 수만번 조회해도 싱글톤 빈에게 주입된 프로토타입빈은 모드 같은 인스턴스이다.

 

이에대한 테스트 코드를 다음과 같이 작성하였다.

    @Test
    void singletonClientUsePrototype() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class, ClientBean.class);
        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        assertThat(count1).isEqualTo(1);

        int count2 = clientBean2.logic();
        assertThat(count2).isEqualTo(2);
    }

    @Scope("singleton")
    @AllArgsConstructor
    static class ClientBean {

        private PrototypeBean prototypeBean;

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

프로토타입 스코프로 설정된 클래스에는 필드에 int형 변수 count 가 non-static으로 선언되어있다. 싱글톤으로 설정된 ClientBean 클래스에서는 의존관곚 주입을 통해 prototypeBean을 주입받는다. 테스트 메소드에서 ClientBean 타입으로 빈을 두번 조회하고 각각 다른 변수에 빈을 저장한다. 그리고 logic() 메소드를 두번 실행하는데 이는 프로토타입 빈의 count 변수를 1 증가 시키고 count를 반환하는 메소드이다.

 

위에서 설명한 것처럼 싱글톤 빈에 주입된 프로토타입 빈은 같은 객체이기 때문에 두번의 logic() 호출에 의해 count는 2가된다.

 

이렇게 되면 프로토타입 빈을 싱글톤 스코프로 설정한 것과 다름없는 것이된다. 이는 프로토타입이 만들어진 목적과는 다를 것이다. 프로토타입 스코프는 사용할 때마다 새로운 빈을 생성해서 사용하는 것을 위해 만들어진 것이다.

 

이와같이 싱글톤과 프로토타입 스코프 빈을 목적에 맞게 함께 사용하기 위해 Provider를 사용한다.

 

 

- 프로토타입 스코프 : 싱글톤 빈과 함께 사용시 Provider로 문제 해결

Provider는 지정한 빈을 컨테이너에서 대신 찾아주는 역할을 한다. 스프링 프레임워크에서 정의된 ObjectProvider, 자바 표준으로 구현된 Provider 두 종류가 있다. 둘다 사용법은 비슷하지만 스프링의 ObjectPRovider가 더 많은 기능들을 제공한다. 하지만 지금은 프로토타입 스코프 빈을 조회하는 예시만 들 것이므로 자바표준으로 정의된 Provider를 사용할 것이다. 사용 방법은 다음과 같다.

@Scope("singleton")
@AllArgsConstructor
static class ClientBean {

    private Provider<PrototypeBean> prototypeBeanProvider;

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

프로토타입 스코프로 정의된 PrototypeBean을 Provider로 받아 생성자 주입을 통해 빈을 주입 받는다. 이렇게 주입 된 빈에서 get() 메소드(ObjectProvider의 경우 getObject()) 를 사용하면 호출될 때마다 새로운 프로토타입 스코프 빈을 생성하여 반환한다. 때문에 다음의 테스트 코드가 성립한다.

@Test
void singletonClientUsePrototype() {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class, ClientBean.class);
    ClientBean clientBean1 = ac.getBean(ClientBean.class);
    ClientBean clientBean2 = ac.getBean(ClientBean.class);
    int count1 = clientBean1.logic();
    assertThat(count1).isEqualTo(1);

    int count2 = clientBean2.logic();
    assertThat(count2).isEqualTo(1);
}

logic()메소드를 호출할 때마다 내부에서 새로운 프로토타입 빈을 생성하고 count를 증가시킨다. 때문에 clientBean1 과 clientBean2에서 logic() 메소드의 지역변수로 존재하는 PrototypeBean은 서로 다른 인스턴스이다. 때문에 logic()은 각각 한번씩 호출되었으므로 count또한 각각 1이 성립한다.

 

이러한 프로토타입 스코프의 빈은 의존관계 주입이 완료된 새로운 객체가 필요할 때 사용한다. 하지만 실무에서는 대체적으로 싱글톤 빈으로 대부분의 문제를 해결하기 때문에 직접 사용할 일은 적다고 한다.


[참고 강의 출처]

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보세요! 📢

www.inflearn.com