-
Bean ScopeJava/Spring 2025. 1. 12. 23:39
스프링의 빈 스코프에 대해 정리하였습니다.
< 빈 스코프란? >
빈 스코프란 빈이 존재할 수 있는 범위를 의미합니다.
지금까지 살펴본 빈은 기본적으로 "싱글톤" 스코프를 가지며, 이는 빈이 스프링 컨테이너와 함께 생성되어 스프링 컨테이너가 종료될 때까지 유지됨을 의미합니다.스프링은 다양한 스코프를 지원합니다.
- 싱글톤(Singleton) : 기본 스코프로, 스프링 컨테이너의 시작부터 종료까지 빈이 유지되는 가장 넓은 범위의 스코프입니다.
- 프로토타입(Prototype) : 스프링 컨테이너가 빈의 생성과 의존성 주입까지만 관여하며, 그 이후로는 관리하지 않는 짧은 범위의 스코프입니다.
- 웹(Web) 관련 스코프 :
- request : 웹 요청이 시작될 때 생성되고, 요청이 완료되면 소멸되는 스코프입니다.
- session : 웹 세션이 생성될 때 시작되고, 세션이 종료될 때까지 유지되는 스코프입니다.
- application : 웹의 서블릿 컨텍스트 범위와 동일하게 유지되는 스코프입니다.
Ex) 빈 스코프 지정 예시
1. 컴포넌트 스캔을 이용한 자동 등록
@Scope("prototype") @Component public class HelloBean {}
2. 수동 등록
@Scope("prototype") @Bean PrototypeBean HelloBean() { return new HelloBean(); }
< 프로토타입 스코프 >
싱글톤 빈의 경우, 스프링 컨테이너는 동일한 빈을 요청할 때마다 새로운 빈을 생성하지 않고 하나의 빈만 유지하며 반환합니다. 반면, 프로토타입 빈은 클라이언트가 요청할 때마다 새로운 빈을 생성하고 의존성을 주입합니다.
싱글톤타입 빈 요청 :
싱글톤 스코프 빈 요청 과정 : 스프링 컨테이너가 스프링 빈을 관리하며, 같은 요청이 와도 같은 객체 인스턴스를 클라이언트에게 반환해 줍니다.
프로토타입 빈 요청 :
프로토타입 스코프 빈 요청 과정 : 프로토타입 빈의 경우, 클라이언트가 요청할 때마다 새로운 빈이 생성되고 의존관계 주입이 이루어집니다. 생성된 빈은 클라이언트에게 전달되며, 이후 스프링 컨테이너는 해당 빈을 관리하지 않습니다. 이로 인해 빈의 관리 책임은 클라이언트에게 넘어갑니다.
따라서, @PreDestroy 같은 종료 메서드는 프로토타입 빈에 대해 호출되지 않습니다. 이는 빈이 생성 및 의존관계 주입 후 스프링 컨테이너에 남지 않기 때문입니다.
코드로 확인하는 싱글톤 타입과 프로토타입 빈의 비교
#1 싱글톤 스코프
public class SingletonTest { @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); assertThat(singletonBean1).isSameAs(singletonBean2); ac.close(); } @Scope("singleton") static class SingletonBean { @PostConstruct public void init() { System.out.println("SingletonBean.init"); } @PreDestroy public void close() { System.out.println("SingletonBean.close"); } } }
실행 결과 : 초기화 메서드를 실행한 후, 동일한 인스턴스의 빈을 조회하고, 종료 메서드가 정상적으로 호출되는 것을 확인할 수 있습니다.
#2 프로토타입 스코프
public class PrototypeTest { @Test void prototypeBeanFind() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class); System.out.println("find prototypeBean1"); PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class); System.out.println("find prototypeBean2"); PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class); System.out.println("prototypeBean1 = " + prototypeBean1); System.out.println("prototypeBean2 = " + prototypeBean2); assertThat(prototypeBean1).isNotSameAs(prototypeBean2); ac.close(); } @Scope("prototype") static class PrototypeBean { @PostConstruct public void init() { System.out.println("SingletonBean.init"); } @PreDestroy public void close() { System.out.println("SingletonBean.close"); } } }
실행 결과 : 프로토타입 빈을 두 번 조회했기 때문에 서로 다른 스프링 빈이 각각 생성되었으며, 초기화도 두 번 실행된 것을 확인할 수 있습니다.
프로토타입 빈은 스프링 컨테이너가 종료될 때 @PreDestroy와 같은 종료 메서드가 전혀 실행되지 않는 것 또한 확인할 수 있습니다.
[ 프로토타입 빈의 특징 정리 ]
- 스프링 컨테이너에 요청할 때마다 새로 생성됩니다.
- 스프링 컨테이너는 프로토타입 빈의 생성, 의존관계 주입, 초기화까지만 관여합니다.
- 종료 메서드가 호출되지 않습니다.
- 따라서, 프로토타입 빈은 조회한 클라이언트가 관리해야 합니다. 종료 메서드 호출도 클라이언트가 직접 수행해야 합니다.
< 프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점 >
프로토타입 스코프의 빈은 보통 요청할 때마다 새로운 인스턴스를 생성하여 사용하고자 할 때 사용됩니다. 하지만 싱글톤 빈과 함께 사용할 경우, 이러한 의도대로 작동하지 않아 예상치 못한 동작이 발생할 수 있으므로 주의가 필요합니다.
프로토타입의 빈을 직접 요청하는 경우 :
: 클라이언트 A가 스프링 컨테이너에 프로토타입 빈을 요청하면, 컨테이너는 새로운 프로토타입 빈 인스턴스(x01)를 생성하여 반환합니다. 이 인스턴스의 초기 count 필드 값은 0입니다. 클라이언트 A는 해당 빈의 addCount() 메서드를 호출하여 count 필드 값을 1 증가시킵니다. 결과적으로, 프로토타입 빈(x01)의 count 값은 1이 됩니다.
동일하게, 클라이언트 B가 스프링 컨테이너에 프로토타입 빈을 요청하면, 컨테이너는 또 다른 새로운 프로토타입 빈 인스턴스(x02)를 생성하여 반환합니다. 이 인스턴스의 초기 count 필드 값도 0입니다. 클라이언트 B는 이 빈의 addCount() 메서드를 호출하여 count 필드 값을 1 증가시킵니다. 따라서 프로토타입 빈(x02)의 count 값 역시 1이 됩니다.
싱글톤 빈 안에서 프로토타입 빈을 사용하는 경우 :
싱글톤타입의 clientBean이 내부 로직을 위해 프토토타입의 prototypeBean을 사용하는 예시입니다.
1. 싱글톤인 clientBean은 스프링 컨테이너가 생성될 때 함께 생성됩니다.
2. clientBean은 의존관계 자동 주입을 통해 스프링 컨테이너에 프로토타입 빈을 요청합니다.
3. 스프링 컨테이너는 새로운 프로토타입 빈을 생성해 반환하고, 이 빈의 count 필드 값은 0입니다.
4. clientBean은 반환된 프로토타입 빈의 참조값을 내부 필드에 보관합니다.
5. 클라이언트 A가 스프링 컨테이너에 clientBean을 요청하면, 싱글톤이므로 항상 동일한 clientBean이 반환됩니다.
6. 클라이언트 A는 반환된 clientBean의 logic() 메서드를 호출합니다.
7. clientBean은 내부 프로토타입 빈의 addCount()를 호출하여 count 값을 1 증가시킵니다.
8. 클라이언트 B가 스프링 컨테이너에 clientBean을 요청하면, 싱글톤이므로 동일한 clientBean이 반환됩니다.
(이때 주의할 점은 clientBean이 내부에 가지고 있는 프로토타입 빈이 이미 과거에 주입된 빈이라는 것입니다. 프로토타입 빈은 주입 시점에 새로 생성되었으며, 사용할 때마다 새로 생성되지 않습니다.)
10. 클라이언트 B가 clientBean의 logic() 메서드를 호출합니다.
11. clientBean은 내부의 프로토타입 빈의 addCount()를 호출하여 count 값을 증가시킵니다. 기존 count 값이 1이었기 때문에 새로운 count 값은 2가 됩니다.
→ 스프링은 주로 싱글톤 빈을 사용하며, 이로 인해 싱글톤 빈이 프로토타입 빈을 활용하는 경우가 종종 발생합니다. 그러나 싱글톤 빈은 생성 시점에만 의존관계 주입을 받습니다. 따라서 프로토타입 빈이 새로 생성되더라도, 이는 싱글톤 빈과 함께 계속 유지되며, 의도와 다르게 동작할 수 있습니다. 실제로는 프로토타입 빈이 주입 시점에만 생성되는 것이 아니라, 사용할 때마다 새로 생성되어 사용되기를 원할 것입니다.
< 프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider 사용 >
#1. 스프링 컨테이너에 요청
: 가장 간단한 방법은 싱글톤 빈이 프로토타입을 사용할 때마다 스프링 컨테이너에 새로 요청하는 것입니다.
@Autowired private ApplicationContext ac; public int logic() { PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class); prototypeBean.addCount(); int count = prototypeBean.getCount(); return count; }
- 이렇게 의존관계를 외부에서 주입(DI) 받는 게 아니라, 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL) 의존관계 조회(탐색)이라 합니다.
- 하지만, 위와 같이 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워집니다.
#2. ObjectFactory, ObjectProvider
: 스프링은 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스로 ObjectProvider를 제공합니다.
(과거에는 ObjectFactory가 있었는데, 여기에 옵션, 스트림 처리 등 편의기능을 추가하여 상속받은 것이 ObjectProvider입니다.)@Autowired private ObjectProvider<PrototypeBean> prototypeBeanProvider; public int logic() { PrototypeBean prototypeBean = prototypeBeanProvider.getObject(); prototypeBean.addCount(); int count = prototypeBean.getCount(); return count; }
- ObjectProvider의 getObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환합니다. (DL)
- 스프링이 제공하는 기능을 사용하지만, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워집니다.
특징
- ObjectFactory : 기능이 단순하고 별도의 라이브러리가 필요 없지만, 스프링에 의존적입니다.
- ObjectProvider : ObjectFactory를 상속받습니다. 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리가 필요 없지만, 스프링에 의존적입니다.
#3. JSR-330 Provider
: 마지막 방법은 javax.inject.Provider라는 JSR-330 자바 표준을 사용하는 방법입니다.
이 방법을 사용하려면 javax.inject:javax.inject:1 라이브러리를 gradle에 추가해야 합니다.
package javax.inject; public interface Provider<T> { T get(); }
//implementation 'javax.inject:javax.inject:1' gradle 추가 필수 @Autowired private Provider<PrototypeBean> prototypeBeanProvider; public int logic() { PrototypeBean prototypeBean = prototypeBeanProvider.get(); prototypeBean.addCount(); int count = prototypeBean.getCount(); return count; }
- provider의 get()을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환합니다. (DL)
- 자바 표준이고, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워집니다.
특징
- get() method 하나로 기능이 매우 단순합니다. (장점이자 단점이다)
- 별도의 라이브러리가 필요합니다.
- 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용 가능합니다.
정리
그렇다면 프로토타입 빈을 언제 사용할까요? 매번 사용할 때마다 의존관계 주입이 완료된 새로운 객체가 필요한 경우 사용하면 됩니다. 그러나, 실무에서는 싱글톤 빈으로 대부분의 문제를 해결할 수 있어 프로토타입 빈을 사용하는 경우가 드뭅니다.
ObjectProvider, JSR-330 Provider 등은 프로토타입뿐 아니라 DL이 필요한 경우 언제든지 사용할 수 있습니다.
<참고>
실무에서 자바 표준인 JSR-330 Provider를 사용할 것인지, 아니면 스프링이 제공하는 ObjectProvider를 사용할 것인지 고민이 될 것입니다.
ObjectProvider는 DL을 위한 편의 기능을 많이 제공해주고 스프링 외에 별도의 의존관계 추가가 필요 없기 때문에 ObjectProvider를 기본으로 사용하시면 됩니다. 만약(정말 그럴 일은 거의 없겠지만) 코드를 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야 한다면 JSR-330 Provider를 사용하면 됩니다.
스프링을 사용하다 보면 이 기능뿐만 아니라 다른 기능들도 자바 표준과 스프링이 제공하는 기능이 겹칠 때가 많이 있습니다.대부분 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에, 특별히 다른 컨테이너를 사용할 일이 없다면, 스프링이 제공하는 기능을 사용하면 됩니다.
< 웹 스코프 >
웹 스코프는 웹 환경에서만 동작하는 스코프입니다.
웹 스코프는 프로토타입과 다르게, 스프링이 해당 스코프의 종료시점까지 관리합니다. 따라서 종료 메서드가 호출됩니다.웹 스코프 종류
- request : HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프. 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리됩니다.
- session : HTTP Session과 동일한 생명주기를 갖는 스코프
- application : 서블릿 컨텍스트(Servlet Context)와 동일한 생명주기를 갖는 스코프
- websocket : 웹 소켓과 동일한 생명주기를 갖는 스코프
사실 세션, 서블릿 컨텍스트, 웹 소켓 같은 용어는 익숙하지 않은 분들도 있을 수 있습니다. 이 글에서는 request 스코프를 예제로 사용하여 설명합니다. 다른 개념들도 범위만 다를 뿐, 동작 방식은 유사합니다.
: 위 그림과 같이 request 웹 스코프는 클라이언트의 request 요청에 따라서 빈을 생성하고, 같은 request에 대해서는 해당 빈을 공유하는 방식으로 동작합니다.
< request 스코프 예제 만들기 >
request 스코프 예제를 만들어보며 동작 과정을 이해해 보겠습니다.
#1. 웹 환경 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
: build.gradle에 web 라이브러리 추가
: 'spring-boot-starter-web' 라이브러리를 추가하면, 스프링 부트는 내장 톰캣 서버를 활용해서 웹 서버와 스프링을 함께 실행시킵니다.
: 아직 어떠한 controller도 세팅하지 않았기 때문에 Whitelabel Error가 나옵니다.
< 참고 >
스프링 부트는 웹 라이브러리가 없으면, 저희가 지금까지 학습한 AnnotationConfigApplicationContext를 기반으로 애플리케이션을 구동합니다.
웹 라이브러리가 추가되면, 웹과 관련된 추가 설정과 환경들이 필요하므로 AnnotationConfigServletWebServerApplicationContext 를 기반으로 애플리케이션을 구동합니다.
#2. request 스코프 예제 개발
이제 예제 코드를 작성해 보겠습니다.
동시에 여러 HTTP 요청이 들어오면, 각 요청이 남긴 로그를 구분하기가 어려울 수 있습니다. 이럴 때, 사용하기 좋은 것이 바로 request 스코프입니다. request 스코프를 활용하여 각 요청에 대한 구분이 가능한 로그를 남기도록 추가 기능을 개발해 보겠습니다.
request를 이용한 로깅 - 기대하는 공통 포맷은 다음과 같습니다: `[UUID][requestURL] {message}`
- UUID를 사용하여 각각의 HTTP 요청을 구분하겠습니다. (UUID는 각 요청에 대해 고유하게 생성됩니다.)
- 또한, 로그에 requestURL 정보를 추가하여, 어떤 URL에 대한 요청으로 남겨진 로그인지 확인할 수 있도록 하겠습니다.
다음은 로그를 출력하기 위한 MyLogger 클래스입니다.
package hello.core.common; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import java.util.UUID; @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); } }
- @Scope(value = "request")를 사용하여, MyLogger 빈을 request 스코프로 지정합니다. 이제 이 빈은 각 HTTP 요청마다 하나씩 생성되며, HTTP 요청이 끝나는 시점에 소멸됩니다.
- 이 빈이 생성될 때, 자동으로 @PostConstruct 초기화 메서드를 통해 UUID를 생성하고 저장합니다. HTTP 요청마다 빈이 하나씩 생성되기 때문에, UUID를 저장해두면 다른 HTTP 요청과 구분할 수 있습니다.
- 빈이 소멸될 때는 @PreDestroy를 사용하여 종료 메시지를 남깁니다.
- requestURL은 이 빈이 생성될 때 알 수 없으므로, 외부에서 setter를 통해 입력받습니다.
다음은 로거가 잘 작동하는지 확인하는 테스트용 컨트롤러입니다.
package hello.core.web; import hello.core.common.MyLogger; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; @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에 저장합니다.
- myLogger는 각 HTTP 요청마다 개별적으로 관리되므로, 다른 HTTP 요청과 값이 섞일 걱정은 없습니다.
- 컨트롤러에서는 "controller test"라는 로그를 남깁니다.
< 참고 >
requestURL을 MyLogger에 저장하는 작업은 컨트롤러보다는 스프링 인터셉터나 서블릿 필터 같은 공통 처리가 가능한 곳을 활용하는 것이 좋습니다. 이 예제에서는 단순화를 위해, 그리고 스프링 인터셉터를 아직 학습하지 않은 분들을 위해 컨트롤러를 사용했습니다. 스프링 웹에 익숙하다면, 인터셉터를 사용하여 구현해보세요.마지막으로 서비스 계층에 대항하는 테스트 코드입니다.
package hello.core.web; import hello.core.common.MyLogger; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Required; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.ResponseBody; @Service @RequiredArgsConstructor public class LogDemoService { private final MyLogger myLogger; public void logic(String id) { myLogger.log("service id = " + id); } }
이제 실행해 보겠습니다.
기대하는 출력 :
[d06b992f...] request scope bean create [d06b992f...][http://localhost:8080/log-demo] controller test [d06b992f...][http://localhost:8080/log-demo] service id = testId [d06b992f...] request scope bean close
실제는 기대와 다르게 애플리케이션 실행 시점에 오류가 발생합니다.
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 스코프 빈은 요청이 들어올 때 생성되므로 초기에는 생성되지 않기 때문입니다. 따라서, 이 빈은 실제 고객의 요청이 발생해야만 생성할 수 있습니다.
이 문제를 해결하기 위해 이전에 학습한 Provider를 활용할 수 있습니다.
< 스코프와 Provider >
첫 번째 해결 방안은 직전에 언급한 Provider를 사용하는 방법입니다. (ObjectProvider를 적용해서 해결해봅시다.)
Controller와 Service에 ObjectProvider를 이용해서 나중에 의존관계가 주입될 수 있도록 변경해 줍니다.
package hello.core.web; @Controller @RequiredArgsConstructor public class LogDemoController { private final LogDemoService logDemoService; private final ObjectProvider<MyLogger> myLoggerProvider; @RequestMapping("log-demo") @ResponseBody public String logDemo(HttpServletRequest request) { MyLogger myLogger = myLoggerProvider.getObject(); String requestURI = request.getRequestURI(); myLogger.setRequestURL(requestURI); myLogger.log("controller test"); logDemoService.logic("testId"); return "OK"; } }
: Controller
package hello.core.web; @Service @RequiredArgsConstructor public class LogDemoService { private final ObjectProvider<MyLogger> myLoggerProvider; public void logic(String id) { MyLogger myLogger = myLoggerProvider.getObject(); myLogger.log("service id = " + id); } }
: Service
이제 main() 메서드로 스프링을 실행하고, 웹 브라우저에 http://localhost:8080/log-demo를 입력해보겠습니다.
: 잘 작동하는 것을 확인할 수 있습니다.
- ObjectProvider를 사용하면 ObjectProvider.getObject()를 호출하는 시점까지 request 스코프 빈의 생성을 지연할 수 있습니다.
- ObjectProvider.getObject()를 호출하는 시점에는 HTTP 요청이 진행 중이므로 request 스코프 빈이 정상적으로 생성됩니다.
- LogDemoController와 LogDemoService에서 ObjectProvider.getObject()를 각각 한 번씩 호출하더라도, 같은 HTTP 요청에서는 동일한 스프링 빈이 반환됩니다.
< 스코프와Proxy >
두 번째 해결 방안은 프록시 방식을 사용하는 것입니다.
MyLogger에 붙인 Scope어노테이션을 다음과 같이 수정합니다.
@Component @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) public class MyLogger { }
- 여기가 핵심입니다. Scope어노테이션에 proxyMode = ScopedProxyMode.TARGET_CLASS를 추가합니다.
- 적용 대상이 인터페이스가 아닌 클래스면 위와 같이 TARGET_CLASS를 선택
- 적용 대상이 인터페이스인 경우 INTERFACES를 선택
- 이렇게 하면, MyLogger의 가짜 프록시 클래스를 생성하여 HTTP 요청과 상관없이 이 프록시 객체를 다른 빈에 미리 주입할 수 있습니다.
잘 동작하는지 확인하기 위해 나머지 코드를 Provider 사용 이전으로 롤백하겠습니다.
package hello.core.web; @Controller @RequiredArgsConstructor public class LogDemoController { private final LogDemoService logDemoService; private final MyLogger myLogger; @RequestMapping("log-demo") @ResponseBody public String logDemo(HttpServletRequest request) { String requestURI = request.getRequestURI(); myLogger.setRequestURL(requestURI); myLogger.log("controller test"); logDemoService.logic("testId"); return "OK"; } }
: Controller
package hello.core.web; @Service @RequiredArgsConstructor public class LogDemoService { private final MyLogger myLogger; public void logic(String id) { myLogger.log("service id = " + id); } }
: Service
: 정상 동작함을 확인할 수 있습니다.
웹 스코프와 프록시 동작 원리
주입된 myLogger를 확인해 보겠습니다.
System.out.println("myLogger = " + myLogger.getClass());
: 실행 결과 지난번 @Configuration을 사용했을 때처럼 CGLIB관련 코드들이 보입니다.
CGLIB는 내 클래스를 상속받은 가짜 프록시 객체를 생성하여 주입합니다.
- @Scope에 proxyMode = ScopedProxyMode.TARGET_CLASS를 설정하면, 스프링 컨테이너는 CGLIB라는 바이트코드 조작 라이브러리를 사용하여 MyLogger를 상속받은 가짜 프록시 객체를 생성합니다.
- 결과를 보면, 우리가 등록한 순수한 MyLogger 클래스 대신 MyLogger$$EnhancerBySpringCGLIB라는 이름의 객체가 등록된 것을 확인할 수 있습니다. 스프링 컨테이너는 "myLogger"라는 이름으로 실제 객체 대신 이 가짜 프록시 객체를 등록합니다.
- ac.getBean("myLogger", MyLogger.class)로 조회해도 프록시 객체가 반환되는 것을 확인할 수 있습니다. 따라서, 의존관계 주입 시에도 가짜 프록시 객체가 주입됩니다.
가짜 프록시 객체는 요청이 있을 때, 내부에서 실제 빈을 요청하는 위임 로직을 포함하고 있습니다.
- 이 가짜 프록시 객체는 실제 `myLogger`를 찾는 방법을 알고 있습니다.
- 클라이언트가 `myLogger.logic()`을 호출하면, 실제로는 가짜 프록시 객체의 메서드를 호출한 것입니다.
- 가짜 프록시 객체는 request 스코프의 실제 `myLogger.logic()`을 호출합니다.
- 가짜 프록시 객체는 원본 클래스를 상속받아 생성되므로, 이 객체를 사용하는 클라이언트는 원본인지 아닌지를 구분할 필요 없이 동일하게 사용할 수 있습니다. (다형성을 이용)
[ 동작 정리 ]
- CGLIB라는 라이브러리를 사용하여, 내 클래스를 상속받은 가짜 프록시 객체를 생성하고 주입합니다.
- 이 가짜 프록시 객체는 실제 요청이 있을 때 내부에서 실제 빈을 요청하는 위임 로직을 포함하고 있습니다.
- 가짜 프록시 객체는 실제 request 스코프와는 무관하며, 단지 가짜 객체로서 위임 로직만을 가지고 싱글톤처럼 동작합니다.
[ 특징 정리 ]
- 프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯 편리하게 request 스코프 빈을 사용할 수 있습니다.
- 중요한 것은 Provider나 proxy를 사용하는 것이 아니라, 실제 객체를 꼭 필요한 시점에 지연 처리하는 개념입니다.
- 단순히 어노테이션만 변경함으로써 클라이언트 코드의 변경 없이 원본 객체를 프록시 객체로 대체할 수 있습니다. 이는 다형성과 DI 컨테이너의 큰 강점입니다.
- 프록시는 웹 스코프뿐만 아니라 다른 스코프에도 활용할 수 있습니다. 예를 들어, 프로토타입 스코프 빈에도 사용할 수 있습니다.
[ 주의점 ]
- 프록시를 사용하면 마치 싱글톤을 사용하는 것처럼 보이지만, 실제로는 다르기 때문에 주의가 필요합니다.
- 이러한 특별한 스코프의 빈은 꼭 필요한 곳에서만 최소한으로 사용해야 합니다. 무분별한 사용은 유지 보수를 어렵게 만들 수 있습니다.
해당 글에 포함된 코드나 그림은 김영한님이 제공해주신 자료를 바탕으로 작성되었습니다.
스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런
스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런
김영한 | 스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보
www.inflearn.com
'Java > Spring' 카테고리의 다른 글
Servlet / JSP / MVC (2) 2025.02.12 Understanding Web Applications (5) 2025.01.14 Bean Lifecycle Callback (3) 2025.01.03 Automatic injection of dependencies (1) 2025.01.03 Component Scan (3) 2024.12.20