본문 바로가기

WINK-(Web & App)/Spring Boot 스터디

[2025 1학기 스프링부트 스터디] 남윤찬 #2주차

반응형

스프링 핵심원리 기본편의 마지막 섹션인 빈 스코프입니다.

빈 스코프란?

빈 스코프: 빈이 존재할 수 있는 범위

    싱글톤 스코프: 스프링 컨테이너가 스프링 생성~종료까지 ‘전부’ 관리함

    프로토타입 스코프: 생성과 의존관계 주입, 초기화까지만 하고 스프링 컨테이너가 관리하지 않음. 클라이언트에 제공X

웹 관련 스코프

    request: 요청이 들어옴~나감까지

    session: 세션이 생성~종료까지

    application: 웹의 서블릿 컨텍스트(?)

스코프 등록은 @Scope() 어노테이션 사용

@Scope("prototype")
@Bean
PrototypeBean HelloBean() {}

 

프로토타입 스코프

싱글톤 스코프의 빈을 조회하면 스프링 컨테이너 안에 하나의 인스턴스만을 생성하지만, 프로토타입 스코프의 빈은 매번 다른 인스턴스를 생성, 의존관계주입, 반환을 한다(스프링 컨테이너에서 관리X → @PreDestroy 메서드 사용 불가, 종료하지 않기 때문).

프로토타입과 싱글톤을 같이 쓰는 방법

싱글톤을 만들 때 프로토타입을 주입 받게 만들면, 주입 받을 때 프로토타입 빈을 생성하기 때문에 의도대로 할 수 없다(요청마다 새로운 프로토타입 빈을 만들도록).

이를 해결하기 위한 방법은 크게 두 가지가 있다

  • 싱글빈이 프로토타입을 쓸 때마다 스프링 컨테이너에 새로 요청한다. 이처럼 의존관계를 외부에서 주입받는 것이 아니라 직접 필요한 의존관계를 찾는 것을 의존관계 조회(탐색), DL(Dependency Lookup)이라고 한다. 하지만 애플리케이션 컨텍스트 전체를 주입 받게 되면 스프링 컨테이너의 종속적이게 되기 때문에 지양하는 방법이다.
// 애플리케이션 컨텍스트 자체를 주입
@Autowired
private ApplicationContext ac;

public int logic() {
	// 필요한 빈을 주입 받은 애플리케이션 컨텍스트에서 조회
	PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
	prototypeBean.addCount();
	int count = prototypeBean.getCount();
	return count;
}
  • 그래서 애플리케이션 컨텍스트를 주입받지 않고 DL을 하는 다른 방법이 ObjectProvider이다(기존에는 ObjectFactory가 있었으나, 편의 기능을 추가해 이를 상속한 ObjectProvider를 주로 사용). getObject() 메서드를 호출하면 스프링 컨테이너를 통해 필요한 빈을 찾을 수 있다(DL).
// ObjectProvider를 사용해 주입 지연
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;

public int logic() {
	// getObject()를 사용하여 필요한 빈을 조회 (DL)
	PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
	prototypeBean.addCount();
	int count = prototypeBean.getCount();
	return count;
}
  • 그러나 이 마저도 스프링에 의존적이기 때문에, 추가적인(마지막) 방법으로는 JSR-330이라는 자바 표준의 인터페이스인 Provider를 사용하는 것이다. 이는 별도의 라이브러리가 필요하지만, get() 메서드 하나로 단순하고 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
import jakarta.inject.Provider;
...
// ObjectProvider 대신 자바 표준인 Provider 사용
@Autowired
private Provider<PrototypeBean> provider;

public int logic() {
	// getObject()와 같은 기능의 get()메서드로 DL
	PrototypeBean prototypeBean = provider.get();
	prototypeBean.addCount();
	int count = prototypeBean.getCount();
	return count;
}
// build.gradle에는 라이브러리를 다음과 같이 추가한다.
...
dependencies {
	implementation 'jakarta.inject:jakarta.inject-api:2.0.1'
}
...

하지만.. 프로토타입 스코프를 직접 사용할 일은 거의 없지만 언젠가 사용할 날을 위해(남의 코드를 볼 때..?) 기억 어딘가에 남겨두자.

 

웹 스코프

말 그대로 웹 환경에서 동작하는 스코프이다.

종류

    request: HTTP 요청 하나가 들어오고 나갈 때까지의 범위. 각 요청마다 별도의 빈 인스턴스가 생성/관리 된다

    session: HTTP session과 동일한 생명주기

    application: ServletContext와 동일한 생명주기

    websocket: 웹 소켓과 동일한 생명주기

이 강의에서는 request 스코프만 예제로 구현한다.(나머지는 이와 비슷한 동작 방식을 가진다고 설명한다)

하지만 설계대로 의존관계주입이 되도록 단순히 구현을 하게 되면, 아래와 같은 오류가 발생한다. request 스코프는 HTTP 요청이 들어왔을 때 빈이 생성되는데, 스프링 컨테이너가 생성될 때는 요청이 없기 때문에 주입해줄 객체가 없어 다음과 같은 오류가 발생하는 것이다.

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;

이러한 오류를 해결하기 위해, 빈의 생성을 지연시키기 위해, HTTP 요청이 오면 request scope 빈의 생성이 정상적으로 처리 되므로 ObjectProvider로 구현한다. request가 왔을 때만 스프링 컨테이너에 빈 생성을 요청(getObject())하면 굳이 동시요청이 들어왔을 때 쓰레드 처리 등등의 귀찮은 작업을 직접 할 필요가 없다.

private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;

// HTTP 요청이 들어오면
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
    // request에서 URL을 추출해서
	String requestURL = request.getRequestURL().toString();
    // ObjectProvider로 찾은(DL해서 찾은) 객체에
	MyLogger myLogger = myLoggerProvider.getObject();
    // URL을 추가해준다
	myLogger.setRequestURL(requestURL);
	
    // 이후에 로직 실행
	myLogger.log("controller test");
	logDemoService.logic("testId");
	return "OK";
}

하지만 이 마저도 복잡했는지, ObjectProvider를 쓰지 않기 위해 Proxy를 사용한다.

@Component
// 프록시로 쓸 객체가 클래스면 TARGET_CLASS, 인터페이스면 INTERFACES로 한다. proxyMode로 프록시 객체로 지정
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
private final LogDemoService logDemoService;
// 스프링 컨테이너 생성시에는 가짜 객체, 프록시 객체가 주입되고,
private final MyLogger myLogger;

// HTTP request가 오면 프록시 객체가 진짜 객체로 바뀌고 로직 실행
@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";
}

MyLogger클래스의 @Scope 어노테이션 옵션을 (value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) 로 지정해준다. 그러면 의존관계를 주입할 때 프록시 객체, 가짜 객체를 미리 주입해두고, 이 객체의 기능을 실제로 호출하는 시점에서 진짜 객체를 찾아서 작동시켜 Provider를 사용했을 때와 같은 결과를 만들어낸다.

핵심은 Provider를 쓰든 Proxy를 쓰든 request 요청이 들어오기 전까지 객체 조회를 지연 시킨다는 것이다. 어노테이션 설정만으로 원본을 프록시 객체로 대체할 수 있는데, 이것이 다형성과 DI 컨테이너, 스프링 컨테이너가 가진 가장 큰 장점이다.

물론 싱글톤처럼 작동하지만 다른 작동 방식을 가지기 때문에 주의해야하며, 이런 특별한 스코프는 무분별하게 사용하면 유지보수에 어려워지므로 필요할 때만 쓰도록 최소화 하자.

반응형