Spring(11) - 빈 스코프 (Bean Scope) - 웹 스코프(Request)

김유담·2024년 6월 23일

spring

목록 보기
11/11
post-thumbnail

김영한 강사님스프링 핵심 원리의 강의 내용과 자료를 이용했음을 밝힙니다.

👨‍💻 웹 스코프

자 이제 프로토타입 스코프는 끝났고 웹 스코프 그 웹 스코프에서 request 웹스코프에 대해 알아

우선 웹 스코프의 특징

  • 웹 스코프는 웹 환경에서만 동작한다.
  • 웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다. 즉 종료 메서드가 호출된다.

그럼 이런 웹스코프에는 다양한 스코프가 있다.

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

여기서 request 스코프를 알아보자.

👨‍💻 환경 설정

위에서 말했듯이 웹 스코프는 웹 환경에서만 작동하므로 web 환경이 동작하도록 라이브러리를 추가해줘야 한다.

build.gradle의 dependencies에
implementation 'org.springframework.boot:spring-boot-starter-web' `
를 추가해주면 된다.

참고로 우리는 AnnotationConfigApplicationContext를 사용했는데 웹 라이브러리를 추가하면 웹과 관련된 추가 설정들이 필요함으로 AnnotationConfigServletWebServerApplicationContext를 사용해야한다.

👨‍💻 예제

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

로그 출력을 클래스이다.

초기화 콜백 함수에서 다른 요청과 구분할 수 있게 고유 번호인 uuid를 생성해서 필드에 저장해놓고 url은 생성되고 넣어줘야 해서 setter를 만들어 놓았다.

LogDemoController

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;

    @RequestMapping("log-demo") // http://localhost:8080/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";
    }
}

로거가 잘 작동하는지 확인하는 컨트롤러이며 HttpServlet으로 요청 URL을 받아서 logger의 setter로 넣어주고 log를 만들어준다.

그리고 서비스 계층에서도 log 출력을 해준다.

logDemoService.logic("testId");

LogDemoService

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;

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

서비스 계층에서도 로그를 출려하기 위한 클래스이다.

이렇게 MyLogger, LogDemoController, LogDemoService를 다 만들고 coreApplication을 실행하고 http://localhost:8080/log-demo에서 새로고침을 하면 요청이 들어간다.

계획한 이상적인 로그는

[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 빈인 MyLogger가 생성되지 않았기 때문이다.

왜 생성되지 않았을까?

그런데 컨테이너에서 LogDemoController의 생성자 주입하면 LogDemoController의 필드인 logDemoService, myLogger에 의존관계가 주입된다.

그.러.나. MyLogger는 request 스코프이다.
요청이 와야 생성이 된다. 그렇기에 아직 생성도 안된 필드에 의존관계를 주입할려고 했기에 오류가 난 것이다.

👨‍💻 스코프와 Provider

이를 해결하기 위한 방법 중 하나는 Provider이다.

프로토타입 스코프 할 때 본 그 Provider 맞다.

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> myLoggerProvider; // check here

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

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

ObjectProvider의 getObject()를 통해서 요청이 들어왔을 때, myLogger를 호출할 수 있다.
빈의 생성을 지연시킬 수 있다.

그리고 LogDemoService도 MyLogger를 필드로 가지고 있기에

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final ObjectProvider<MyLogger> myLoggerProvider;// check here

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

이렇게 provider를 적용해서 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

이렇게 잘 나오는 것을 확인할 수 있다.

http://localhost:8080/log-demo에서 새로고침을 하면 계속해서 HTTP요청이 들어가고 그 요청들을 uuid를 통해서 구분할 수 있다는 것을 확인할 수 있다.

👨‍💻 스코프와 프록시

이거 약간 롬복같은 느낌이 난다.

그때 롬복이 개발자들이 얼마나 게으르면 혹은 효율을 추구하면 이렇게 기능들이 나오지? 라고 했는데 이 프록시도 비슷한 맥락이다.

정말 간단하다.

원래 오류 생기던 예제에서 MyLogger의 @Scope(value = "request")@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)로 바꿔주기만 하면 끝이다.

참고로 적용대상이 class여서 TARGET_CLASS이다.
인터페이스인 경우에는 INTERFACES를 선택해야한다.

그리고 coreApplication을 실행하면 결과가 예상한대로 나오는 것을 알 수 있다.

자 그러면 이 프록시는 뭐 하는 친구일까?

사전의 말과 일치한다.

MyLogger의 가짜 프록시 클래스, 가짜이지만 대리역할을 맡은 클래스를 필드 myLogger에 넣어놓는 것이다.

그래서 LogDemoController에서 그냥 myLogger의 클래스를 확인해보면

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

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

이런 식으로 바이트코드 조작된(CGLIB) 가짜 프록시 객체가 들어있는 것을 확인할 수 있다.

심지어 스프링 컨테이너에도 myLogger라는 이름으로 이 가짜 프록시 객체가 들어있고, getBean으로 불러와도 가짜 프록시 객체가 조회된다.

그럼 가짜가 어떻게 코드 정상적으로 돌아가게 만들까?

바로 가짜한테 요청이오면 가짜가 진짜 빈을 찾는 위임 로직이 있기 때문이다.

예를 들어 myLogger.log()를 호출하면 당연히 가짜의 메서드를 호출한 것이다.
하지만 가짜가 진짜 request 스코프의 myLogger.log()를 호출해서 반환하기에 코드는 정상적으로 작동한다.

장점

  • 지연처리 됨
  • 클라이언트 입장에서는 원본인지 아닌지 모르니 다형성을 가진다.
  • 간단하다

주의점

  • 싱글톤이라고 생각할 수 있지만 실제로는 다르게 동작하니 주의
  • 프록시는 특별한 스코프이기에 필요한 곳에만 최소화해서 사용하기.

👨‍💻 마무리

이로서 김영한 강사님스프링 핵심 원리 강의 내용을 다 정리했다.
처음에는 내 생각도 많이 넣고 좀 색다르게 작성할려고 노력했는데 후반부로 갈 수록 시간도 없고 내용도 어려워지니 김영한 강사님이 수업한 내용과 내가 이해한 내용 이 2개만 가지고 작성한 것 같아서 아쉽기도 하다.

하지만 마지막 내용이 어려웠지만 정리하면서 이해를 할 수 있어서 velog를 작성하길 잘했다는 생각도 든다.

1달정도 전역하기 전에 최대한 많이 들을 생각이다.
그리고 사이드 프로젝트도 하면 좋겠다.
어쩌면 게시판 사이드 프로젝트를 살릴 수도?

아무튼 이제 다음 강의 http 웹 기본지식를 이제 들어야겠다.

BYE BYE~

profile
잘하진 못할지언정 꾸준히 하는 개발자:)

0개의 댓글