개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › Spring › IoC 컨테이너와 DI

빈 스코프

프로토타입 빈인데 왜 객체가 하나뿐인가

스코프는 컨테이너가 빈 객체를 몇 개 만들고 언제까지 쓰는지를 정합니다. 싱글톤은 컨테이너에 하나, 프로토타입은 컨테이너에 달라고 할 때마다 새로 하나, request는 HTTP 요청마다 하나입니다. 그런데 싱글톤 빈이 다른 스코프의 빈을 주입받으면 기대와 다른 일이 생깁니다.

실행으로 대조 · 스프링 부트 4.1.1 서버를 띄워 아홉 가지 조합에 HTTP 요청을 보내 대조

이 실습에 나오는 개념

빈 스코프
빈 인스턴스가 만들어지고 유지되는 범위다. 기본값인 singleton은 컨테이너에 하나만 만들어 모두가 공유한다. prototype은 요청할 때마다 새로 만든다. 웹 환경에는 HTTP 요청마다 만드는 request, 세션마다 만드는 session 스코프가 있다.
스프링 빈(Bean)
스프링 IoC 컨테이너가 생성하고 관리하는 객체다. 개발자가 new로 직접 만든 객체와 달리 컨테이너가 의존성을 주입하고 초기화와 소멸을 관리하며, 프록시를 통해 트랜잭션이나 AOP 같은 부가 기능을 적용할 수 있다.
ObjectProvider
빈 객체를 바로 주입받지 않고, 필요한 시점에 컨테이너에서 꺼낼 수 있게 해 주는 스프링의 도구다. 생성자나 필드의 타입을 ObjectProvider<Cart>처럼 적으면 컨테이너가 넣어 주고, getObject()를 부를 때마다 컨테이너에 그 타입의 빈을 달라고 요청한다. 그래서 프로토타입 빈은 부를 때마다 새 객체가, request 스코프 빈은 지금 처리 중인 요청의 객체가 나온다. 싱글톤 빈이 자기보다 수명이 짧은 빈을 쓸 때 주로 쓴다. 한 번의 작업 안에서 같은 객체를 계속 써야 하면 한 번만 꺼내 지역 변수에 담아 둔다.
스코프 프록시
싱글톤이 아닌 스코프의 빈을 다른 빈에 주입할 때, 진짜 객체 대신 넣어 주는 대리 객체다. @Scope에 proxyMode = ScopedProxyMode.TARGET_CLASS를 적으면 컨테이너가 그 클래스를 상속한 프록시를 만들어 주입한다. 프록시는 메서드가 불릴 때마다 컨테이너에서 그 스코프에 맞는 진짜 객체를 찾아 호출을 넘긴다. request 스코프와 함께 쓰면 싱글톤 빈의 코드를 바꾸지 않고도 요청마다 다른 객체를 쓰게 되며, @RequestScope에는 이 설정이 들어 있다. 프로토타입 스코프와 함께 쓰면 메서드를 부를 때마다 새 객체가 만들어지므로 주의한다.
프록시(Proxy)
실제 객체를 대신해 호출을 받는 대리 객체다. 호출을 받으면 부가 작업을 수행한 뒤 실제 객체에 전달한다. 스프링은 AOP와 @Transactional 같은 기능을 적용할 빈을 프록시로 감싸 컨테이너에 등록하므로, 다른 빈이 주입받는 것은 실제 객체가 아니라 프록시다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. 컨테이너 시작: orderController, orderService, cart 세 빈을 등록합니다. orderController와 orderService는 싱글톤이고, cart의 스코프는 프로토타입입니다.
  2. orderService 빈 만들기: 생성자가 Cart를 요구해 컨테이너에 Cart를 달라고 합니다. 프로토타입 빈은 달라고 할 때마다 새로 만들므로, 컨테이너가 Cart 1번을 새로 만들어 넘깁니다. OrderService는 이 객체를 필드에 넣어 둡니다.
  3. 기동이 끝났습니다. 지금까지 만들어진 Cart 객체는 1개입니다.
  4. 요청 1: GET /order가 들어옵니다. OrderController의 order()가 orderService.cartNo()를 두 번 부릅니다.
  5. cartNo() 1번째 호출: 필드에 들어 있는 Cart 1번의 no()를 부릅니다. 주입은 OrderService를 만들 때 한 번뿐이었으므로, 프로토타입이어도 새 객체가 만들어지지 않습니다.
  6. cartNo() 2번째 호출: 필드에 들어 있는 Cart 1번의 no()를 부릅니다. 주입은 OrderService를 만들 때 한 번뿐이었으므로, 프로토타입이어도 새 객체가 만들어지지 않습니다.
  7. 요청 1의 응답은 "1,1"입니다.
  8. 요청 2: GET /order가 들어옵니다. OrderController의 order()가 orderService.cartNo()를 두 번 부릅니다.
  9. cartNo() 1번째 호출: 필드에 들어 있는 Cart 1번의 no()를 부릅니다. 주입은 OrderService를 만들 때 한 번뿐이었으므로, 프로토타입이어도 새 객체가 만들어지지 않습니다.
  10. cartNo() 2번째 호출: 필드에 들어 있는 Cart 1번의 no()를 부릅니다. 주입은 OrderService를 만들 때 한 번뿐이었으므로, 프로토타입이어도 새 객체가 만들어지지 않습니다.
  11. 요청 2의 응답은 "1,1"입니다.

실행 결과

코드

@Component
@Scope("prototype")
class Cart {
    private static int created = 0;   // 지금까지 만들어진 Cart 객체 수
    private final int no;             // 몇 번째로 만들어진 객체인가

    Cart() {
        created++;
        no = created;
    }

    public int no() {
        return no;
    }
}

@Component
class OrderService {                  // 스코프를 적지 않았으므로 싱글톤
    private final Cart cart;

    OrderService(Cart cart) {         // 주입은 OrderService가 만들어질 때 한 번뿐
        this.cart = cart;
    }

    int cartNo() {
        return cart.no();
    }
}

@RestController
class OrderController {
    private final OrderService orderService;

    OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @GetMapping("/order")
    String order() {
        int first = orderService.cartNo();
        int second = orderService.cartNo();   // 한 요청 안에서 두 번 쓴다
        return first + "," + second;
    }
}

이 모형의 전제

아홉 가지 조합을 이 PC의 스프링 부트 4.1.1(스프링 프레임워크 7.0.9, 내장 톰캣 11)에서 실행해 대조했습니다. 조합마다 화면과 같은 구조의 소스를 컴파일해 서버를 띄우고, 실제로 GET /order 요청을 두 번 보내 응답에 적힌 Cart 번호를 읽었습니다. Cart의 생성자가 불린 횟수(기동 직후, 요청 두 번 뒤), OrderService의 필드에 들어 있는 것(Cart, ObjectProvider, 프록시), 기동 실패의 예외 사슬(UnsatisfiedDependencyException → UnsatisfiedDependencyException → ScopeNotActiveException → IllegalStateException)을 비교했습니다. 요청 두 개는 하나가 끝난 뒤 다음 것을 보냈습니다(동시에 보낸 경우는 "싱글톤 빈과 상태" 실습에서 다룹니다). 다음도 실행으로 확인했습니다: 프로토타입 빈을 주입받는 싱글톤 빈이 둘이면 서로 다른 객체를 받는 것, 프로토타입에 프록시를 쓰면 add로 넣은 값이 바로 다음 size 호출에서 보이지 않는 것(0이 나옴), @RequestScope를 붙인 빈은 그대로 주입받아도 기동되고 요청마다 하나인 것, request 스코프 빈의 @PreDestroy가 요청이 끝날 때마다 불리는 것. 다루지 않은 것: session 스코프와 application 스코프, 프로토타입 빈을 꺼내는 다른 방법(@Lookup, jakarta.inject.Provider), 프록시 방식 가운데 인터페이스를 쓰는 것(ScopedProxyMode.INTERFACES). 화면의 Cart는 객체를 세려고 static 변수를 썼는데, 요청이 동시에 들어오는 실제 코드라면 이렇게 세면 안 됩니다.

앞 실습: 싱글톤 빈과 상태 · 다음 실습: 빈 생명주기 콜백

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요