HyunsooZo's TIL logo HyunsooZo's TIL

Bean Scope?

Spring Bean은 기본적으로 Singleton Scope로 생성된다.
Scope는 번역 그대로 Bean이 존재할 수 있는 범위를 뜻한다.

Spring은 다음과 같은 다양한 Scope를 지원한다 |Scope|Description| |–|–| |Singleton|기본스코프, 스프링 컨테이너의 시작-종료까지 유지되는 가장 넓은 범위의 스코프| |Prototype|스프링컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프| |request
(web)|web 요청이 들어오고 나갈 때까지 유지되는 스코프| |session
(web)|Web 세션이 생성되고 종료될 때까지 유지되는 스코프| |application
(web)|웹의 서블릿 컨텍스와 같은 범위로 유지되는 스코프|

Compnent Scan 시

@Scope("prototype")
@Compnent
public class HelloBean{}

수동 등록 시

@Scope("prototype")
@Bean
PrototypeBean HelloBean(){
    return new HelloBean();
}

Singleton Scope

Singleton Scope Bean을 조회하면 Spring Container는 항상 같은 인스턴스의 Spring Bean을 반환한다.
1. 싱글톤 스코프의 빈을 스프링 컨테이너에 요청
2. 스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환
3. 이후 스프링 컨테이너에 같은 요청이 와도 같은 객체 인스턴스의 스프링 빈 반환
public class SingletonTest {
    @Test
    void singleTonBeanFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
        SingletonBean bean1 = ac.getBean(SingletonBean.class);
        SingletonBean bean2 = ac.getBean(SingletonBean.class);
        System.out.println("bean1 = " + bean1);
        System.out.println("bean2 = " + bean2);
        Assertions.assertThat(bean1).isSameAs(bean2);
        ac.close();
    }

    @Scope("singleton") //("singleton") 생략가능
    static class SingletonBean{
        @PostConstruct
        public void init(){
            System.out.println("SingletonBean.init");
        }
        @PreDestroy
        public void destroy(){
            System.out.println("SingletonBean.destroy");
        }
    }
}

Prototype Scope

Prototype Scope 를 Spring Container에 조회하면 Spring Container는 항상 새로운 인스턴스를 생성하여 반환한다.
1. 프로토 타입 스코프의 빈을 스프링 컨테이너에 요청
2. 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계에 주입하여 빈을 반환.
3. 이후에 같은 ㅛ청이 와도 항상 새로운 프로토타입 빈을 생성해 반환

핵심
Spring Containerprototype Bean 을 생성하고, 의존관계 주입, 초기화까지만처리한다는 것이다.
클라이언트에 Bean을 반환하고, 이후 Spring Container는 생성된 prototype Bean을 관리하지 않는다. 즉, prototype Bean을 관리할 책임은 prototype Bean을 받은 Client에 있다.
그러므로 @PreDestroy 같은 종료 메서드가 호출되지 않는다.

public class PrototypeTest {
    @Test
    void prototypeBeanFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        System.out.println("find prototypeBean1");
        PrototypeBean bean1 = ac.getBean(PrototypeBean.class);
        System.out.println("find prototypeBean1");
        PrototypeBean bean2 = ac.getBean(PrototypeBean.class);
        System.out.println("bean1 = " + bean1);
        System.out.println("bean2 = " + bean2);
        Assertions.assertThat(bean1).isNotSameAs(bean2);
        ac.close();
    }

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

테스트 결과

find prototypeBean1 // 두 개 빈 조회될때마다 따로 생성
prototypeBean.init
find prototypeBean2 // 두 개 빈 조회될때마다 따로 생성
prototypeBean.init
bean1 = hello.core.scope.PrototypeTest$PrototypeBean@3f4faf53  // 인스턴스 다름
bean2 = hello.core.scope.PrototypeTest$PrototypeBean@7fd50002  // 인스턴스 다름

//close되지 않음. -> @preDestroy 실행 안되었음을 확인할 수 있음

23:17:14.257 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@69b2283a, started on Sat May 13 23:17:14 KST 2023

Prototype, Singleton Scope를 같이 쓸때 문제점

Spring Container에서 prototype scope Bean을 요청하면, 항상 새로운 객체 인스턴스를 생성하여 반환한다.
하지만 Singleton Bean과 함께 사용할때에는 의도한 대로 잘 동작하지 않으므로 주의가 필요하다
public class SingletonWPrototypeTest1 {
    @Test
    void prototypeFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        PrototypeBean bean1 = ac.getBean(PrototypeBean.class);
        bean1.addCount();
        Assertions.assertThat(bean1.getCount()).isEqualTo(1);

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

    @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 final PrototypeBean prototypeBean;  

        @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.inti"+this);
        }
        @PreDestroy
        public void destroy(){
            System.out.println("PrototypeBean.destroy");
        }
    }
}

++참고 해결방법 (prototype Bean을 사용할때마다 생성하기)

Singleton Bean이 prototype Bean을 사용할 때 마다 Spring Container에 새로 요청하기. (원시적인 방법이므로 추천되지 않음)

해결방법(2) (@ObjectProvider 사용하기)

@ObjectFactory or ObjectProvider Annotation 사용하기!!

@ObjectProvider의 핵심 컨셉은 prototype Bean을 대신 조회해 주는 대리인 같은 느낌이다!

다만 Spring에 의존적인 코드가 된다는 단점도 존재한다.

@Autowired
        private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public class SingletonWPrototypeTest1 {
    @Test
    void prototypeFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        PrototypeBean bean1 = ac.getBean(PrototypeBean.class);
        bean1.addCount();
        Assertions.assertThat(bean1.getCount()).isEqualTo(1);

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

    @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.inti"+this);
        }
        @PreDestroy
        public void destroy(){
            System.out.println("PrototypeBean.destroy");
        }
    }
}
/*
PrototypeBean.intihello.core.scope.SingletonWPrototypeTest1$PrototypeBean@6f10d5b6
PrototypeBean.intihello.core.scope.SingletonWPrototypeTest1$PrototypeBean@222a59e6
두 개의 각각 다른 인스턴스가 반환되는 것을 볼 수 있다. 
*/

해결방법(3) - Provider 라이브러리 사용하기

`get()` 메서드 하나로 기능이 매우 단순한편
별도의 라이브러리가 필요함.
하지만 자바 표준이므로 Spring이 아닌 다른 Container에서도 사용가능!

Dependency

//build.gradle
dependencies {
    ...
    // 라이브러리 추가! 
	implementation 'javax.inject:javax.inject:1'
    ...
}

사용예시

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

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

Web Scope

web scope는 웹 환경에서만 동작한다.
프로토타입과는 다르게 Spring이 해당 스코프의 종료시점까지 관리하므로, 종료메서드가 호출된다.
web scope 종류 -
request
(web)
web 요청이 들어오고 나갈 때까지 유지되는 스코프
각 Http 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.
session
(web)
Web 세션이 생성되고 종료될 때까지 유지되는 스코프
HTTP Session과 동일한 생명주기를 가진다.
application
(web)
웹의 서블릿 컨텍스와 같은 범위로 유지되는 스코프
서블릿 컨텍스트와 동일한 생명주기를 가진다.
websocket
(web)
웹소켓과 동일한 생명주기를 가지는 스코프

Request Scope

web library 추가

`spring-boot-starter-web` 라이브러리를 추가하면 스프링부트는 내장 톰캣 서버를 활용해서 웹서버와 스프링을 함께 실행시킨다.
dependencies {
    ...
	implementation 'org.springframework.boot:spring-boot-starter-web'
    ...
}

아래와 같이 로그가 남도록 Request Scope를 활용해볼 것이다. UUID를 이용해 HTTP요청을 구분할 것이고,
requestURL정보도 추가로 넣어 어떤 URL을 요청해서 남은 로그인지 확인해보겠다.

@Component
@Scope(value = "request")
public class MyLogger {
    private String uuid;
    private String requestURL;

    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    public void log(String message) {
        System.out.println("[" + uuid + "]" + " [" + requestURL + "] " + message);
    }

    @PostConstruct
    public void init() {
        this.uuid = UUID.randomUUID().toString();
        System.out.println("[" + uuid + "] request scope bean crete:" + this);
    }

    @PreDestroy
    public void close() {
        System.out.println("[" + uuid + "] request scope bean close:" + this);
    }
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final ObjectProvider<MyLogger> myLoggerProvider;
    public void logic(String testId) {
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log("service id = " + testId);
    }
}

@Controller
@RequiredArgsConstructor
public class LogDemoController {
    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> myLoggerProvider;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request){
        String requestURL = request.getRequestURL().toString();
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.setRequestURL(requestURL);
        myLogger.log("controllet test");
        logDemoService.logic("testId");
        return "ok";
    }
}

Log

[5d138431-afac-4ab0-a20c-a9f5b25409be] request scope bean crete:hello.core.common.MyLogger@4b153236
[5d138431-afac-4ab0-a20c-a9f5b25409be] [http://localhost:8080/log-demo] controllet test
[5d138431-afac-4ab0-a20c-a9f5b25409be] [http://localhost:8080/log-demo] service id = testId
[5d138431-afac-4ab0-a20c-a9f5b25409be] request scope bean close:hello.core.common.MyLogger@4b153236

Scope 와 Proxy

다만 위와 같이 web scope를 사용하게 되면
ObjectProvider(MyLogger) 와 같은 추가적인 코드작성이 필요하다. 하지만 Proxy를사용하면 위와 같은 코드추가 없이 해결이 가능하다.
가짜 Proxy Class(CGLIB)를 만들어 주입시키는 개념이다.
단 무분별한 사용은 유지보수에 어려움을 주므로 꼭 필요할 때 사용하도록..

@Scope(value = “request”, proxyMode = ScopedProxyMode.TARGET_CLASS)추가

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) //proxyMode = ScopedProxyMode.TARGET_CLASS 추가
public class MyLogger {
    private String uuid;
    private String requestURL;

    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    public void log(String message) {
        System.out.println("[" + uuid + "]" + " [" + requestURL + "] " + message);
    }

    @PostConstruct
    public void init() {
        this.uuid = UUID.randomUUID().toString();
        System.out.println("[" + uuid + "] request scope bean crete:" + this);
    }

    @PreDestroy
    public void close() {
        System.out.println("[" + uuid + "] request scope bean close:" + this);
    }
}

   @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request){
        String requestURL = request.getRequestURL().toString();
        myLogger.setRequestURL(requestURL);
        myLogger.log("controllet test");
        logDemoService.logic("testId");
        return "ok";
    }
    @Service
    @RequiredArgsConstructor
    public class LogDemoService {
        private final MyLogger myLogger;
        public void logic(String testId) {
            myLogger.log("service id = " + testId);
        }
    }

Log

[09fcbeae-8fc8-49da-b309-b958e1e63f0b] request scope bean crete:hello.core.common.MyLogger@7b7b8f33
[09fcbeae-8fc8-49da-b309-b958e1e63f0b] [http://localhost:8080/log-demo] controllet test
[09fcbeae-8fc8-49da-b309-b958e1e63f0b] [http://localhost:8080/log-demo] service id = testId
[09fcbeae-8fc8-49da-b309-b958e1e63f0b] request scope bean close:hello.core.common.MyLogger@7b7b8f33
TOP