[스프링 핵심 원리 - 기본편] 9. 빈 스코프

HJ·2022년 7월 31일
0

김영한 님의 스프링 핵심 원리 - 기본편 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard


1. 빈 스코프

1-1. 싱글톤

  • 스프링 빈은 기본적으로 싱글톤 스코프로 생성

  • 스프링 컨테이너의 시작과 종료까지 유지되는 스코프


1-2. 프로토타입

  • 스프링 컨테이너가 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 스코프

  • 의존관계를 주입하고 초기화 메소드까지 호출해준다

  • 그 후 클라이언트에게 반환하면 스프링 컨테이너에서 관리하지 않기 때문에 프로토타입 빈을 관리할 책임은 클라이언트에게 있다

  • 그래서 @PreDestroy와 같은 종료 메서드가 호출되지 않는다

  • 지정방법 : @Scope("prototype")


1-3. 웹 관련 스코프

  • 스프링 웹과 관련된 기술이 들어가야 사용할 수 있는 스코프

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

    • 요청이 들어올 때 생성, 나갈 때 destroy
  • session : 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프

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




2. 프로토 타입 스코프 - 싱글톤 빈과 비교

2-1. 요청 : 싱글톤 빈 vs 프로토타입 빈

  • 싱글톤 빈 요청

    • 싱글톤 스코프의 빈을 스프링 컨테이너에 요청하면 스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환하기 때문에 몇 번의 요청을 해도 객체 인스턴스의 스프링 빈이 반환된다

  • 프로토타입 빈 요청

    • 프로토타입 스코프의 빈을 스프링 컨테이너에 요청하면 스프링 컨테이너는 요청이 들어온 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입하고 클라이언트에게 반환한다

    • 하지만 프로토타입 빈은 클라이언트에게 반환된 이후 스프링 컨테이너가 관리하지 않기 때문에 요청을 할 때마다 새로운 프로토타입 빈이 생성되어 반환된다


2-2. 요청 테스트 : 싱글톤 빈 vs 프로토타입 빈

  • 스프링 컨테이너 생성 이후 ( 빈 조회 전 )에 초기화 메서드(init)가 1번만 실행

  • 빈을 조회하면 같은 객체(인스턴스)임을 확인

  • 스프링 컨테이너가 종료(Closing)까지 유지되기 때문에 종료 메서드(destroy)가 실행


  • 스프링 컨테이너에서 빈을 조회할 때 생성되고, 초기화 메소드도 실행된다

  • 빈을 2번 조회했으므로 2개의 서로 다른 스프링 빈이 생성되고, 초기화도 2번 실행

  • 스프링 컨테이너가 생성과 의존관계 주입 그리고 초기화 까지만 관여하기 때문에 스프링 컨테이너가 종료될 때(Closing) 종료 메서드가 실행되지 않음

  • prototypeBean1.destroy() 처럼 직접 호출해주어야 함




3. 싱글톤 빈과 프로토타입 빈을 함께 사용할 때의 문제점

3-1. 예시

  • clientBean이라는 싱글톤 빈이 의존관계 주입을 통해 프로토타입 빈을 주입받아서 사용하는 예시

  • clientBean은 싱글톤이므로, 스프링 컨테이너 생성 시점에 함께 생성되고, 의존관계 주입이 발생한다

  • 프로토타입 빈이 의존관계이기 때문에 스프링 컨테이너에 요청하고 생성된 프로토타입 빈을 받는다

  • clientBean은 프로토타입 빈을 내부 필드에 보관 ( 프로토타입 빈을 관리할 책임은 클라이언트인 clientBean에게 있음 )


  • 클라이언트 A가 스프링 컨테이너에 clientBean을 요청 ( 싱글톤이므로 항상 같은 clientBean이 반환 )

  • 클라이언트 A가 clientBean.logic() 호출하면 clientBean은 prototypeBean의 addCount()를 호출해서 프로토타입 빈의 count를 증가 ( 0 -> 1 )


  • 마찬가지로 클라이언트 B가 스프링 컨테이너에 clientBean을 요청 ( 싱글톤이므로 항상 같은 clientBean이 반환 )

  • clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다

  • 의존관계 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지, 프로토타입 빈을 사용 할 때마다 새로 생성되는 것이 아니다

  • 그러므로 클라이언트 B가 clientBean.logic() 호출하면 clientBean은 이미 주입받은 prototypeBean의 addCount() 를 호출해서 프로토타입 빈의 count를 증가 ( 1 -> 2 )


3-2. 결론

  • 스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환한다

  • 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 싱글톤 빈과 프로토타입 빈이 같이 사용되면 프로토타입 빈이 새로 생성되지만 싱글톤 빈과 함께 계속 유지된다

  • 그렇기 때문에 클라이언트 B가 동일한 작업을 했을 때, 이미 존재하던 프로토타입 빈이 사용된 것이다

  • 주입 시점에만 프로토타입 빈이 생성되는 것이 아니라 사용할 때마다 프로토타입 빈이 생성되게 하려면?? ( 아래에서 설명하는 내용 )




4. 싱글톤 빈과 함께 사용 시 문제 해결

  • 싱글톤 빈과 프로토타입 빈을 함께 사용할 때, 사용할 때 마다 항상 새로운 프로토타입 빈을 생성하기

4-1. 방법 1 : 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청

static class ClientBean {
    @Autowired
    private ApplicationContext ac;

    public int logic() {
        PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
        ...
    }
}
  • clientBean이 @Autowired로 ApplicationContext를 받는다

  • ac.getBean() 을 통해 항상 새로운 프로토타입 빈이 생성 ( 프로토타입 빈은 조회할 때 생성되기 때문 )

  • 의존관계를 외부에서 주입(DI) 받는게 아니라 이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup (DL) 의존관계 조회(탐색) 이라한다

  • but> ApplicationContext 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다

  • ApplicationContext 전체를 주입받는 것이 아니라 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 DL 정도의 기능만 제공하는 무언가가 있으면 된다


4-2. 방법 2 : ObjectProvider<T> 사용

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

  • ObjectProvider<T>

    • 스프링에 의존적임

    • 우리가 직접 찾는 것이 않도록 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공

    • 필요할 때마다 스프링 컨테이너에 요청하는 기능을 사용할 수 있게 된다

    • 예전에 사용하던 ObjectFactory<T>를 상속함

      • ObjectFactory<T>getObject() 하나만을 제공
  • getObject() 를 호출하면 스프링 컨테이너에서 프로토타입 빈을 찾아서 반환한다 (DL)

  • 위의 결과 사진을 보면 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다


4-3. 방법 3 : javax.inject.Provider 라는 JSR-330 자바 표준을 사용

@Scope("singleton")
static class ClientBean {

    @Autowired
    private Provider<PrototypeBean> provider;

    public int logic() {
        PrototypeBean prototypeBean = provider.get();
        ...
    }
}
  • 이 방법을 사용하려면 javax.inject:javax.inject:1 라이브러리를 gradle에 추가해야 한다

  • get()이 위의 getObject()와 동일한 기능을 제공한다 ( DL )

  • 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있음




5. 웹 스코프

5-1. 웹 스코프 특징

  • 웹 스코프는 웹 환경에서만 동작한다

  • 웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리하기 때문에 종료 메서드가 호출된다


5-2. 웹 스코프 종류

  • request

    • HTTP 요청이 들어오고 나갈 때 까지 유지되는 스코프

    • 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리된다

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

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

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


5-3. 참고

  • 웹 스코프는 웹 환경에서만 동작하기 때문에 build.gradle에 라이브러리를 추가해서 사용해야 한다

    • implementation 'org.springframework.boot:spring-boot-starter-web'
  • 위의 라이브러리를 추가하면 스프링 부트는 내장 톰켓 서버를 활용해서 웹 서버와 스프링을 함께 실행하기 때문에 main 메서드를 실행하면 웹 애플리케이션이 실행된다

  • 스프링 부트는 웹 라이브러리가 없으면 AnnotationConfigApplicationContext 을 기반으로 애플리케이션을 구동한다

  • 웹 라이브러리가 추가되면 웹과 관련된 추가 설정과 환경들이 필요하므로
    AnnotationConfigServletWebServerApplicationContext 를 기반으로 애플리케이션을 구동한다




6. request 스코프

6-1. 개념

  • 동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어려운데 이럴 때 사용하기 좋은것이 request 스코프

  • 클라이언트 A, B가 스프링 컨테이너에 동시에 요청한 경우

  • Controller 로직에서 request scope와 관련된 것을 호출하면 각각의 request 요청에 맞춰 해당 클라이언트 전용 객체( 스프링 빈 )가 생성돼서 A와 B에 다른 인스턴스가 할당된다

  • 특정 클라이언트가 Service를 호출하는 경우에도 특정 클라이언트에 맞는 객체를 바라보게 된다

  • HTTP request에 대한 response가 나갈 때 객체 파괴


6-2. 예시

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    // 이 부분이 오류
    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        ...
    }
}
  • 위처럼 Controller와 Service를 작성하고 서버를 실행시키면 오류가 발생

  • @RequiredArgsConstructor가 있기 때문에 스프링 컨테이너가 생성될 때 MyLogger 객체 의존관계 주입이 자동으로 이루어진다 ( @Autowired와 동일 )

  • 의존관계 주입을 위해 스프링 컨테이너에 MyLogger 객체를 달라고 요청

  • MyLogger는 request scope이고 request scope 는 고객의 요청이 들어와야 생성된다

  • but> 서버를 실행하는 시점에는 HTTP request가 없기 때문에 request scope이 생성되지 않았고 생성되지 않은 것을 주입하려고 해서 오류가 발생함

    • BeanCreationException 발생



7. 오류해결 1 : ObjectProvider

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> myLoggerObjectProvider;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        MyLogger myLogger = myLoggerObjectProvider.getObject();
        ...
    }
}

  • Service도 동일하게 코드 변경

  • ObjectProvider 덕분에 getObject() 를 호출하는 시점까지 request scope 빈을 스프링 컨테이너에 요청하는 것을 지연할 수 있음

  • 즉, getObject() 를 호출하는 시점에 MyLogger가 생성된다

    • 그 후 MyLogger에 작성해놓은 init()이 호출된다
  • getObject() 를 호출하시는 시점에는 HTTP 요청이 진행중이므로 request scope 빈이 생성된다

  • getObject() 를 LogDemoController , LogDemoService에서 각각 한 번씩 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환




8. 오류해결 2 : 프록시

8-1. 코드 수정

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
    ...
 }
  • LogDemoController , LogDemoService는 Provider 적용 이전 코드로 다시 작성

  • proxyMode = ScopedProxyMode.TARGET_CLASS 를 추가

    • 적용 대상이 클래스면 TARGET_CLASS

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


8-2. 동작 설명

  1. MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입한다

    • @ScopeproxyMode = ScopedProxyMode.TARGET_CLASS) 를 설정하면 스프링 컨테이너는 CGLIB 라는 바이트코드를 조작하는 라이브러리를 사용해서, MyLogger를 상속받은 가짜 프록시 객체를 생성

    • 생성된 객체를 출력해보면 MyLogger$$EnhancerBySpringCGLIB 이라는 클래스로 만들어진 객체가 대신 등록된 것을 확인할 수 있다

    • 스프링 컨테이너에 myLogger라는 이름으로 가짜 프록시 객체를 등록하기 때문에 ac.getBean()으로 조회해도 프록시 객체가 조회되며, 의존관계 주입 역시 이 가짜 프록시 객체가 주입된다


  1. 생성된 가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다

    • 클라이언트가 myLogger.logic() 을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이고

    • 가짜 프록시 객체는 request scope의 진짜 myLogger.logic()을 호출한다

    • 가짜 프록시 객체는 실제 request scope과 관계없이 내부에 단순한 위임 로직만 있고 싱글톤 처럼 동작한다

    • 가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 원본인지 아닌지 모르게, 동일한 방식으로 사용할 수 있다 (다형성)


8-3. 특징

  • Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다

  • 단지 어노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있는데 이게 다형성과 DI 컨테이너가 가진 큰 강점

  • 웹 스코프가 아니어도 프록시는 사용할 수 있다




참고> 인텔리제이 단축키

  • 자동 리펙터링 : Ctrl + Alt + N

0개의 댓글