빈 스코프

su dong·2023년 7월 25일
0

종류

빈 스코프 = 빈이 존재할 수 있는 범위

싱글톤

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

프로토타입

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

웹 관련 스코프

request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프이다.
session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프이다.
application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프이다.


프로토타입 스코프

싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다. 반면에 프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다. 그리고 반환하고 나서는 스프링 컨테이너에서는 가지고 있지 않는다.

프로토타입 빈을 요청하는 시점에 생성하고, 의존관계 주입해서 반환 후 새로운 요청이 오면 또 새로 만들어서 반환한다.

-> 클라이언트가 책임지고 종료메서드를 호출해 줘야한다.
-> 그래서 @PreDestroy 같은 종료 메서드가 호출되지 않는다.

Test

package com.example.springframework.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.AssertionsForClassTypes.assertThat;

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

        PrototypeBean bean1 = ac.getBean(PrototypeBean.class);
        PrototypeBean bean2 = ac.getBean(PrototypeBean.class);
        System.out.println("bean1 = " + bean1);
        System.out.println("bean2 = " + bean2);
        assertThat(bean1).isNotSameAs(bean2);
        ac.close();
    }

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

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

출력 결과

1) close가 작동되지 않음!
-> 만약 종료를 호출하고 싶다면

bean1.destroy(); 
bean2.destroy();

이런 식으로 직접 호출해줘야 한다.
2) 서로 다른 bean이 생성된 것을 알 수 있음 -> init도 2번 호출됨


프로토타입 스코프를 싱글톤 스코프와 동시 사용시 생기는 문제점

그럼 프로토타입으로 구현된 빈이 있고 그 빈은 count=0이라는 속성을 가지고 있다고 하자. 그리고 addCount()를 통해서 값이 1만큼 올라갈 수 있을때,

public class SingletonWithPrototypeTest1 {
    @Test
    void prototypeFind(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(prototypeBean.class);
        prototypeBean bean1 = ac.getBean(prototypeBean.class);
        prototypeBean bean2 = ac.getBean(prototypeBean.class);

        bean1.addCount();
        System.out.println(bean1.getCount());
        assertThat(bean1.getCount()).isEqualTo(1);
        bean2.addCount();
        System.out.println(bean2.getCount());
        assertThat(bean2.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");
        }
    }
}

위 테스트는 통과가 된다. 즉 각각 1씩 오른 것이다.(싱글톤이 아니니깐 당연한지도!)


Singleton with Prototype

그렇다면 만약 clientBean(싱글톤)이 프로토타입빈에 의존하고 있는 경우에도 다른 사용자의 요청에 각각 다르게 프로토타입빈이 응답할 수 있을까?
정답은 아니다! 왜냐하면 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();
        assertThat(count1).isEqualTo(1);
        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        assertThat(count2).isEqualTo(2);
    }

    @Scope("singleton")
    static class ClientBean {
        private final PrototypeBean prototypeBean;

        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 - ApplicationContext 주입받은 후 매번 호출

만약 싱글톤으로 구현하면서 매번 고객이 다를때마다 다른 응답을 하고 싶다면,
ApplicationContext를 직접 주입받아서 logic()메서드를 실행할때마다 application.getBean(Prototype.class)를 가져와서 실행해주면 된다.

@Scope("singleton")
    static class ClientBean2 {
        @Autowired
        ApplicationContext applicationContext;

        public int logic() {
            PrototypeBean prototypeBean = applicationContext.getBean(PrototypeBean.class);
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

하지만 이 방법은 코드가 지저분해지므로 지양된다.
의존관계를 외부에서 주입(DI) 받는게 아니라 이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup (DL) 의존관계 조회(탐색) 이라한다.
그런데 이렇게 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다.
지금 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 딱! DL 정도의 기능만 제공하는 무언가가 있으면 된다.


해결방안 2 - ObjectProvider 사용(구버전 : ObjectFactory)

@Scope("singleton")
    static class ClientBean3 {
        @Autowired
        private ObjectProvider<PrototypeBean> prototypeBeanProvider;

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

ObjectProvider를 사용하면 .getObject()시점에 스프링컨테이너를 통해 해당 빈을 찾아서 반환한다.(DL)
이렇게 하면 좋은 점이 ApplicationContext의 전체 기능을 사용하는 것이 아니라, 필요한 기능만 짤라서 사용하기 때문에 스프링 컨테이너에 대해 의존적이지 않게 된다.


해결방안 3 - JSR-330 Provider

build.gradle에 의존성 추가

implementation 'javax.inject:javax.inject:1'

실습코드

import javax.inject.Provider;

//javax.inject.provider
    @Scope("singleton")
    static class ClientBean4 {
        @Autowired
        private Provider<PrototypeBean> prototypeBeanProvider;

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

특징

get() 메서드 하나로 기능이 매우 단순하다.
별도의 라이브러리가 필요하다.
자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.


Request Scope

http요청시 생성되고 요청이 끝나면 소멸됨. 요청당 생성된다


스코프와 프록시

profile
사람들을 돕는 문제 해결사, 개발자 sudong입니다. 반갑습니다. tkddlsqkr21@gmail.com

0개의 댓글