[스프링 핵심 원리 - 기본편] 빈 스코프 (싱글톤, 프로토타입, 웹)

강신현·2022년 9월 1일
0

✅ ObjectProvider ✅ Provider


💡 빈 스코프

빈 스코프는 번역 그대로 빈이 존재할 수 있는 범위를 뜻한다.

종류

  • 싱글톤 스코프
    기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
  • 프로토타입 스코프
    스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프
  • 웹 스코프
    • request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프
    • session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
    • application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프

등록 방법

  1. 컴포넌트 스캔 자동 등록
@Scope("prototype")
@Component
public class HelloBean {}
  1. 수동 등록
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
    return new HelloBean();
}

💡 싱글톤 스코프

스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환

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);
        assertThat(singletonBean1).isSameAs(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");
        }
    }
}

실행결과

SingletonBean.init // 빈 초기화 메서드를 실행
singletonBean1 = hello.core.scope.PrototypeTest$SingletonBean@54504ecd // 같은 인스턴스의 빈을 조회
singletonBean2 = hello.core.scope.PrototypeTest$SingletonBean@54504ecd // 같은 인스턴스의 빈을 조회
org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing SingletonBean.destroy // 종료 메서드까지 정

💡 프로토타입 스코프

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

  • 따라서 반환 이후에는 관리하지 않고 버린다.
  • 프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있다.
    그래서 @PreDestroy 같은 종료 메서드가 호출되지 않는다. (종료 메서드에 대한 호출도 클라이언트가 직접 해야한다.)

- 필요성

싱글톤의 경우 객체를 1개만 생성한다는 장점은 있으나
Web 같이 여러명이 같은 객체를 사용하는 과정에서 문제가 발생할 소지가 있다.

예를 들어 클래스의 멤버변수를 만들고 그것을 메소드에 넣은 파라미터가 해당 멤버변수의 값을 변경할 수 있는 구조로 설계가 되어 있을 경우
여러 사람이 같은 객체를 사용하기 때문에 내가 변경한 값을 다른 사람이 또 변경시킬수 있는 문제가 있는 것이다.

그래서 Bean Scope를 Prototype으로 바꿔서 Spring Bean 객체를 만들때 여러사람이 공유하는 변수가 없게끔 설계하고
외부에서 입력받은 파라미터값이 내부 멤버변수의 값을 변경하지 못하게끔 해주는게 좋다.
그러면 요청할때마다 Bean 객체가 생성되기 때문에 충돌현상이 발생하지를 않는다.

매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용하면 된다.
그런데 실무에서 웹 애플리케이션을 개발해보면, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에
프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.


public class PrototypeTest {
    @Test
    public void prototypeBeanFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        System.out.println("find prototypeBean1");
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class); System.out.println("find prototypeBean2");
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class); System.out.println("prototypeBean1 = " + prototypeBean1); System.out.println("prototypeBean2 = " + prototypeBean2);
        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");
        }
    }
}

실행결과

find prototypeBean1
PrototypeBean.init // 싱글톤 빈은 스프링 컨테이너 생성 시점에 초기화 메서드가 실행 되지만, 프로토타입 스코프의 빈은 스프링 컨테이너에서 빈을 조회할 때 생성
find prototypeBean2
PrototypeBean.init 
prototypeBean1 = hello.core.scope.PrototypeTest$PrototypeBean@13d4992d // 프로토타입 빈을 2번 조회했으므로 완전히 다른 스프링 빈이 생성 (초기화도 2번 실행)
prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@302f7971
org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing

💡 싱글톤 빈, 프로토타입 빈 동시 사용

- 문제

스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 된다.
그런데 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지된다.
서로 다른 스코프 처리 방식이 충돌을 원하던 로직대로 처리가 안되는 문제가 발생한다.

  • clientBean 은 싱글톤이므로, 보통 스프링 컨테이너 생성 시점에 함께 생성되고, 의존관계 주입도 발생한다.
  1. clientBean 은 의존관계 자동 주입을 사용한다.
    주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청한다.
  2. 스프링 컨테이너는 프로토타입 빈을 생성해서 clientBean 에 반환한다.
    프로토타입 빈의 count 필드 값은 0이다.
    이제 clientBean 은 프로토타입 빈을 내부 필드에 보관한다. (정확히는 참조값을 보관)
  • 클라이언트 A는 clientBean 을 스프링 컨테이너에 요청해서 받는다.
    싱글톤이므로 항상 같은 clientBean 이 반환된다.
  1. 클라이언트 A는 clientBean.logic() 을 호출한다.
  2. clientBean 은 prototypeBean의 addCount() 를 호출해서 프로토타입 빈의 count를 증가한다.
    count값이 1이 된다.
  • 클라이언트 B는 clientBean 을 스프링 컨테이너에 요청해서 받는다.
  • 싱글톤이므로 항상 같은 clientBean 이 반환된다.
    👉 clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다.
    주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지, 사용 할 때마다 새로 생성되는 것이 아니다.
  1. 클라이언트 B는 clientBean.logic() 을 호출한다.
  2. clientBean 은 prototypeBean의 addCount() 를 호출해서 프로토타입 빈의 count를 증가한다.
    원래 count 값이 1이었으므로 2가 된다.
    👉 원래 의도대로 prototypeBean을 새로 만들어서 사용하지 못하고 싱글톤인 clientBean에 의해 기존에 있던 것을 다시 사용하게 된다.

- 해결법

1. 스프링 컨테이너에 새로 요청

가장 간단한 방법은 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청하는 것이다.

 @Autowired
  private ApplicationContext ac;
  public int logic() {
      PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
      prototypeBean.addCount();
      int count = prototypeBean.getCount();
      return count;
 }
  • ac.getBean() 을 통해서 항상 새로운 프로토타입 빈이 생성된다.
    👉 하지만 이렇게 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다.

2. ObjectProvider

지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 ObjectProvider 이다.

  • Dependency Lookup (DL) 의존관계 조회(탐색) : 의존관계를 외부에서 주입(DI) 받는게 아니라 이렇게 직접 필요한 의존관계를 찾는 것
  • ObjectFactory 에서 편의 기능을 추가해서 만든 것이 ObjectProvider
@Autowired
  private ObjectProvider<PrototypeBean> prototypeBeanProvider;
  public int logic() {
      PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
      prototypeBean.addCount();
      int count = prototypeBean.getCount();
      return count;
}
  • prototypeBeanProvider.getObject() 을 통해서 항상 새로운 프로토타입 빈이 생성된다.
  • ObjectProvider 의 getObject() 를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)
  • 스프링이 제공하는 기능을 사용하지만, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.

👉 하지만 스프링에 의존하기 때문에 다른 컨테이너에서도 사용할 수 없다.

3. Provider

자바 표준을 사용하여 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공

//implementation 'javax.inject:javax.inject:1' gradle 추가 필수

@Autowired
private Provider<PrototypeBean> provider;

public int logic() {
    PrototypeBean prototypeBean = provider.get();
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}
  • get() 메서드 하나로 기능이 매우 단순하다.
  • 별도의 라이브러리가 필요하다.
  • 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.

💡 웹 스코프

웹 환경에서만 동작한다.
프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다.

  • 따라서 종료 메서드가 호출된다.

종류

  1. request : HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다.
  2. session : HTTP Session과 동일한 생명주기를 가지는 스코프
  3. application : 서블릿 컨텍스트( ServletContext )와 동일한 생명주기를 가지는 스코프
  4. websocket : 웹 소켓과 동일한 생명주기를 가지는 스코프

- request 스코프

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

예제

동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어려운데 이럴때 사용하기 딱 좋음

requestURL을 MyLogger에 저장하는 부분은 컨트롤러 보다는 공통 처리가 가능한 스프링 인터셉터나 서블릿 필터 같은 곳을 활용하는 것이 좋으나
예제를 단순화하고, 아직 스프링 인터셉터를 학습하지 않았으므로 컨트롤러를 사용하여 구현했다.

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

👉 스프링 애플리케이션 실행 시점에는 http 요청이 없기 때문에 request 스코프 빈은 아직 생성되지 않아 오류가 발생하므로 Provider를 사용해야 한다.
즉, ObjectProvider을 사용하여 ObjectProvider.getObject() 를 호출하는 시점까지 request scope 빈의 생성을 지연해야 한다.

@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("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}
@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final ObjectProvider<MyLogger> myLoggerProvider;

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

👉 프록시 방식을 사용하면 코드를 더 간소화 할 수 있다. (강의 자료 참고)


강의 출처

[인프런 - 김영한] 스프링 핵심 원리 - 기본편

https://okky.kr/articles/990588?note=2417006

profile
땅콩의 모험 (server)

0개의 댓글