-
(스프링 기본) 31 - 웹 스코프 + 프록시개발/Spring 2024. 10. 30. 10:19
* 웹 스코프 특징
- 웹 환경에서만 동작한다.
- 스프링이 해당 스코프의 종료 시점까지 관리한다. -> 종료 메서드 호출
■ 웹 스코프 종류
- request : HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프
각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리
- session : HTTP Session 과 동일한 생명주기 가지는 스코프
- application : ServletContext 와 동일한 생명주기 가지는 스코프
- websocket : 웹 소켓과 동일한 생명주기 가지는 스코프
■ 예제 실습
//web 라이브러리 추가 implementation 'org.springframework.boot:spring-boot-starter-web'
웹환경 구동을 위해 gradel 에 다음 내용을 추가해준다.
※ 포트 변경 필요 시
main/resources/application.properties
server.port=9090
다음 코드 추가해준다. (포트 번호는 본인이 원하는 방식 무관하다.)
@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 created: " + this); } @PreDestroy public void close() { System.out.println("[" + uuid + "] " + "request scope bean closed: " + this); } }
로그 출력을 위한 MyLogger 클래스를 만들어 준다.
@Scope(value = "request") 를 통해 request 스코프로 지정한다.
따라서 해당 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청 끝나는 시점에 소멸된다.
빈이 생성되는 시점에 자동으로 @PostConstruct 초기화 매서드를 사용해 uuid 를 생성해 저장한다.
HTTP 하나당 생성되므로 다른 HTTP 요청과 구분 가능하다.
빈 소멸 시점에는 @PreDestroy 를 사용해 종료 메세지를 남긴다.
requestURL 은 빈이 생성되는 시점에는 알 수 없으므로 외부에서 setter 로 받는다.
@Controller @RequiredArgsConstructor public class LogDemoController { private final LogDemoService logDemoService; private final MyLogger myLogger;
@RequiredArgsConstructor 를 사용해서 필수 required 값에 대한 자동으로 생성자 만들어준다.
더불어 생성자가 한 개이면 자동으로 의존성이 주입된다.
@RequestMapping("/log-demo") @ResponseBody public String logDemo(HttpServletRequest request) { String requestURL = request.getRequestURI().toString(); myLogger.setRequestURL(requestURL); myLogger.log("Controller"); logDemoService.logic("testId"); return "OK"; }
@ResponseBody : 문자 그대로 내보내기 가능 (우리는 현재 화면단이 없기 때문에)
HttpServletRequest : 자바에서 제공하는 서블릿 규약 중 Http Request 정보를 받을 수 있다. (고객 요청 정보)
이렇게 되면 고객이 요청한 url 정보를 알 수 있다.
log() 와 logic() 메서드를 통해 각각 로그를 출력하도록 만든다.
@Component @Scope(value = "request") public class MyLogger { private String uuid; private String requestURL;
myLogger 의 현재 scope 는 request 로 되어있다. 따라서 현재 http 요청 받고 끝나기 전까지 생명주기인데
요청이 받기도 전에 myLogger 생성 의존관계 주입을 요청해서 오류가 난다.
따라서 myLogger 생성 및 주입 단계를 고객 요청 이후에 처리해야 하고
여기에 이전 시간에 배운 Provider 를 활용하면 된다.
※ 참고
String requestURL = request.getRequestURI().toString(); myLogger.setRequestURL(requestURL);
해당 코드는 공통 처리가 가능한 스프링 인터셉터나 서블릿 필터 같은 곳에 있는 것이 좋다.
현재는 예제를 위해 컨트롤러에 배치했다.
- 인터셉터 : http 요청이 컨트롤러 호출 직전에 공통화해서 처리할 수 있는 것 (공통처리)
@Service @RequiredArgsConstructor public class LogDemoService { private final MyLogger myLogger; public void logic(String id) { myLogger.log("service id = " + id); } }
scope 를 request 로 지정하지 않으면 모든 정보가 서비스 계층에 넘어가는데
파라미터가 많아 지저분해지고 웹과 관련된 정보가 서비스 계층에도 넘어간다.
request scope MyLogger 덕분에 파라미터로 넘기지 않고,
MyLogger 멤버변수에 저장해서 코드와 계층 깔끔하게 유지가 가능하다.
※ 웹과 관련된 정보는 컨트롤러 까지만 사용.
서비스 계층은 웹 기술에 종속되지 않고, 가급적 순수하게 유지하는 것이 유지보수 관점에서 좋음.
■ 스코프와 Provider
private final ObjectProvider<MyLogger> myLoggerProvider;
ObjectProvider 를 통해 Mylogger 의존성 주입이 아니라,
Dependency Lookup 직접 필요한 의존관계를 찾는 의존성 조회 기능을 넣어준다.
@RequestMapping("/log-demo") @ResponseBody public String logDemo(HttpServletRequest request) { MyLogger myLogger = myLoggerProvider.getObject(); String requestURL = request.getRequestURI().toString(); myLogger.setRequestURL(requestURL); myLogger.log("Controller"); logDemoService.logic("testId"); return "OK"; }
그리고 요청을 받았을 때 getObject() 로 myLogger 꺼낸다.
@Service @RequiredArgsConstructor public class LogDemoService { private final ObjectProvider<MyLogger> myLoggerProvider; public void logic(String id) { MyLogger myLogger = myLoggerProvider.getObject(); myLogger.log("service id = " + id); } }
마찬가지로 서비스도 변경해준다.
이렇게 되면 컨트롤러 요청 왔을때 해당 의존성을 생성하고 주입시켜줘서 문제가 발생하지 않는다.
로그 정상적으로 남으며, 다른 요청을 주면 uuid 가 달라져서 요청별로 구분이 가능하다.
MyLogger myLogger = myLoggerProvider.getObject();
해당 코드로 빈이 처음으로 생성된다.
@PostConstruct public void init() { uuid = UUID.randomUUID().toString(); System.out.println("[" + uuid + "] " + "request scope bean created: " + this); }
생성 이후 init 호출돼서 uuid 가 만들어지고 찍어주게 된다.
myLogger.setRequestURL(requestURL);
set 을 통해 URL 을 담아두게 되고,
myLogger.log("Controller"); logDemoService.logic("testId"); return "OK";
컨트롤러, 서비스 각각 로그를 찍어주게 되고,
@PreDestroy public void close() { System.out.println("[" + uuid + "] " + "request scope bean closed: " + this); }
http 요청 종료 전 close 호출돼서 찍어주고
return "OK";
마지막 리턴 ok 화면에 반환한다.
■ 정리
- ObjectProvider.getObject() 활용하여 request scope 빈의 생성을 지연 (=스프링 컨테이너 빈 생성 요청을 지연)
- ObjectProvider.getObject() 호출 시점에는 HTTP 요청 진행중이여서 빈 생성이 정상 처리됨.
- 같은 HTTP 요청이면 같은 스프링 빈 반환해준다.
■ 스코프와 프록시
@Component @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
proxyMode = ScopedProxyMode.TARGET_CLASS 하게 되면,
MyLogger 를 가짜(프록시) 만들게 된다.
적용 대상이 클래스면 TARGET_CLASS 를 선택,
인터페이스라면 INTERFACES 선택한다.
이렇게 하면 MyLogger 가짜 프록시 클래스를 만들어두고
HTTP request 와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다.
스프링 컨테이너는 CGLIB 라는 바이트 코드 조작 라이브러리를 사용해
MyLogger 상속받은 가짜 프록시 객체를 생성한다.
MyLogger$$SpringCGLIB$$0 클래스로 만들어진 객체가 대신 등록되고,
스프링 컨테이너에 myLogger 이름으로 진짜 대신 가짜 프록시 객체를 등록한다.
의존관계 주입도 가짜 프록시가 된다.
※ 프록시?
프록시 서버는 클라이언트가 자신을 통해서 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해 주는 컴퓨터 시스템이나 응용 프로그램을 가리킨다. 서버와 클라이언트 사이에 중계기로서 대리로 통신을 수행하는 것을 가리켜 '프록시',
그 중계 기능을 하는 것을 프록시 서버라고 부른다.
가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
클라이언트가 myLogger.log 호출하면 가짜 프록시 객체의 메서드를 호출하는데,
가짜 프록시 객체는 request 스코프의 진짜 myLogger.log 를 호출한다.
가짜 프록시 객체는 원본 클래스를 상속 받아 만들어져서
클라이언트는 원본인지 아닌지 모르게 동일하게 사용이 가능하다.
* 동작 정리
CGLIB 라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어 주입한다.
가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다.
가짜 프록시 객체는 request scope 관계 없고, 단순히 위임 로직만 있고 싱글톤처럼 동작한다.
* 특징 정리
- 프록시 객체로 싱글톤 빈 사용하듯이 request scope 사용이 가능하다.
- ★ 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 (Provider, Proxy)
- 어노테이션 설정 변경으로 원본 객체를 프록시 객체로 대체 가능하다.
-> 다형성과 DI 컨테이너가 가진 가장 큰 장점이다.
- 웹 스코프 아니어도 프록시는 사용할 수 있다.
* 주의점
- 싱글톤을 사용하는 것 같지만 다르게 동작하니까 주의해야 한다.
- 특별한 scope 는 꼭 필요한 곳에 최소한만 사용하자.
'개발 > Spring' 카테고리의 다른 글
(스프링 기본) 30 - 빈 스코프 (싱글톤, 프로토 타입 - ObjectProvider, Provider) (0) 2024.10.29 (스프링 기본) 29 - 어노테이션 @PostConstruct, @PreDestroy (0) 2024.10.29 (스프링 기본) 28 - 빈 등록 초기화, 소멸 메서드 (initMethod, destroyMethod) (0) 2024.10.29 (스프링 기본) 27 - 인터페이스 InitializingBean, DisposableBean (0) 2024.10.29 (스프링 기본) 26 - 빈 생명주기 콜백 (0) 2024.10.29