김영한의 스프링 핵심 원리(기본편) - 스프링 빈의 스코프와 Provider 그리고 Proxy

2025. 2. 2. 15:59·김영한의 스프링 핵심 원리(기본편)

Question

  • 스프링 빈의 3가지 스코프에 대해 설명하시오
  • 프로토타입 빈이 싱글톤 빈과 뭐가 다른지 설명하시오
  • 프로토타입 빈을 싱글톤 빈과 같이 사용했을 때 발생하는 문제점과 해결 방안을 설명하시오
  • request scope에 대해 설명하시오
  • request scope를 사용했을 때 발생할 수 있는 문제점을 설명하시오
  • requset scope의 시점 문제를 해결하는 Proxy의 동작 방식을 설명하시오

 

 

스프링 빈의 스코프에 대해 설명하시오

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

 

프로토타입 빈

  • 매 객체 요청마다 싱글톤 처럼 같은 객체를 받는게 아니라 계속 새로운 객체를 받음
  • 스프링 컨테이너는 프로토타입 빈을 생성, 의존관계 주입, 초기화 까지만 처리하고 그 뒤에 반환이나 관리는 하지 않음
  • 따라서, 이에 대한 관리는 클라이언트가 직접 해줘야 하며 @PreDestory같은 어노테이션은 동작하지 않음
  • 사실상 실무에서 거의 싱글톤 빈만 사용하지 프로토 타입은 별로 사용할 일은 없음

 

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

  • 위 그림에서 clientBean은 싱글톤 빈이고 싱글톤 빈 안에 PrototypeBean을 생성해서 사용중임.
  • 프로토타입 빈을 사용하는 이유는 매 요청때마다 새로운 프로로타입을 할당받기 위함임
  • 하지만, 싱글톤 빈은 처음부터 끝까지 계속 같은 객체고, 싱글톤 빈안에 생성된 프로토타입 빈 또한 한 번 할당된 이후에는 변할 수 없음.
  • 따라서, 클라이언트A가 addCount() 로직을 호출해서 프로토타입 빈의 count 값을 1 올린 후, 클라이언트B가 같은 로직을 호출 시, 새로운 프로토타입에서 count를 1 올린게 아닌 클라이언트A가 올린 값에서 1을 더 올리게 됨.
  • 이는, 프로토타입을 생성한 의도와 달라지는 문제가 발생함.

 

프로토타입 빈을 싱글톤 빈과 함께 사용시 DL을 통해 해결

@Scope("prototype")
@Component
static class PrototypeBean {
    private int count = 0;

    public void addCount() {
        count++;
    }

    public int getCount() {
        return count;
    }
    
    ...
}


static class ClientBean {
	@Autowired
	private ApplicationContext ac;
	
    public int logic() {
		PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
		prototypeBean.addCount();
		int count = prototypeBean.getCount();
		return count;
	}
 }
  • 싱글톤 빈에서 프로토타입 빈을 생성할 때 객체 주입으로 받지 않고 직접 필요한 의존관계를 찾아서 매번 넣어 줌.
  • 이 방식을 Dependency Lookup(DL) 의존관계 탐색이라고 함.
  • 하지만, 이 방법은 스프링 컨테이너에 종속적인 코드가 되며 단위 테스트에 어려움이 생김.
  • 스프링 컨테이너의 종속성은 없애고 DL만 사용하는 방법이 필요

 

ObjectFactory, ObjectProvider

public class SomeClass {

    @Autowired
    private ObjectProvider<PrototypeBean> prototypeBeanProvider;

    public int logic() {
        PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
        prototypeBean.addCount();
        int count = prototypeBean.getCount();
        return count;
    }
}
  • ObjectProvider는 DL 서비스를 제공함.
  • 과거에는 ObjectFactory 였는데 여기에 편의 기능이 추가되어 ObjectProvider가 만들어짐
  • 단위테스트 하기 훨씬 쉬워짐.

 

JSR-330 Provider

@Autowired
private Provider<PrototypeBean> provider;

public int logic() {
    PrototypeBean prototypeBean = provider.get();
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}
  • javax.inject.Provider라는 JSR-330 자바 표준
  • 스프링이 아니라 자바 표준
  • DL 기능을 제공

 

웹 스코프

  • 웹 스코프는 웹 환경에서만 동작
  • 프로토타입과 다르게 스프링이 해당 스코프의 종료 시점까지 관리해주기 때문에 종료 메서드가 호출됨
  • 웹스코프 종류
    • request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴 스가 생성되고, 관리된다.
    • session: HTTP Session과 동일한 생명주기를 가지는 스코프
    • application: 서블릿 컨텍스트( ServletContext )와 동일한 생명주기를 가지는 스코프
    • websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프
  • 위 그림은 request 스코프 예제이며, 요청이 들어올 때마다 별도의 requset scope의 Bean이 만들어지며, 같은 Requset 내에서 Controller가 사용하는 Bean과 Service가 사용하는 Bean은 같음.
  • 반면에, 다른 Request에서 사용하는 Bean은 달라짐.

 

Request Scope 예제

@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);
    }
}
  • 로그를 출력하기 위한 MyLogger
  • 각 리퀘스트마다 다른 UUID를 별도로 생성해서 출력하고자 함
  • @Scope(value = "request")를 사용해 requset 스코프 지정
  • 이 빈은 HTTP 요청 당 하나씩 생성되고 HTTP 요청이 끝나는 시점에 소멸 됨
  • requestURL은 빈이 생성되는 시점에는 알 수 없으므로 Setter로 입력 받음
  • 스프링이 관리하므로 @PreDestory 메서드 사용 가능
  • 하지만 위의 MyLogger를 그냥 빈으로 등록해서 사용하면 아래와 같은 에러가 발생함
    • 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;
    • 왜냐 하면, request scope 빈은 실제로 요청이 들어와야 Bean이 생성이 되는데, Controller나 Service 는 일반적으로 싱글톤 빈으로 생성이 되어 있고, 싱글톤 빈은 애플리케이션이 실행될 때 생성이 되는데 이 때 생성자 주입으로 request scope 빈을 넣으려고 하면 해당 빈은 아직 생성이 되어있지 않기 때문

 

Provider를 통한 Requset Scope 빈의 생성 시점 문제 해결

private final ObjectProvider<MyLogger> myLoggerProvider;
MyLogger myLogger = myLoggerProvider.getObject();
  • 위 코드를 사용하면, 에러 없이 동작이 가능함
  • 그 이유는 Provider는 getObject() 함수를 호출 할 때, 실제로 해당 빈이 필요해지면 빈을 생성하고 주입함.
  • 이 방식은 지연 주입 방식이며 빈이 실제로 사용되기 전까지는 생성되지 않음
  • 따라서, 이전의 프로토타입 빈의 방식에서도 프로토타입은 미리 생성이 되어 있지 않아도 요청이 올때 딱 생성해서 줌.

 

프록시를 통한 Requset Scope 빈의 생성 시점 문제 해결

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
  • proxyMode = ScopedProxyMode.TARGET_CLASS 를 추가
    • 적용 대상이 인터페이스가 아닌 클래스면 TARGET_CLASS 를 선택
    • 적용 대상이 인터페이스면 INTERFACES 를 선택
  • 이 방법을 사용하면 이전의 ObjectProvider 같은 복잡한 내용 없이 기존의 내용처럼 사용 가능
  • 이 방법은 CGLIB이라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입해줌
  • 가짜 프록시는 요청이 오면 그 때 내부에서 진짜 빈을 요청하는 위임 로직이 들어가 있음.
    • 클라이언트들은 myLogger.log를 호출하지만 실제로는 가짜 프록시 메서드를 호출
    • 가짜 프록시 객체는 진짜 myLogger.log를 호출
    • 클라이언트 입장에서는 진짜든 가짜든 상관이 없지만 원하는 결과를 얻을 수 있음.(다형성)
  • 동작 정리
    • CGLIB이라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입
    • 가짜 프록시 객체는 실제 요청이 오면 내부에서 실제 빈을 요청하는 위임 로직이 있음
    • 가짜 프록시는 실제 request scope과 관련이 전혀 없고 진짜 mock이고 내부에 단순 위임 로직만 있고 싱글톤임
  • 특징 정리
    • 프록시 객체 때문에 마치 싱글톤 빈 처럼 requset scope 빈을 사용 가능
    • Provider든 Proxy든 결국 핵심 아이디어는 객체 조회를 필요한 시점까지 지연처리 한다는 것
    • 어노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있으며, 이게 바로 다형성과 DI의 강점
    • 웹 스코프가 아니라도 프록시는 사용 가능함
  • 주의 점
    • 싱글톤과 비슷해 보이지만 실제론 다르게 동작하므로 주의해야 함
    • requset scope와 같은 이러한 스코프는 꼭 필요한 곳에서만 최소화해서 사용해야지, 무분별하게 사용하면 유지보수성이 어려워짐.

 

'김영한의 스프링 핵심 원리(기본편)' 카테고리의 다른 글

김영한의 스프링 핵심 원리(기본편) - 총 정리  (0) 2026.01.19
김영한의 스프링 핵심 원리(기본편) - 스프링 빈의 라이프사이클과 생명주기 콜백(@PostConstruct, @PreDestory)  (2) 2025.02.02
김영한의 스프링 핵심 원리(기본편) - Bean 주입 방식과 Bean 중복 조회 문제  (2) 2025.02.02
김영한의 스프링 핵심 원리(기본편) - @ComponentScan과 @Autowired  (2) 2025.01.31
김영한의 스프링 핵심 원리(기본편) - 스프링의 싱글톤 패턴과 @Configuration  (1) 2025.01.31
'김영한의 스프링 핵심 원리(기본편)' 카테고리의 다른 글
  • 김영한의 스프링 핵심 원리(기본편) - 총 정리
  • 김영한의 스프링 핵심 원리(기본편) - 스프링 빈의 라이프사이클과 생명주기 콜백(@PostConstruct, @PreDestory)
  • 김영한의 스프링 핵심 원리(기본편) - Bean 주입 방식과 Bean 중복 조회 문제
  • 김영한의 스프링 핵심 원리(기본편) - @ComponentScan과 @Autowired
5jyan5
5jyan5
  • 5jyan5
    jyan
    5jyan5
  • 전체
    오늘
    어제
    • 분류 전체보기 (242)
      • 김영한의 스프링 핵심 원리(기본편) (8)
      • 김영한의 스프링 핵심 원리 - 고급편 (11)
      • 김영한의 스프링 MVC 1편 (1)
      • 김영한의 스프링 DB 1편 (3)
      • 김영한의 스프링 MVC 2편 (3)
      • 김영한의 ORM 표준 JPA 프로그래밍(기본편) (9)
      • 김영한의 스프링 부트와 JPA 활용2 (2)
      • 김영한의 실전 자바 - 중급 1편 (1)
      • 김영한의 실전 자바 - 고급 1편 (9)
      • 김영한의 실전 자바 - 고급 2편 (9)
      • Readable Code: 읽기 좋은 코드를 작성.. (2)
      • 김영한의 실전 자바 - 고급 3편 (9)
      • CKA (118)
      • 개발 (37)
      • 경제 (4)
      • 리뷰 (1)
      • 정보 (2)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      log trace
      @args
      WAS
      단방향 맵핑
      조회 성능 최적화
      양방향 맵핑
      자바
      빈 후처리기
      hibernate5module
      고급
      gesingleresult
      requset scope
      reentarantlock
      @within
      락
      Target
      jpq
      JPQL
      페치 조인
      @discriminatorvalue
      jdk 동적 프록시
      프록시 팩토리
      @discriminatorcolumn
      버퍼
      Thread
      김영한
      프록시
      typequery
      스레드
      cglib
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.2
    5jyan5
    김영한의 스프링 핵심 원리(기본편) - 스프링 빈의 스코프와 Provider 그리고 Proxy
    상단으로

    티스토리툴바