[Spring-기본] 웹 스코프

DANI·2023년 11월 28일

Spring[김영한T]

목록 보기
30/31
post-thumbnail

📕 웹 스코프의 특징

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

📑 웹 스코프 종류

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


🔍 request 스코프의 예시

각 클라이언트에 각각 할당되는 request 스코프가 있다.





🔍 예제로 알아보자!

💾 build.gradle

implementation 'org.springframework.boot:spring-boot-starter-web' 추가


💻CoreApplication 실행

🚫 에러 발생

Description:

The bean 'fixedDiscountPolicy', defined in class path resource [hello/core/DiscountConfig.class], could not be registered. A bean with that name has already been defined in file [C:\Users\Dani\Desktop\core\out\production\classes\hello\core\Discount\FixedDiscountPolicy.class] and overriding is disabled.

Action:

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

🔑 해결 방안

💾 application.properties에 다음을 추가
👉 spring.main.allow-bean-definition-overriding=true

overriding의 default 값이 false로 나와서 생기는 오류



서버가 8080포트로 정상 작동한다.





💾 MyLogger

package hello.core.common;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import java.util.UUID;

@Component
@Scope(value = "request")
public class MyLogger {
    private String uuid;
    private String requestURL;

    // 빈이 생성되는 시점에는 requestURL을 알 수 없으므로 수정자 주입으로 받는다.
    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    public void log(String msg){
        System.out.println("[" + uuid + "]" + "[" + requestURL + " ] " + msg);
    }

    // 초기화
    @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);
    }
}


💾 LogDemoController

package hello.core.web;

import hello.core.common.MyLogger;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@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";
    }
}


💾 LogDemoService

package hello.core.web;

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

}

🔴 CoreApplication 실행

🔵 실행 결과

🚫 에러 발생

Caused by: org.springframework.beans.factory.support.ScopeNotActiveException: 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 스코프는 클라이언트의 요청이 있어야 생성된다.



🔑 해결 방안

Provider 사용
✅ 프록시 방법 사용 사용



🔑 Provider 사용


💾 LogDemoController

package hello.core.web;

import hello.core.common.MyLogger;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;



@Controller
@RequiredArgsConstructor
public class LogDemoController {
    private final LogDemoService logDemoService;
    // ObjectProvider 추가
    private final ObjectProvider<MyLogger> myLoggerObjectProvider;

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

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


💾 LogDemoService

package hello.core.web;

import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogDemoService {

    // ObjectProvider 추가
    private final ObjectProvider<MyLogger> myLoggerObjectProvider;

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

🔴 CoreApplication 실행

🔵 실행 결과


ObjectProvider 덕분에 ObjectProvider.getObject()를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다.

서버를 한 번 더 호출하면 다른 인스턴스가 생성된다. request는 한 번의 요청에 한 번의 인스턴스가 각각 생성된다.



🔍 서비스 로직 사이에 대기시간을 추가한 후 서버를 여러 번 새로고침 해보자!

package hello.core.web;

import hello.core.common.MyLogger;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;



@Controller
@RequiredArgsConstructor
public class LogDemoController {
    private final LogDemoService logDemoService;
    // ObjectProvider 추가
    private final ObjectProvider<MyLogger> myLoggerObjectProvider;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) throws InterruptedException {

        String requestURL = request.getRequestURL().toString();
        MyLogger myLogger = myLoggerObjectProvider.getObject();
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");

        // 서비스로직 사이에 대기시간 추가
        Thread.sleep(2000);

        logDemoService.logic("testId");
        return "OK";
    }
}

🔵 실행 결과

여러 번 요청이 와도 기존에 요청했던 인스턴스가 유지된 채로 서비스 로직까지 생성된다.




🔑 프록시 방법 사용


💾 LogDemoController 에 추가

  // myLogger클래스를 출력해보자!
  System.out.println(myLogger.getClass());

💾 LogDemoServiceProvider이전으로 수정한다!

💾 MyLogger

package hello.core.common;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;

import java.util.UUID;

@Component
// 프록시 모드를 추가한다.
// 적용 대상이 인터페이스일 경우 타켓변경
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
    private String uuid;
    private String requestURL;

    // 빈이 생성되는 시점에는 requestURL을 알 수 없으므로 수정자 주입으로 받는다.
    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    public void log(String msg){
        System.out.println("[" + uuid + "]" + "[" + requestURL + " ] " + msg);
    }

    // 초기화
    @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);
    }
}

🔵 실행 결과


CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입해준다.

@ScopeproxyMode = ScopedProxyMode.TARGET_CLASS를 설정하면 스프링 컨테이너는 CGLIB라는 바이트코드를 조작하는 라이브러리를 사용해서, MyLogger를 상속받은 가짜 프록시 객체를 생성한다.

가짜 프록시 객체는 원본 클래스를 상속받아 만들어졌기 때문에 사용하는 클라이언트 입장에서는 원본인지 아닌지 모르게 동일하게 사용된다(다형성)




✨ 프록시 객체 정리✨

  • 가짜 프록시 객체는 내부에 진짜 myLogger를 찾는 방법을 알고있다.

  • 클라이언트가 myLogger.log()를 호출하면 가짜 프록시 객체를 호출하는 것(CGLIB)이고, 가짜 프록시 객체는 request 스코프의 진짜 myLogger.log()fmf ghcnfgksek.

  • 가짜 프록시 객체는 실제 request scope와는 관계가 없고, 단순한 위임 로직만 있으며 싱글톤처럼 동작한다.

  • 애너테이션 변경만으로 프록시 객체를 생성할 수 있다. (다형성, DI 컨테이너가 가진 장점)



    ✨ 어느 방법을 쓰든 중요한 것은 객체 조회를 필요한 시점까지 지연처리 할 수 있다는 점이다. ✨

0개의 댓글