본문 바로가기
Web/spring study

[Spring] 빈 스코프 (@Scope)

by 장인이 2023. 1. 10.

0. 개요

 이번 게시물에서는 스프링 스코프에 대해 작성할 것이다. 해당 게시물을 보기 전, 아래 링크의 글을 보고 오는 것을 추천한다.

(스프링 빈과 스프링 컨테이너)

 

 

1. 빈 스코프란?

모든 설명에 나오는 예시 프로젝트의 설계 방식은 아래 링크를 참고
https://imgzon.tistory.com/114

 

 빈 스코프빈이 존재할 수 있는 범위를 뜻한다. 스프링은 다음과 같은 다양한 스코프를 지원한다.

 

- 싱글톤: 기본 스코프(default), 스프링 컨테이너의 시작과 끝을 함께하는 가장 넓은 범위의 스코프이다.

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

 

- 웹 관련 스코프

  - request: 웹 요청이 들어올때 생성되며, 나갈때 까지 유지되는 스코프이다.

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

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

@Scope("prototype")
@Component
public class TestBean {}

 

빈 스코프는 위와 같이 @Scope 애노테이션을 사용하여 지정할 수 있다.

 

2. 싱글톤 스코프

 

싱글톤 패턴에 대한 자세한 내용은 아래 게시물을 참고 부탁한다.

(싱글톤 패턴)

 

 

3. 프로토타입 스코프 

 클라이언트가 프로토타입 스코프를 요청할 경우, 그 시점에서 프로토타입 빈을 생성하고, 의존관계를 주입한다.

 

 그 후 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환해준다. 여기서 주목할 점은, 같은 요청이 오면 그때마다 새로운 프로토타입 빈을 생성해서 반환한다.

 

 즉, 스프링 컨테이너는 프로토타입 빈 생성, 의존관계 주입, 초기화까지만 처리한다. 그 이후는 빈을 받은 클라이언트에서 책임을 져야 한다. 따라서 @PreDestroy 같은 소멸 콜백이 호출되지 않는다.

(@PreDestroy란?)

 

 

4. 싱글톤 빈과 프로토타입 빈을 함께 사용시 문제점

 이때, 싱글톤 빈과 프로토타입 스코프의 빈을 함께 사용할 경우 문제점이 발생한다. 즉, 싱글톤 빈과 프로토타입 스코프 빈이 서로 의존관계로 엮여있을 경우, 큰 문제가 발생한다. 아래 그림 흐름대로 쭉 이해해보자.

 

 싱글톤인 clientBean프로토타입 스코프인 prototypeBean 의존관계를 주입받는다고 가정해보자. cliendBean을 싱글톤이므로, 빈 생성과 의존관계 주입이 스프링 컨테이너 생성시점에 이루어진다. 그렇다면,

 

1) clientBean은 의존관계 자동 주입을 사용한다.

2) 스프링 컨테이너는 프로토타입 빈을 생성, clientBean에 넘겨준다. 이때 prototypeBean의 count 필드 값은 0이다.

 

 그 후,

1) 클라이언트 A가 clientBean을 스프링컨테이너에 요청해서 반환받는다.

2) 그 후, clientBean의 logic 메서드를 호출한다.

3) prototypeBean의 count값이 증가된다.

4) 따라서 count값은 1이 된다.

 

1) 클라이언트 B도 clientBean을 스프링컨테이너에 요청해서 반환받는다.

2) 여기서 clientBean이 참조하고 있는 prototypeBean은 이미 과거에 주입이 끝난 빈이다. 즉, 사용할때마다 새로 생성되는 것이 아니다!

3) 클라이언트 B는 clientBean의 logic 메서드를 호출한다.

4) count값이 증가해서 2가 된다.

 

우리가 기대한 결과: 클라이언트가 실행할때마다 각 요청마다 count값이 1

실제 결과: count값이 1, 2, ... 늘어나게 됨

 

 

5. DL(Dependency Lookup)

 4번의 문제를 가장 간단하게 해결하는 방법은, 싱글톤 빈이 프로토타입 빈을 사용할때마다 컨테이너에 새로 요청하는 방식이다.

public class ClientBean {
    @Autowired
    private ApplicationContext ac;
    
    public int logic() {
    	PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
        protoTypeBean.addCount();
        int count = prototypeBean.getCount();
        return count;
    }
}

 

 위 코드를 보면, 직접 ac.getBean() 메서드를 통해서 logic 메서드가 실행될때마다 새로운 프로토타입 빈이 생성된다. 이렇게 의존관계를 외부에서 주입받는 것이 아닌, 직접 찾는 것을 DL(Dependency Lookup)이라고 한다.

 

 하지만, 지금 상황은 ApplicationContext 전체를 주입받게 되므로, 스프링 컨테이너에 종속적이게 되며 단위테스트가 어려워진다.

 

 

6. ObjectFactory, ObjectProvider

 5번의 상황과 달리, 스프링에는 프로토타입 빈을 대신 찾아주는 기능만 제공하는 ObjectProvider를 제공해준다(DL정도 기능만 제공). 과거에 ObjectFactory를 사용했으며, 여기에 여러 편의기능을 추가해서 ObjectProvider가 탄생하였다.

public class ClientBean {

    @Autowired
    private ObjectProvider<PrototypeBean> prototypeBeanProvider;
    
    public int logic() {
        PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
        prototypeBean.addCount();
        int count = prototypeBean.getCount();
        return count;
    }
}

 

 prototypeBeanProvider.getObject() 메서드를 통해 logic 함수가 실행될때마다 새로운 프로토타입 빈이 생성되는 것을 알 수 있다. 따라서 우리가 필요한 딱 DL 기능만 제공해준다.

 

 

7. 프로토타입 빈은 언제?

 프로토타입 빈은 언제 생성해서 사용해야 할까? 정답은 프로토타입 빈의 정의, 즉 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요할 경우 사용하면 된다. 하지만 이를 사용할 일은 극히 드물다

 

 참고로 ObjectProvider는 프로토타입 빈 뿐만 아니라, DL이 필요한 경우 언제든지 사용 가능하다.

 

 

8. 웹 스코프

 웹 스코프는 이름에서 알 수 있듯이, 웹 환경에서 동작한다. 또한 프로토타입과 다르게 스프링 컨테이너가 해당 스코프의 종료시점까지 관리한다. 즉, 소멸 메서드 또한 호출 가능하다.

 

웹 스코프의 종류를 다시 한번 정리해 보자.

- 웹 스코프 종류

  - request: HTTP 웹 요청이 들어올때 생성되며, 나갈때 까지 유지되는 스코프이다. 각각의 HTTP 요청마다 별도의 인스턴스가 생성된다.

  - session: 웹 세션이 생성되고 종료될때까지 유지되는 스코프이다. HTTP Session과 같은 생명주기를 지닌다.

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

 

이 중에서 request 스코프에 대해서 알아보자 (나머지는 웹 지식을 더 습득한 후 알아보자)

 

 

9. request 스코프

 request 스코프의 경우 각 클라이언트의 HTTP request 요청에 따라, 각각 request 스코프 빈이 생성된다. 하지만 여기서 프로토타입 스코프에서 겪었던 비슷한 문제가 발생한다.

 

 Controller는 싱글톤 빈이므로 스프링 컨테이너가 생성될때 빈이 생성되고 의존관계 주입을 받는데, 이때 request 스코프는 아직 생성되지 않는다. 따라서 의존관계 주입이 불가능하여, 애플리케이션 실행 시 오류가 발생한다.

Error creating bean with name 'myLogger': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton;

 

 

10. request 스코프와 Provider

 첫 번째 해결방법은 6번에서 소개한 ObjectProvider를 활용하는 것이다.

@Controller
@RequiredArgsConstructor
public class LogDemoController {
    //생략
    private final ObjectProvider<MyLogger> myLoggerProvider;
    
    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
    	MyLogger myLogger = myLoggerProvider.getObject();
        //생략
    }
}

 

 

 

11. request 스코프와 프록시

두번째 방법은 프록시 방식을 사용하는 것이다.

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {}

request 스코프에 proxyMode = ScopedProxyMode.TARGET_CLASS 를 추가해 주면 된다.

 

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

- 적용 대상이 클래스면 TARGET_CLASS

 

이 설정만 해주면 Controller에서 별다른 세팅 없이 애플리케이션을 실행해도, 정상적으로 작동된다.

 

어떻게 된건지 확인하기 위해 주입된 myLogger를 확인해보면, 다음과 같다.

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

//출력 결과
myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d

 

 CGLIB 라이브러리가 내 클래스를 상속받은 가짜 프록시 객체를 만들어서 미리 controller에 주입되는 것이다! 이는 @ComponentScan시 싱글톤을 유지하기 위해 CGLIB 라이브러리가 한 행동과 유사하다.

(@ComponentScan과 싱글톤)

 

 

 즉, 미리 가짜 프록시 객체를 스프링 컨테이너에 등록해 두고, 실제 HTTP 요청이 오면 그때 내부에서 진짜 빈을 요청하는 로직을 실행한다.

 

 클라이언트가 myLogger.logic() 호출 -> 사실 가짜 프록시 객체의 메서드를 호출 -> 가짜 프록시 객체가 진짜 myLogger.logic()을 호출

 

 하지만 다형성에 의해, 클라이언트에 입장에서는 원본 클래스와 동일하게 사용할 수 있다.

 

 참고로 꼭 웹 스코프가 아니어도 프록시를 사용할 수 있다. 이렇게 프록시를 적용하면 마치 싱글톤을 사용하는 것 같아 편하지만, 주의해서 사용해야 한다. 무분별하게 사용하면 유지보수하기 어려워진다...

 

 


위 내용은 김영한 님의 인프런 강의 "스프링 핵심 원리 - 기본편"의 내용과 강의자료를 토대로 작성된 게시글입니다.

강의 링크:

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

댓글