스프링 기본 section10

Sean·2025년 5월 30일

빈 스코프

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

스프링 빈은 기본적으로 싱글톤 스코프로 생성된다. 스프링은 다양한 스코프 지원함.

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

컴포넌트 스캔 자동 등록

@Scope("prototype")
@Component
public class HelloBean {}

수동 등록

@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
	return new HelloBean();
}

프로토타입 스코프

싱글톤 스코프 빈 조회 : 항상 같은 인스턴스 빈 반환
프로토타입 스코프 : 항상 새로운 인스턴스 생성, 반환

싱글톤 빈 요청 과정
1. 스프링 컨테이너에 요청
2. 스프링 컨테이너가 스프링 빈 반환 (싱글톤 스코프 빈은 컨테이너 생성시 이미 생성되어있음)
3. 나중에 같은 요청 오면, 같은 인스턴스 반환

프로토타입 빈 요청
1. 프로토타입 스프링 빈을 컨테이너에 요청
2. 해당 시점에 프로토타입 빈 생성 & 의존관계 주입.
3. 생성한 프로토타입 빈을 클라이언트에 반환
4. 이후에 같은 요청 와도 항상 새로운 프로토타입 빈 생성

프로토타입 스코프 빈 경우 : 빈 생성 + DI + 초기화 까지만 처리, 이후 관리 X. 넘겨받은 클라이언트에 책임 넘어감. @PreDestroy 종료 메서드 호출 안됨.

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

프로토타입 빈은 늘 새로운 인스턴스 생성. 하지만 싱글톤 빈 같이 사용하면 의도한 대로 작동X.

프로토타입 빈 요청

  • 별개의 인스턴스들이기에, count 각각 늘음.

싱글톤 빈에서 프로토타입 빈 사용

상황 : clientBean(싱글톤)이 프로토타입 빈 주입받아 사용.

  1. clientBean은 싱글톤 빈이기에 컨테이너 생성시 빈 생성 & DI 됨. DI 과정에서 프로토타입 빈 요청됨.
  2. 스프링 컨테이너가 프로토타입 빈 생성해서 clientBean에 반환.
  3. 이로서 프로토타입 빈을 내부에 보관하게 됨.(참조값을 보관)

문제 : 다른 사용자가 호출하더라도 싱글톤 빈이기에 같은 clientBean이 호출됨. 문제는 프로토타입 빈까지 동일한 것이 호출된다는 것. 왜냐하면 새로운 프로토타입 빈 생성이 아닌, 이전에 clientBean에 주입되어있는 프로토타입 빈이 사용되기 때문.

Provider로 문제 해결

싱글톤+프로토타입 사용 경우, 어떻게 해야 항상 새로운 프로토타입 빈을 생성할 수 있을까?

방법1. 스프링 컨테이너에 요청

가장 간단한 방법. 프로토타입 빈 사용할 때 마다 스프링 컨테이너에 새로 요청.

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 정도 기능만 가진게 있을까?

방법2. ObjectFactory, ObjectProvider

ObjectProvider : 지정한 빈을 컨테이너에서 대신 찾아주는 DL 제공.
ObjectFactory : 과거에 사용된 것. 추가기능 추가된 것이 ObjectProvider

@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;

public int logic() {
	PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
	prototypeBean.addCount();
	int count = prototypeBean.getCount();
	return count;
}
  • 항상 새로운 빈이 생성됨.
  • getObject() : 컨테이너를 통해 빈 찾아 반환 (DL)

특징
ObjectFactory : 단순한 기능. 별도 라이브러리 필요X. 스프링에 의존
ObjectProvider : ObjectFactory 상속. 옵션, 스트림 처리 등 기능 많음. 별도 라이브러리 필요X. 스프링 의존

방법3. JSR-330 Provider

@Autowired
private Provider<PrototypeBean> provider;

public int logic() {
	PrototypeBean prototypeBean = provider.get();
	prototypeBean.addCount();
	int count = prototypeBean.getCount();
	return count;
}
  • get() 통해 DL
  • 자바 표준
  • 단위 테스트, mock 코드 만들기 쉬워짐.

특징

  • get() 메서드 하나로 기능 매우 단순.
  • 별도 라이브러리 필요
  • 자바 표준이므로, 스프링 아닌 다른 컨테이너에서도 사용 가능.

정리

  • 프로토타입 빈 언제 사용? -> 매번 사용때마다 의존관계 주입 완료된 새로운 객체 필요시.
    하지만 실무에선 싱글톤 빈으로 대부분 문제 해결되어, 프로토타입 사용 드묾.
  • ObjectProvider, JSR330 Provider 등은 프로토타입 뿐만 아니라 DL 필요한 경우 언제든 사용 가능.

JSR vs ObjectProvider

ObjectProvider는 DL 위한 편의 기능 많이 제공하고, 스프링 외 별도 의존관계 추가 필요 없기 때문에 편리함. 만약 스프링 아닌 다른 컨테이너에서도 사용할 수 있어야 한다면 JSR-330 사용해야함.

자바 표준, 스프링 제공하는 기능 많이 겹침. 또한 대부분 스프링이 더 다양하고 편리한 기능 제공. 그렇기에 특별히 다른 컨테이너 사용 안한다면, 스프링이 제공하는 기능 사용하자.

웹 스코프

웹 스코프란?

  • 웹 환경에서만 동작
  • 프로토타입과 달리, 스프링이 해당 스코프의 종료 시점까지 관리.

웹 스코프 종류

  • request : http 요청 하나 들어오고 나갈 때 까지 유지되는 스코프. 각 http 요청마다 별도의 인스턴스 생성&관리됨.
  • session : http Session과 동일한 생명주기 가지는 스코프.
  • application : 서블릿 컨텍스트(ServletContext)와 동일한 생명주기 가지는 스코프
  • websocket : 웹 소켓과 동일한 생명주기 가지는 스코프

HTTP request 요청 당 각각 할당되는 request 스코프

request 스코프 예제 개발
동시에 여러 HTTP 요청 오면 정확시 어떤 요청이 남긴 로그인지 구분 어려움 -> request 스코프로 해결.

[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

위의 결과 내기 위한 코드

@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);
	}
}
  • uuid 저장함으로서, 다른 http 요청과 구분 가능.
@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";
	}
}
  • 로거 작동 확인 테스트용 컨트롤러

LogDemoService 추가

@Service
@RequiredArgsConstructor
public class LogDemoService {

	private final MyLogger myLogger;

	public void logic(String id) {
		myLogger.log("service id = " + id);
	}
}
  • request scope 없이 파라미터로 모든 정보 서비스 계층에 넘기면, 파라미터 많아서 지저분해짐. 더 문제는 requestURL 같은 웹과 관련된 정보가 웹과 관련없는 서비스 계층까지 넘어가게 됨. 웹과 관련된 부분은 컨트롤러까지만 사용해야 한다.
  • request scope의 MyLogger 덕분에 이런 부분 파라미터로 안넘기고, MyLogger의 멤버변수에 저장해서 코드,계층 깔끔하게 유지 가능.

기대 출력

[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 스코프 빈은 생성 안됨. 요청이 와야 생성된다.

방법1. 스코프와 Provider

아까 배운 Provider 사용해보자.

@Controller
@RequiredArgsConstructor
public class LogDemoController {

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

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

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

	@Service
	@RequiredArgsConstructor
	public class LogDemoService {

		private final ObjectProvider<MyLogger> myLoggerProvider;

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

브라우저에서 url 요청시 로그

[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
  • ObjectProvider 사용하여 request scope 빈 생성을 지연시킬 수 있게됨
  • ObjectProvider.getObject() 호출 시점에는 HTTP 요청 진행중이므로 request scope 빈 생성 정상 처리됨.

방법2. 스코프와 프록시

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

MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있음.

  • 적용 대상 : 클래스면 TARGET_CLASS, 인터페이스면 INTERFACES
@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";
	}
}

@Service
@RequiredArgsConstructor
public class LogDemoService {

	private final MyLogger myLogger;

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

-> 문제없이 실행됨.

웹 스코프와 프록시 동작 원리

주입된 myLogger 확인

System.out.println("myLogger = " + myLogger.getClass());

출력 결과

myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d

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

  • @ScopeproxyMode = ScopedProxyMode.TARGET_CLASS를 설정하면 스프링 컨테이너는 CGLIB라는 바이트코드를 조작하는 라이브러리 사용하여, MyLogger를 상속받은 가짜 프록시 객체를 생성한다.
  • 결과 보면, 순수한 MyLogger 클래스 X.
    MyLogger$$EnhancerBySpringCGLIB 라는 클래스로 만들어진 객체가 등록되어있음.
  • 스프링 컨테이너에 "myLogger"라는 이름으로 가짜 프록시 객체 등록함.
  • ac.getBean("myLogger", MyLogger.class) 해도 프록시 객체가 조회됨.
  • 의존관계 주입도 가짜 프록시 객체가 주입됨.
  • DI도 프록시 객체가 주입됨.

프록시 객체 : 요청 오면 내부에서 진짜 빈을 요청하는 위임 로직을 가짐.

  • 클라이언트가 myLogger.log() 호출하면 가짜 프록시 객체의 메서드를 호출한 것
  • 가짜 프록시 객체는 request 스코프의 진짜 myLogger.log() 호출

동작 정리

  • CGLIB 라이브러리로 진짜를 상속받은 프록시 객체 만들어 주입
  • 프록시 객체는 요청 오면 내부에서 요청하는 위임 로직 가짐.

특징 정리

  • 프록시 객체 통해, 싱글톤 빈 사용하듯 request scope 사용할 수 있음.
  • Provider, 프록시 뭘 쓰든, 핵심은 객체 조회 필요 시점까지 지연처리하는 것.
  • 웹 스코프 아니어도 프록시 사용 가능
  • 어노테이션 설정 변경만으로 프록시 객체 가능 -> 다형성, DI 강점
    특정 객체가 아닌, 클래스or인터페이스 기준으로 사용하기 때문에 문제없이 동작.
    또한 주입 대상을 스프링이 직접 결정해서 넣어줌. 내가 한건 MyLogger myLogger 밖에 없음.

주의점

  • 싱글톤같지만 다르게 동작하기에 주의
  • 특별한 scope는 최소한 사용. 무분별 사용은 유지보수 어렵게 만듦.
profile
저스트두잇

0개의 댓글