빈(Bean) 스코프

ILLION·2023년 3월 29일
0

Spring Framework

목록 보기
5/5

스프링 빈 스코프는 빈의 생성과 소멸에 관한 범위를 정의하는 방법입니다. 즉, 스코프는 번역 그대로 빈이 존재할 수 있는 범위를 뜻합니다.

프링은 다양한 빈 스코프를 제공합니다. 각 스코프는 특정한 상황에서 빈 인스턴스의 생성과 소멸을 처리하도록 설계되었습니다. 가장 기본적인 스코프는 싱글톤(Singleton)으로, 스프링 컨테이너 내에서 한 개의 빈 인스턴스만 생성되고 이를 모든 빈에서 공유합니다.

싱글톤을 포함하여 다음과 같은 스코프가 있습니다.

  1. Singleton: 애플리케이션 전체에서 하나의 빈 인스턴스를 생성하고 공유합니다. 이는 스프링의 기본 스코프이며, 따로 설정하지 않으면 모든 빈은 이 스코프로 생성됩니다.

  2. Prototype: 빈 요청마다 새로운 인스턴스를 생성합니다. 매번 새로운 객체를 생성하므로 상태를 유지하지 않습니다.

  3. Request: 웹 애플리케이션에서 HTTP 요청 당 하나의 빈 인스턴스를 생성합니다. 각각의 HTTP 요청이 처리될 때마다 새로운 인스턴스를 생성하고 이를 요청 스코프에 저장합니다.

  4. Session: 웹 애플리케이션에서 HTTP 세션 당 하나의 빈 인스턴스를 생성합니다. HTTP 세션이 생성될 때마다 새로운 인스턴스를 생성하고 이를 세션 스코프에 저장합니다.

  5. Global session: Portlet 기반 웹 애플리케이션에서 Portlet 세션 당 하나의 빈 인스턴스를 생성합니다.

  6. Application: ServletContext 당 하나의 빈 인스턴스를 생성합니다. 웹 애플리케이션이 시작될 때 스프링 컨테이너가 생성되며, 이 때 하나의 인스턴스를 생성하고 이를 애플리케이션 스코프에 저장합니다.

  7. Websocket: WebSocket 당 하나의 빈 인스턴스를 생성합니다. 각각의 WebSocket 세션이 시작될 때마다 새로운 인스턴스를 생성하고 이를 WebSocket 스코프에 저장합니다.

싱글톤 스코프 & 프로토타입 스코프 요청 시 비교

싱글톤 스코프(singleton scope)

  1. 싱글톤 스코프의 빈을 스프링 컨테이너에 요청한다.
  2. 스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환한다.
  3. 이후 스프링 컨테이너에 같은 요청이 와도 같은 객체 인스턴스의 빈을 반환.

프로토타입 스코프(prototype scope)

  1. 프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.
  2. 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.
  3. 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에게 반환한다.
  4. 이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환한다.

참고!! 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계를 주입, 초기화까지만 담당한다. 즉! 생성된 프로토타입 빈을 클라이언트에게 반환한 후엔 스프링 컨테이너는 생성되었던 프르토타입 빈을 관리하지 않는다. 그래서 @PreDestroy같은 소멸 메소드가 호출되지 않는다.


싱글톤 스코프 빈 요청 테스트코드 예시

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).isEqualTo(singletonBean2);
        ac.close();
    }

    @Scope("singleton")
    static class SingletonBean {

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

출력 결과

  • ac.getBean으로 해당 클래스를 두번 요청했지만 같은 인스턴스를 반환했다.
  • ac.close로 스프링 컨테이너를 종료 시킬 때 해당 클래스의 소멸 메소드가 정상적으로 호출 된 것을 확인할 수 있다.

프로토타입 스코프 빈 요청 테스트코드 예시

public class PrototypeBeanFind {

    @Test
    public void prototypeBeanFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);

        PrototypeBean PrototypeBean1 = ac.getBean(PrototypeBean.class);
        PrototypeBean PrototypeBean2 = ac.getBean(PrototypeBean.class);
        System.out.println("singletonBean1 = " + PrototypeBean1);
        System.out.println("singletonBean2 = " + PrototypeBean2);
        Assertions.assertThat(PrototypeBean1).isNotSameAs(PrototypeBean2);
        ac.close();
    }

    @Scope("prototype")
    static class PrototypeBean {

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

출력 결과

  • 싱글톤 빈은 스프링 컨테이너 생성 시점에 초기화 메서드가 실행 되지만 프로토타입 스코프의 빈은 스프링 컨테이너에서 빈을 조회할 때 생성되고, 초기화 메소드도 실행된다.
  • 프로토타입 빈을 2번 조회했으므로 다른 인스턴스의 빈이 생성되고, 초기화도 2번 실행된 것을 확인할 수 있다.
  • 프로토타입 빈은 스프링 컨테이너가 빈의 생성, 의존관계 주입 그리고 초기화까지만 관리하기 때문에 스프링 컨테이너가 종료될 때 @PreDestroy같은 소멸 메소드가 실행되지 않는다.

프로토타입 스코프가 싱글톤 빈과 함께 사용할 때 문제점

위에서 얘기했듯이 프로토타입 스코프 빈을 요청하면 항상 새로운 객체 인스턴스를 생성하고 반환한다고 얘기했습니다.
하지만 싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않으므로 주의해야한다.

먼저 스프링 컨테이너에 프로토타입 빈을 직접!! 요청하는 예시코드를 보여드리겠습니다.

public class SingletonWithPrototypeTest1 {

    @Test
    void prototypeFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        prototypeBean1.addCount();
        Assertions.assertThat(prototypeBean1.getCount()).isEqualTo(1);

        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        prototypeBean2.addCount();
        Assertions.assertThat(prototypeBean2.getCount()).isEqualTo(1);
    }

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

  • 클라이언트 A,B가 프로토타입 빈을 요청한다.
  • 스프링 컨테이너는 프로토타입 빈을 각자 새로 생성해서 반환한다.
  • 각각의 count 필드 변수는 0인 상태에서 addCount()메소드를 호출하면서 count 필드 변수는 각각 1이 된다.

이번에는 clientBean이라는 싱글톤 빈이 의존관계 주입을 통해 프로토타입 빈을 주입받아 사용하는 예시코드를 보여드리겠습니다.

public class SingletonWithPrototypeTest1 {

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

        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        Assertions.assertThat(count1).isEqualTo(1);

        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        Assertions.assertThat(count2).isEqualTo(2);
    }

    @Scope("singleton")
    static class ClientBean{

        private PrototypeBean prototypeBean; // 생성시점에 주입

        @Autowired // 생성자가 1개이면 @Autowired 생략가능
        public ClientBean(PrototypeBean prototypeBean) {
            this.prototypeBean = prototypeBean;
        }

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

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

순서!!!

  1. ClienBean은 싱글톤이기에 기본적으로 스프링 컨테이너가 생성 시점시점에 함께 빈들이 생성되고 의존관계 주입도 발생한다.
  2. ClientBean은 의존관계 자동 주입(@Autowired)을 사용한다. 주입 시점에 스프링 컨테이너에게 PrototypeBean타입의 프로토타입 빈을 요청한다.
  3. 스프링 컨테이너는 프로토타입 빈을 생성해서 ClientBean에게 반환한다.

클라이언트 A는 ClientBean을 스프링 컨테이너에게 요청해서 받는다. ClientBean은 싱글톤이기에 항상 같은 인스턴스를 반환된다.

  1. 클라이언트 A는 clientBean.logic()메소드를 호출한다.
  2. clientBean은 prototypeBean의 addCount()메소드를 호출해서 프로토타입 빈의 count 필드변수의 값을 증가시킨다. count = 1

클라이언트 B도 클라이언트 A와 마찬가지가 같은 싱글톤 빈을 반환된다.

  1. 클라이언트 B는 clientBean.logic()메소드를 호출한다.
  2. clientBean은 prototypeBean의 addCount()메소드를 호출해서 프로토타입 빈의 count를 증가시킨다. 원래 count값이 1이었으므로 2가 된다.

중요한 점은 싱글톤 빈인 clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거(빈의 생성시점)에 주입이 끝난 빈이다. 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이므로 사용 할 때마다 새로 생성되는 것이 아니다.!!!

프로토타입 스코프가 싱글톤 빈과 함께 사용할 때 문제점 해결 방법

싱글톤 빈과 프로토타입 빈을 함께 사용할 때, 사용할 때 마다 항상 새로운 프로토타입 빈을 생성할 수 있는 방법을 말씀드리겠습니다.

  1. ObjectFactory 혹은 ObjectProvider
  2. Provider

ObjectProvider사용 시

public class SingletonWithPrototypeTest1 {

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

        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        Assertions.assertThat(count1).isEqualTo(1);

        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        Assertions.assertThat(count2).isEqualTo(1);
    }

    @Scope("singleton")
    static class ClientBean{

        @Autowired
        private ObjectProvider<PrototypeBean> prototypeBeanProvider;

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

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

실행하면 prototypeBeanProvider.getObject()을 통해 항상 새로운 프로토타입 빈을 생성되는 것을 확인할 수 있으며 또한 각각의 count가 1인 것을 확인할 수 있다.

Provider사용

Provider인터페이스를 사용하기 위해서는 라이브러리를 추가해야 한다.

스프링부트 3.0미만
javax.inject:javax.inject:1

스프링부트 3.0이상
jakarta.inject:jakarta.inject-api:2.0.1

@Autowired
private Provider<PrototypeBean> provider;

public int logic() {
    PrototypeBean prototypeBean = provider.get();
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}
profile
결과를 중요시하기보단 과정을 중요하게 생각하는 마음가짐

0개의 댓글