빈 스코프란
이제까지 배운 내용으로는 빈이 생성되고, 스프링 컨테이너에 저장되고, 스프링 컨테이너가 종료될 때까지 유지된다.
이건 빈이 기본적으로 싱글톤 스코프로 생성되기 떄문이다.
스코프는 빈이 존재할 수 있는 범위를 뜻한다.
빈 스코프를 지정하는 방법은 다음과 같다.
@Scope("prototype")
@Component
public class HelloBean {}
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}
프로토타입 스코프
싱글톤 스코프 빈을 조회하면 항상 같은 빈을 반환한다
반면 프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 반환한다.
위와같이 프로토타입 스코프의 빈은 스프링 컨테이너에 요청될 때마다 새로 생성된다.
여기서 중요한 점은 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다.
-> @PreDestroy와 같은 종료메서드는 호출되지 않는다.
스프링 컨테이너는 프로토타입 빈의 생성, 의존관계 주입, 초기화까지만 관여한다.
-> 종료메서드에 대한 호출은 클라이언트가 직접 해야한다.
프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점
프로토타입 스코프를 싱글톤 빈 내에서 주입하면 의도대로 잘 동작하지 않는다.
원래는 클라이언트가 프로토타입 빈 생성을 요청하고, 클라이언트가 해당 빈 안에 있는 수를 하나 늘리면 클라이언트 별로 다른 빈이 있기에 클라이언트끼리 서로 영향을 받지 않는다.
그런데 이런 식으로 싱글톤 내에서 프로토타입 빈을 주입받아서 사용할 때에는
클라이언트 A와 B가 각각 싱글톤 내의 프로토타입 빈 내의 count를 호출하면, 기존 프로토타입 빈의 의도와 다르게 서로 영향을 받게된다.
(A가 count를 1 늘리고, B가 1 늘리면 각각 1이 되는게 아니라 2가 된다.)
프로토타입 스코프 - 싱글톤 빈과 함께 사용 시 Provider로 문제 해결
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
가장 간단한 방법은 위 코드와 같이 로직이 수행될 때마다 빈을 하나하나 다시 생성하는 것이다.
이렇게 의존관계를 외부에서 주입받는 것이 아니라 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL)이라고 한다.
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;
}
이들은 Spring에 의존한다.
JSR-330 Provider
이는 자바 표준을 사용하는 방법이다.
`jakarta.inject:jakarta.inject-api:2.0.1`
위 라이브러리를 그래들에 추가해야한다.
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
그러면 위와 같이 간단하게 DL기능을 구현할 수 있다.
Provider은 스프링 외의 다른 컨테이너를 사용할 때 써야한다.
이처럼 스프링과 자바 표준 모두에 같은 기능이 있는 경우가 있는데, 특별한 경우가 아니라면 더 다양한 기능을 제공하는 spring을 사용하면 된다.
웹 스코프
웹 스코프는 웹 환경에서만 동작하고 종료시점까지 관리한다
웹 스코프 종류
request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴 스가 생성되고, 관리된다.
session:HTTP Session과 동일한 생명주기를 가지는 스코프
application:서블릿 컨텍스트( `ServletContext` )와 동일한 생명주기를 가지는 스코프
websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프
클라이언트A와 B가 동시에 요청을 했을 때 controller에서 service에 요청을하면 이 때 빈이 각각 하나씩 따로 생성된다.
request 스코프 예제 만들기
동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분이 어렵다.
따라서 위와 같이 UUID를 통해서 HTTP 요청을 구분하려고 한다.
package hello.core.common;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
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);
}
}
MyLogger은 request 스코프이다.
따라서 HTTP요청이 들어올 때마다 새로 생성된다.
이 안에는 uuid와 requestUrl 필드가 있어서 이 정보를 각각 저장할 수 있다.
package hello.core.web;
import hello.core.common.MyLogger;
import hello.core.logdemo.LogDemoService;
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";
}
}
위 코드는 테스트용 컨트롤러이다
HTTP 요청을 받을때마다 MyLogger에는 새로운 빈이 주입된다.
package hello.core.logdemo;
import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
위 코드는 비지니스 로직이 있는 서비스 계층이다.
원래였으면 requestUrl 같은 정보들을 service 계층에게도 전달해야해서 파라미터가 복잡해지는 등의 문제가 있었는데
request scope 덕분에 계층을 깔끔하게 유지할 수 있게 되었다.
하지만 위 코드는 오류가 난다. 스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, Request 스코프 빈은 실제 고객의 요청이 와야 생성할 수 있기 때문이다.
스코프와 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";
} }
컨트롤러를 위와 같이 사용하면 ObjectProvider 덕분에 ObjectProvider.getObject()를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다.
마찬가지로 Service단에서도 ObjectProvider을 사용하면 다행히도 같은 HTTP 요청에 대해서는 같은 빈이 반환된다.
스코프와 프록시
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
간단하게 선언해줄 때 이런 식으로 하게 되면 MyLogger의 가짜 프록시 클래스를 만들어두고, 이를 다른 빈에 미리 주입해 둘 수 있다.
동작 원리는 위와 같다. 일단, 가짜 프록시 객체가 주입되고 스프링 컨테이너에 이 가짜 프록시 객체를 등록한다.
이 때 가짜 프록시 객체는 내부에 진짜 myLogger를 찾는 방법을 알고 있다.
따라서 클라이언트가 myLogger.log()를 호출하면 가짜 프록시 객체는 진짜 객체를 호출한다.
클라이언트 입장에서는 원본인지 아닌지도 모르게, 동일하게 사용할 수 있다(다형성)
이렇게 하면 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 있다.
핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리한다는 것이다.
하지만 마치 싱글톤을 사용하는 것 같이 동작하지만, 실제로는 다르게 동작하고, 무분별하게 사용하면 유지보수가 어려워지니까 주의해야한다.
출처
인프런 김영한 스프링 핵심원리 - 기본편
스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런
김영한 | , 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보세요! 📢 수강 전 확인해주세요! 본 강의는 자바 스프링 완전 정복 시리즈의 두 번째 강의입니다. 우아한형제들 최연
www.inflearn.com
드디어 완강! 이제 mvc로 넘어가야겠다.
'Back-end > Spring' 카테고리의 다른 글
[Spring] 서블릿 (0) | 2025.04.04 |
---|---|
[Spring] 웹 애플리케이션의 이해 (0) | 2025.03.30 |
[Spring] 빈 생명주기 콜백 (1) | 2025.02.27 |
[Spring] 의존관계 자동 주입 (1) | 2025.02.27 |
[Spring] 컴포넌트 스캔 (5) | 2024.11.29 |