[Spring] 9-5. 웹 스코프

송광호·2024년 1월 24일

[Spring]

목록 보기
41/41
post-thumbnail

Spring 시리즈는 혼자 공부하며 기록으로 남기고, 만약 잘못 학습 한 지식이 있다면 공유하며 피드백을 받고자 작성합니다.
스프링에 대해 깊게 공부해보고자 인프런의 김영한 강사님께서 강의를 진행하시는 (스프링 핵심 원리 - 기본편) 강의를 수강하며 정리하는 글입니다.
혹여나 글을 읽으시며 잘못 설명된 부분이 있다면 지적 부탁드리겠습니다.


웹 스코프

특징

  • 웹 환경에서만 동작한다.
  • 웹 스코프는 스프링이 해당 스코프의 종료시점까지 관리한다. 종료 메서드 호출 됨!

종류

  • request : HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프로 응답이 나갈 때까지가 이 스코프의 범위이다.
  • session : HTTP 세션과 동일한 생명주기를 가진다.
  • application : 서블릿 컨텍스트(가 뭐지?)와 동일한 생명주기를 가진다.
  • websocket : 웹 소켓과 동일한 생명주기를 가진다.

하위 3개는 웹기술 공부하다보면 자연스레 이해가 간다고 하시는데.. 커리큘럼 다음강의(모든 개발자를 위한 HTTP 웹 기본 지식) 이거 들어보면 나오지 않을까.. 싶다

스코프 동작 구조

  • 만약 서로 다른 클라이언트가 0.00001초까지 동일하게 요청을 한다고 생각해보자.
  • 로그를 찍는다고 예시를 들면 A전용, B전용 객체가 만들어진다.
  • HttpRequest 마다 별도로 관리한다.

Request Scope 예제

웹 환경 추가

  • 웹 스코프는 웹 환경에서만 동작한다. 따라서 web 라이브러리의 추가가 필요하다

build.gradle 추가 필요

//web 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
  • 설치 후 CoreApplication을 실행 시켜보자.
Tomcat started on port(s) : 8080 ~~~~

라는게 뜨면 정상적으로 잘 동작한 것이다. http://localhost.com:8080 으로 들어가면 오류페이지가 나온다면 정상이다.

참고: 스프링은 내장톰캣 서버를 사용해서 웹 서비스와 스프링을 함께 실행시키는 것이다. 외장 톰캣을 사용하면 java 설치, war파일 빌드 다 따로따로 해야한다.

참고: 스프링 부트는 웹 라이브러리를 추가하면 웹과 관련된 추가 설정과 환경들이 필요하므로 AnnotationConfigServletWebServcerApplicationContext 를 기반으로 애플리케이션을 구동한다.
이름 짱기네;;

예제 개발

  • 만약 서비스가 너무 잘돼서 동시에 여러개의 요청이 온다고 생각해보자.
  • 로그를 여러개 남기는데.. 로그가 다 섞여서 구분하기 힘들다.
  • 이때 필요한게 하나의 HTTP 요청이 처리될 때까지 관리되는 Rquest Scope!

아래와 같이 로그가 남도록 개발을 해보자

[uuid][url] 서비스명

[srf41232...] request scope bean create
[srf41232...][http://localhost:8080/log-demo] controller test
[srf41232...][http://localhost:8080/log-demo] service id = testId
[srf41232...] request scope bean close
  • UUID를 이용하여 HTTP요청을 구분한다.
  • requestURL 정보도 추가하여 어떤 URL을 요청해서 남긴 로그인지 확인한다.

MyLogger 코드

@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);
    }
}
  • RequestURL은 나중에 별도로 세팅하도록 세터로 구현한다.
  • 요청을 구분해야하기 때문에 UUID의 생성은 URL요청이 들어왔을때 생성하도록 한다.
  • Request Scope는 소멸메서드가 호출이 된다.

LogDemoContoller 코드

@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("test Id");
        return "OK";
    }
}
  • @ResponseBody 애노테이션은 문자 그대로 응답을 보낼 수 있다.
    • 보통은 요청이 뷰 템플릿을 거쳐서 렌더링 돼서 나가야한다.
  • HttpServletRequest : http-request 정보를 받을 수 있다.

LogDemoService 코드

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;

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

실행

  • 기대하는대로 실행이 되는지 한번 보자. 애플리케이션 실행!
 Error creating bean with name 'myLogger': Scope 'request' is not active for the
 current thread;

라는 오류가 나온다면? 정상이다.

  • 글 초반부에 설명했듯이 myLoggerReqeust Scope라서 HTTP 요청이 들어와야 생성이 되는데 이미 있지도 않은걸 주입하려고하니 오류가 나는것이다.

해결방법?

  • 이전에 배운 Provider 쓰면 해결이 바로된다

Scope와 Provider

Object 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();
    }

    ...
    
    
}

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final ObjectProvider<MyLogger> myLoggerProvider;
    
    ...
}
  • ObjectProvider를 사용하여 .getObject() 메서드를 호출하는 시점까지 MyLogger 즉 Request 빈의 생성을 지연할 수 있다.

실행 결과

[c83056c5...] request scope bean create : hello.core.common.MyLogger@5035f3bb
[c83056c5...][http://localhost:8080/log-demo] [controller test]
[c83056c5...][http://localhost:8080/log-demo] [service id = test Id]
[c83056c5...] request scope bean close : hello.core.common.MyLogger@5035f3bb

Scope와 Proxy

  • 오류나는 코드 그대로 사용하고싶은데.. 그게더 깔끔한데.. 라고 생각하면 프록시 방식을 적용해보도록 한다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {}
  • 프록시 적용방법은 proxyMode = ScopedProxyMode.TARGET_CLASS 를 적용해주면 된다.
    • 적용 대상이 클래스면 TARGET_CLASS 선택
    • 적용 대상이 인터페이스면 INTERFACES 선택
  • 코드를 오류나던 코드로 돌리고, 실행시켜보면 Provider를 적용했을때의 결과랑 완전히 같은걸 볼 수 있다.

프록시 동작 원리

  • 한번 주입된 myLogger에 뭐가 들어와있나 출력을 찍어보자

myLogger = class hello.core.common.MyLogger$$SpringCGLIB$$0

  • 으이! 머야! 내가 구현한게 아니잖아!

CGLIB 라이브러리로 기존 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다

  • 그리고 스프링 컨테이너에 myLogger 라는 이름으로 진짜 대신에 이 가짜 프록시 객체가 등록된다.
    따라서 ac.getBean("myLogger", MyLogger.class)로 조회해도 가짜 프록시 객체가 조회된다.

프록시 동작 구조 그림

  • 프록시 객체는 진짜 myLogger를 찾는 방법을 알고있다.
  • 클라이언트가 logic() 메서드를 호출하면 프록시 객체의 메서드를 호출한 것이다.
  • 가짜 프록시 객체는 request scope의 진짜 로직을 호출한다.
  • 이 가짜 프록시 객체는 원본 클래스를 상속받아 만들어졌기 때문에 클라이언트 입장에서는 사실 원본인지 가짜인지 모르고 원본처럼 사용이 가능하다(다형성!)

특징

  • Provider를 사용하든, 프록시를 사용하는 핵심 아이디어진짜 객체 조회를 꼭 필요한시점까지 지연처리 한다.
  • 단순한 애노테이션 설정 변경만으로 원본 객체를 프록시로 대체할 수 있다는 것은 대단하다.
    이게 바로 다형성과 DI 컨테이너가 가진 가장 큰 강점이다!
  • 꼭 웹 스코프가 아니어도 프록시는 사용 가능하다.

주의점

  • 마치 프록시 객체때문에 싱글톤처럼 보이긴 하지만 같다고 생각하면 큰일난다. 주의해서 사용해야함
  • 이런 특별한 스코프는 꼭 필요한 상황에서만 최소한으로 사용해야한다.

0개의 댓글