티스토리 뷰

Spring

[Spring] 빈 스코프

최MAX 2022. 4. 3. 23:34

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

 

스프링의 다양한 스코프

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

 

지금까지 싱글톤 스코프만 봤으니 프로토타입, 웹 스코프를 보자.

 


 

프로토타입  스코프

 

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

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

 

 

  1. 프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.
  2. 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.
  3. 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환한다. (스프링 컨테이너에서 더이상 관리않음)
  4. 이후에 스프링 컨테이너에서 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환한다.

핵심은 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다는 것이다. 프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있다. 그래서 '@PreDestory' 같은 종료 메서드가 호출되지 않는다.

 

SingletonBean 테스트

@Test
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);
    Assertions.assertThat(singletonBean1).isSameAs(singletonBean2);
    ac.close();
}

 

실행 결과

@363a52f로 같은 객체 반환

 

PrototypeBean 테스트

@Test
void prototypeBeanFind() {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
    System.out.println("find prototypeBeanFind1");
    PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
    System.out.println("find prototypeBeanFind2");
    PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
    System.out.println("prototypeBean1 = " + prototypeBean1);
    System.out.println("prototypeBean2 = " + prototypeBean2);
    Assertions.assertThat(prototypeBean1).isNotSameAs(prototypeBean2);

    ac.close();
}

실행 결과

서로 다른 객체, 또한 destroy()도 실행되지 않는다!

 


프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점

클라이언트 A가 clienBean의 logic(count 값을 1 증가시킴)을 호출하고 이어서 클라이언트 B가 logic()을 호출하는 상황

  • 클라이언트 B는 clientBean을 스프링 컨테이너에 요청해서 받는다. 싱글톤이므로 항상 같은 clientBean이 반호나된다.
  • clientBean이 내부에 가지고있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다. (클라이언트 A의 호출로 인해서) 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성 된 것이지, 사용할 때마다 새로 생성되는 것이 아니다.
  • 클라이언트 B가 clientBean.logic()을 호출하면, 이미 클라이언트 A의 logic()호출로 인해서 count값이 1이었으므로 count 값이 2가 된다.

 

SingletonWithPrototypeTest 코드

@Test
void singletonClientUserPrototype() {
    AnnotationConfigApplicationContext ac =
            new AnnotationConfigApplicationContext(ClientBean.class , PrototypeBean.class);
    ClientBean clientBean1 = ac.getBean(ClientBean.class);
    int count1 = clientBean1.logic();
    System.out.println("count1: " + count1);
    assertThat(count1).isEqualTo(1);

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

테스트 통과 후 실행 결과

count값이 증가한다.

 

프로토타입 빈을 주입 시점에만 새로 생성하는 것이 아니라, 사용할 때 마다 새로 생성해서 사용하려면,


프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결

 

ObjectProvider<> 사용

@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의 특징

  • 의존관계를 외부에서 주입(DI)받는게 아니라 이처럼 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL) 의존관계 조회(탐색)이라 한다.
  • ObjectProvider의 getObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.
  • ObjectProvider는 지금 딱 필요한 DL 정도의 기능만 제공한다.
  • 스프링에 의존한다.
JSR330 Provider도 존재한다. 사용하기 위해서 implementation 'javax.inject:javax.inject:1'를 build.gradl에 추가

 

정리

  • 프로토타입 빈의 사용 시점: 매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요할 때 사용.
  • ObjectProvider, JSR330 Provider 등은 프로토타입 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있다.

웹 스코프

 

  • 웹 환경에서만 동작한다.
  • 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다. 따라서 종료 메서드가 호출된다.

 

웹 스코프 종류

  • request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프. 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.
  • session: HTTP Session과 동일한 생명주기를 가지는  스코프
  • application: ServletContext와 동일한 생명주기를 가지는 스코프
  • websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프

 

클라이언트 A와 B의 HTTP request에 따라서 A 전용 / B 전용 빈이 생성돼서 운영이 되다가 응답이 나갈 때 해당 빈이 소멸된다.

 


request 스코프 예제 만들기

 

우선 web 환경 라이브러리 추가

implementation 'org.springframework.boot:spring-boot-starter-web'

 

  • 동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다. 이럴때 사용하기 좋은 것이 request 스코프이다.
  • 기대하는 공통 포맷: [UUID][requestURL] {message}

 

LogDemoController

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        myLogger.setRequestURL(requestURL);

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

 

  • HttpServletRequest를 통해서 요청 URL을 받는다.
    • requestURL 값: http://localhost:8080/log-demo
  • 위에서 받은 requestURL 값을 myLogger에 저장해둔다.
  • 컨틀로러에서 controller test라는 로그를 남긴다.

LogDemoService

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;

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

 

실행 결과

스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 웹에서 실제 고객의 요청이 와야 생성할 수 있다. 즉, 웹에서 아무 요청도 없었는데 요청을 가져오려고 하니까 에러가 발생함.

 


스코프와 Provider

첫번째 해결방안: Provider (ObjectProvider) 사용

 

 

myLogger를 다음과 같이 수정

private final ObjectProvider<MyLogger> myLoggerProvider;

이후 객체를 생성할 때 getObject()를 호출

MyLogger myLogger = myLoggerProvider.getObject();

 

브라우저에 localhost:8080/log-demo 입력 시

 

화면에 OK 메시지 출력

실행 결과

 

  • ObjectProvider 덕분에 ObjectProvider.getObject()를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다.
  • ObjectProvider.getObject()를 호출하는 시점에는 HTTP 요청이 진행중이므로 request scope 빈의 생성이 정상 처리된다.
  • ObjectProvider.getObject()를 LogDemoController, LogDemoService에서 각각 한번씩 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환된다.

 

그냥 private final MyLogger myLogger; 만 에러 없이 사용할 수 있는 방법은 없을까?

 


스코프와 프록시

@Component
@Scope(value="request")
public class MyLogger {

MyLogger 클래스의 @Scope를 변경

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

 

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

  • @Scope의 'proxyMode = ScopedProxyMode.TARGET_CLASS를 설정하면 스프링 컨테이너는 CGLIB라는 바이트 코드를 조작하는 라이브러리를 사용해서, MyLogger를 상속받은 가짜 프록시 객체를 생성한다.
  • 그리고 스프링 컨테이너에 "myLogger"라는 이름으로 진짜 대신에 이 가짜 프록시 객체를 등록한다.
  • ac.getBean("myLogger", MyLogger.class) 로 조회해도 프록시 객체가 조회되는 것을 확인할 수 있다
  • 그래서 의존관계 주입도 이 가짜 프록시 객체가 주입된다.

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

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

동작 정리

  • CGLIB라는 라이브러리로 내 클래스를 상속받은 가짜 프록시 객체를 만들어서 주입한다.
  • 이 가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다.
  • 가짜 프록시 객체는 실제 request scope와는 관계가 없다. 그냥 가짜이고 내부에 단순한 위임 로직만 있고 싱글톤 처럼 동작한다.
꼭 웹 스코프가 아니어도 프록시는 사용할 수 있다.

 

[출처] 스프링 핵심 원리 - 기본편, 김영한님

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함