조컴퓨터

Bean Scope - singleton, prototype, request 본문

공부/Spring

Bean Scope - singleton, prototype, request

챠오위 2022. 1. 22. 22:06

빈 스코프란?

스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료될 때까지 유지될 수 있는 것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. 스코프는 번역 그대로 빈이 존재할 수 있는 범위를 뜻한다.

 

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

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

- request: 웹 요청이 들어오고 나갈 때까지 유지되는 스코프

- session: 웹 세션이 생성되고 종료될 때까지 유지되는 스코프

- application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프

 

spring.io 에서 제시하는 Bean Scope

 

 

프로토타입 스코프 prototype scope

싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다. 

하지만 프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다.

 

스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다. 

클라이언트에 빈을 반환하고, 이후 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않는다. 프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있다. 그래서 @PreDestroy 같은 종료 메서드가 호출되지 않는다.

 

 

 

※ 싱글톤 빈과 함께 사용시 문제점

package hello.core.scope;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
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.Assertions.*;

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);
//        System.out.println("clientBean1 = " + clientBean1);

        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        assertThat(count2).isEqualTo(2);
//        System.out.println("clientBean2 = " + clientBean2);
    }

    static class ClientBean {
        private final PrototypeBean prototypeBean;  // 생성 시점에 주입

        @Autowired
        public ClientBean(PrototypeBean prototypeBean) {
            this.prototypeBean = prototypeBean;
        }

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

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

- 위에서 clientBean 이 내부에 가지고 있는 프로토타입 빈은 첫 생성 시점에 주입이 끝난 빈이다. 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지, 사용할 때마다 새로 생성되는 것이 아니다.

- 위에서 싱글톤 빈(clientBean) 은 생성 시점에만 의존관계 주입을 받기 때문에 프로토타입 빈이 생성되기는 하지만, 해당 프로토타입 빈(prototypeBean) 은 싱글톤 형태처럼 clientBean 과 함께 계속 유지가 된다는 것이 문제이다.

 

- 프로토타입 빈을 주입 시점에만 새로 생성하는 것이 아니라, 사용할 때마다 새로 생성되는 것을 원한다. 어떻게 해야 할까? 다음과 같은 방법으로 해결 가능하다.

 

1. DL(Dependency Lookup) 방식: 빈에 접근하기 위해 컨테이너가 제공하는 API 를 이용하여 빈을 찾는 것

2. Scope proxy: (말 그대로 프록시)

 

 

 

1. DL(Dependency Lookup) 방식

1-1) @Autowired 를 이용해 ApplicationContext 를 의존관계 주입한 후, getBean() 을 호출한다.

이 방법은 스피링 컨테이너에 의존한다는 단점이 있다. 단위 테스트도 어려워진다.

 

1-2) ObjectFactory, ObjectProvider

이 방법은 ApplicationContext 에 직접 접근하지 않고 스프링이 제공하는 ObjectProvider 라는 빈을 이용하여 빈을 가져올 수 있게 도와준다. 

 

- ObjectFactory: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존

- ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리 등의 편리 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존

@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;

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

- prototypeBeanProvider.getObject() 를 통해서 항상 새로운 프로토타입 빈이 생성된다.

- ObjectProvider 의 getObject() 를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)

- 스프링이 제공하는 기능을 사용하지만, 기능이 단순하므로 단위 테스트를 만들거나 mock 코드를 만들기 쉬워진다.

 

 

1-3) JSR-330 Provider

라이브러리 javax.inject.Provider 라는 JSR-330 Provider 를 사용하는 방법

//implementation 'javax.inject:javax.inject:1' gradle 추가 필수
@Autowired
private Provider<PrototypeBean> provider;

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

- provider.get() 을 통해서 항상 새로운 프로토타입 빈이 생성된다.

- provider 의 get() 을 호출하면 냉부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)

- 자바 표준이고, 기능이 단순하므로 단위 테스트를 만들거나 mock 코드를 만들기 쉬워진다.

 

 

2) Scope Proxy 를 이용한 해결 방법을 살펴보기 이전에 웹 스코프를 먼저 살폈다.

 

 

 

웹 스코프

- 웹 환경에서만 동작한다.

- 프로토타입과 다르게 스프링이 해당 스코프의 종료 시점까지 관리한다. 따라서 종료 메서드가 호출된다.

 

 

웹 스코프의 종류

- request: HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다.

- session: HTTP Session 과 동일한 생명 주기를 가지는 스코프

- application: 서블릿 컨텍스트(ServletContext) 와 동일한 생명 주기를 가지는 스코프

- websocket: 웹 소켓과 동일한 생명 주기를 가지는 스코프

 

 

 

웹 스코프는 웹 환경에서 동작하기 때문에 웹 환경을 위한 라이브러리 추가가 필요하다. (tomcat 등)

//web 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-web'

 

참고) 위의 라이브러리를 추가하면 스프링 부트는 내장 톰캣 서버를 활용해서 웹 서버와 스프링을 함께 실행시킨다.

(스프링 부트 기능인 듯하다. 이로 인해 JAR 파일로 한데 묶어서 서버에 올리면 톰캣과 함께 동작하는 것 같다.)

 

참고) 스프링 부트는 웹 라이브러리가 없으면 AnnotationConfigApplicationContext 를 기반으로 애플리케이션을 구동한다. 웹 라이브러리가 추가되면 웹과 관련된 추가 설정과 환경들이 필요하므로 AnnotationConfigServletWebServerApplicationContext 를 기반으로 애플리케이션을 구동한다.

 

 

request 스코프 예제

- 스코프와 Provider, 스코프와 Proxy

package hello.core.common;

import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.UUID;

@Component
@Scope(value = "request", 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() {
        uuid = UUID.randomUUID().toString();
        System.out.println("[" + uuid + "] request scope bean create:" + this);
    }

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

- @Scope 에서 proxyMode = ScopedProxyMode.TARGET_CLASS 를 설정

-- 적용 대상이 인터페이스가 아닌 클래스면 TARGET_CLASS 

-- 적용 대상이 인터페이스면 INTERFACES

- MyLogger 의 가짜 프록시 클래스를 생성하고, HTTP request 와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다.

 

package hello.core.web;

import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

@Controller	// MyLogger 가 잘 작동하는지 확인하는 테스트용 컨트롤러
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURI().toString();
        System.out.println("myLogger = " + myLogger.getClass());
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}

참고) requestURL 을 MyLogger 에 저장하는 부분은 컨트롤러 보다는 공통 처리가 가능한 스프링 인터셉터나 서블릿 필터 같은 곳을 활용하는 것이 좋다. 스프링 웹에 익숙하다면 인터셉터를 사용해서 구현해보자.

 

package hello.core.web;

import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;

    public void logic(String id) {
        myLogger.log("service id = " + id);
    }
}

- request scope 를 사용하지 않고 파라미터로 이 모든 정보를 서비스 계층에 넘긴다면, 파라미터가 많아서 지저분해진다. 더 문제는 requestURL 같은 웹과 관련된 정보가 웹과 관련없는 서비스 계층까지 넘어가게 된다. 웹과 관련된 부분은 컨트롤러까지만 사용해야 한다. 서비스 계층은 웹 기술에 종속되지 않고, 가급적 순수하게 유지하는 것이 유지보수 관점에서 좋다.

 

 

웹 스코프와 프록시 동작 원리

myLogger 에 대한 정보를 확인하면

System.out.println("myLogger = " + myLogger.getClass());

 

출력 결과

myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$7a6f9a74

- MyLogger 클래스가 아닌 MyLogger$$EnhancerBySpringCGLIB 이라는 클래스로 만들어진 객체가 대신 등록된 것을 알 수 있다.

- 스프링 컨테이너는 CGLIB 이라는 바이트코드를 조작하는 라이브러리를 사용해서 MyLogger 를 상속받은 가짜 프록시 객체를 생성한 것이다.

- 스프링 컨테이너에 myLogger 라는 이름으로 진짜 대신에 이 가짜 프록시 객체를 등록한다.

- ac.getBean("myLogger", MyLogger.class) 로 조회해도 프록시 객체가 조회되는 것을 확인할 수 있다. ** 이 부분 확인 필요

- 의존관계 주입도 이 가짜 프록시 객체가 주입된다.

- CGLIB 이라는 라이브러리가 내 클래스를 상속받은 가짜 프록시 객체를 만들어서 주입한다.

 


* 내가 이해하는 proxy 의 원리는 client 와 스프링 컨테이너 사이에서 client 가 던져주는 request 를 받고 그 request 를 스프링 컨테이너에 던져서 return 받은 후 해당 return 값을 그대로 client 에게 돌려주는 것이다.

** 여기서 client 에게 돌려줄 때 스프링 컨테이너가 누구인지 모르게(?) 던져준다. 

*** 이는 client 와 서버 사이의 proxy server 의 관계도에서 착안하여 이해하였는데, client 와 스프링 컨테이너의 관계 또한 이렇지 않을까 싶다. 


 

- 가짜 프록시 객체는 실제 요청이 올 때, 내부에서 진짜 빈을 요청하는 위임 로직을 호출한다. 

- 가짜 프록시 객체는 내부에 진짜 myLogger 를 찾는 방법을 알고 있다.

- 클라이언트가 myLogger.logic() 을 호출하는 것은 사실은 가짜 프록시 객체의 메서드를 호출한 것이다.
- 가짜 프록시 객체는 request 스코프의 진짜 myLogger.logic() 를 호출한다.
- 가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 원본인지 아닌지도 모르게, 동일하게 사용할 수 있다. (다형성)

 

 

 

프록시의 특징

- Provider, Proxy 두 가지 다 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연 처리한다는 점이다.

- 프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯 편리하게 request scope 를 사용할 수 있다. (Provider 과 다르게 원본 코드를 거의 손대지 않았다는 점을 봤을 때)

- 단지 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 이것이 바로 다형성과 DI 컨테이너가 가진 큰 강점이다.

 

 

 

참고)

1.  김명한 님의 '스프링 핵심 원리 - 기본편' 에서 빈 스코프

2. Bean 그리고 scope :: Enjoy when you can, Endure when you must (tistory.com)

 

Bean 그리고 scope

지난번 Bean이 무엇인지 간략하게 알아봤습니다. (IoC container, Bean 관련 개념이 아직 부족하시다면 간략히 이전 포스트 참고해 주세요.) Bean을 사용하는 이유 중 하나가 scope가 자동으로 singleton으로

exponential-e.tistory.com

3. https://blog.naver.com/gngh0101/222344239921

 

Spring Bean Scope 내부 속으로 (singleton, prototype, request, session, application ... etc)

Spring Bean Scope 내부 속으로 (singleton, prototype, request, session, application ... etc) Bean...

blog.naver.com

4. Spring 4.3 ObjectProvider - 머루의개발블로그 (wonwoo.ml)

 

Spring 4.3 ObjectProvider - 머루의개발블로그

이번 포스팅은 Spring 4.3에서 추가된 인터페이스에 대해서 알아보자. 다 알아보기에는 필자도 어느 것이 추가 된지 정확하게 몰라서 다 알아보지는 못하고 제목에 써 있는 그대로 ObjectProvider 대해

wonwoo.ml

 

 

 

 

'공부 > Spring' 카테고리의 다른 글

쓰레드 풀  (0) 2022.01.24
서블릿 컨테이너(WAS)  (0) 2022.01.24
빈 생명주기 콜백 - @PostConstruct, @PreDestroy, @Bean  (0) 2022.01.21
@Primary, @Qualifier 우선순위 (질문 및 숙제)  (0) 2022.01.21
Lombok 과 최신 트랜드  (0) 2022.01.21