꼬물꼬물
빈 스코프 [웹 스코프] 본문
싱글톤은 스프링 컨테이너의 시작과 끝까지 함께하는 매우 긴 스코프
프로토타입은 생성과 의존관계 주입, 그리고 초기화까지만 진행하는 특별한 스코프
웹 스코프
- 웹 스코프는 웹 환경에서만 동작한다.
- 웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다. 따라서 종료 메서드가 호출된다.
웹 스코프의 종류
- request: HTTP 요청이 들어오고 나갈 때까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성, 관리 된다.
- session: HTTP Session과 동일한 생명주기를 가지는 스코프
- application: 서블릿 컨텍스트와 동일한 생명주기를 가지는 스코프
- websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프
하나의 HTTP request 내에서는 같은 객체를 바라보게 된다.
프로토 타입은 요청마다 생성된다면 리퀘스트는 HTTP 리퀘스트에 맞춰서 각각의 하나를 관리한다.
A가 요청하면 A 전용 빈 객체가 생성되어 사용되다가 응답이 끝나면 파기된다.
Request 스코프 예제 만들기
웹 스코프는 웹 환경에서만 동작하므로 web 환경이 동작되도록 라이브러리를 추가해야한다.
//web 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
- spring-boot-starter-web 라이브러리를 추가하면 스프링 부트는 내장 톰캣 서버를 활용해 웹 서버와 스프링을 함께 실행시킨다.
- 스프링 부트는 웹 라이브러리가 없으면 AnnotationConfigApplicationContext를 기반으로 애플리케이션을 구동한다.
- 웹 라이브러리가 추가되면 웹과 관련된 추가 설정과 환경들이 필요해 AnnotationConfigServletWebServerApplicationContext를 기반으로 애플리케이션을 구동한다.
request 스코프 예제 개발
동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다. 이때 사용하기 좋은 것이 request 스코프이다.
- 기대하는 공통 포맷: [UUID][requestURL][message]
- UUID를 사용해 HTTP 요청을 구분하자
- requestURL 정보도 추가로 넣어 어떤 URL을 요청해 남은 로그인지 확인하자
@Scope("request")
@Component
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("[" + this.uuid + "] " + "[" + requestURL + "] " + "[" + message + "]");
}
@PostConstruct
public void init(){
this.uuid = UUID.randomUUID().toString();
System.out.println("[" + this.uuid + "] " + "request scope bean create: " + this);
}
@PreDestroy
public void close(){
System.out.println("[" + this.uuid + "] " + "request scope bean close: " + this);
}
}
- log를 출력하기 위한 MyLogger 클래스
- @Scope(value="request")를 사용해 request 스코프로 지정. 이제 이 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸한다.
- @PostConstruct 초기화 메서드를 사용해 빈이 생성되는 시점에 uuid를 생성해 넣어준다.
- requestURL은 빈이 생성되는 시점에는 알 수 없으므로, 외부에서 setter로 입력받는다.
- HTTPServletRequest를 통해 요청 URL을 받는다.
- 받은 requestURL값을 저장한다. myLogger는 HTTP 요청 당 각각 구분되므로 다른 HTTP 요청 때문에 값이 섞이는 걱정은 하지 않아도 된다.
- 컨트롤러에서 controller test라는 로그를 남긴다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody // view없이 문자 반환
public String logDemo(HttpServletRequest request){ // HttpServletRequest는 자바에서 제공하는 http 요청 정보를 받을 수 있는 것.
String requestURL = request.getRequestURI().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("test ID");
return "OK";
}
}
- 오류 발생.
- 스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주잉ㅂ이 가능하지만 request 스코프 빈은 고객의 요청이 와야 생성된다. @RequiredArgsConstructor에서 문제가 생긴다.
1) Provider 사용하기
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody // view없이 문자 반환
public String logDemo(HttpServletRequest request){ // HttpServletRequest는 자바에서 제공하는 http 요청 정보를 받을 수 있는 것.
String requestURL = request.getRequestURI().toString();
// 현 시점에 가져오기
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("test ID");
return "OK";
}
}
@RequiredArgsConstructor
@Service
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " +id);
}
}
- ObjectProvider를 사용해 request가 들어오고 MyLogger를 주입 받는다.
- 동시에 여러 요청이 와도 요청마다 각각 객체를 관리한다.
- ObjectProvider 덕분에 Provider.getObject() 호출 시점까지 스프링 컨테이너에게 요청을 지연할 수 있다.
- Provider.getObject()를 호출하는 시점에는 HTTP 요청이 진행중이므로 request scope 빈 생성이 정상 처리된다.
- 같은 HTTP Request면 controller, service에서 찾는 스프링 빈은 같은 빈이 유지된다.
2) 스코프와 프록시
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class MyLogger {}
- proxyMode = ScropedProxyMode.TARGET_CLASS를 추가했다.
- 적용 대상이 인터페이스가 아닌 클래스면 TARGET_CLASS
- 적용 대상이 인터페이스면 INTERFACES 선택
- MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둔다.
- 기능을 실제 호출하는 시점에 진짜를 찾아서 동작한다.
- CGLIB라는 라이브러리로 내 클래스를 상속받은 가짜 프록시 객체를 만들어 주입한다.
- @Scope(proxyMode=ScopedProxyMode.TARGET_CASS)를 설정하면 스프링 컨테이너는 CGLIB이라는 바이트 코드를 조작하는 라이브러리를 사용해 MyLogger를 상속받은 가짜 프록시 객체를 생성한다.
- 의존관계 주입도 가짜 프록시 객체가 주입되어있다.
가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
- 가짜 프록시 빈은 매부에 실제 MyLogger를 찾는 방법을 알고있음
- mylogger.logic()도 가짜 프록시 객체의 메서드를 호출한 것이다.
- 가짜 프록시 객체가 request 스코프의 진짜 myLogger.logic()을 호출한다.
- 가짜 프록시 객체는 원본 클래스를 상속받아 만들어져 클라이언트는 동일하게 사용할 수 있다.(다형성)
- 가짜 프록시 객체는 싱글톤처럼 동작한다.
✔️ 정리
- 프록시 객체 덕분에 클라이언트는 싱글톤 빈을 사용하듯 편리하게 request scope를 사용할 수 있다.
- provider와 프록시의 핵심은 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다.
- 단지 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 다형성과 DI 컨테이너가 가진 큰 장점!
- 마치 싱글톤처럼 사용하는 것 같지만 다르게 동작하기 때문에 주의!
- ㅣ런 특별한 Scope는 최소화해 사용해야 한다.
'스터디 > 스프링 핵심 원리 - 기본편' 카테고리의 다른 글
빈 스코프 [프로토타입 스코프] (0) | 2022.10.03 |
---|---|
빈 생명주기 콜백 (0) | 2022.10.01 |
의존관계 자동 주입 [조회한 빈 모두 사용하기, 자동/수동 기준] (1) | 2022.09.30 |
의존관계 자동 주입 [@Autowired 필드 명, @Qualifier, @Primary, 애노테이션 생성] (1) | 2022.09.30 |
의존관계 자동 주입 [롬복과 최신 트랜드] (0) | 2022.09.30 |