Spring

[Spring 완전 정복 시리즈] 22편 - 빈 스코프 완전 정복 (2): 웹 스코프와 Provider/프록시 패턴

dev-nadan 2025. 8. 1. 13:50

이전 편에서는 싱글톤과 프로토타입 스코프에 대해 정리했다.

이번 편에서는 실제 웹 애플리케이션에서 자주 사용되는 웹 스코프(Request, Session 등)의 개념과

스프링에서 이를 안전하게 사용하는 방법을 알아본다.

 


웹 스코프란?

웹 스코프는 웹 요청(Request)과 세션(Session)의 생명주기에 따라 빈의 생존 범위를 지정한다.

스코프 이름 생존 범위
request 하나의 HTTP 요청이 들어와서 나갈 때까지
session 하나의 HTTP 세션이 생성되어 종료될 때까지
application 서블릿 컨텍스트(ServletContext) 범위
websocket 웹소켓 연결이 유지되는 동안

 


Request 스코프 사용 예시

@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 closed: " + this);
    }
}

 

  • uuid를 통해 각 요청별로 로그를 구분할 수 있도록 한다
  • 요청 URL을 저장하면 어떤 요청에서 남긴 로그인지 추적 가능하다

 


웹 스코프를 Controller에서 직접 사용할 때 발생하는 문제

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    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");
        return "OK";
    }
}

위 코드는 오류가 발생할 수 있다.

 

스프링 컨테이너가 생성될 때는 HTTP 요청이 존재하지 않기 때문에
@Scope("request") 빈을 주입하려 하면 IllegalStateException이 발생한다.

해결 방법 1: ObjectProvider 사용

@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();
        myLogger.setRequestURL(requestURL);
        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}
@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final ObjectProvider<MyLogger> myLoggerProvider;

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

 

  • getObject()를 호출할 때 HTTP 요청이 존재하면 그 시점에 빈을 생성함
  • Provider를 사용하면 HTTP 요청이 실제로 존재할 때까지 생성을 지연시킬 수 있다

해결 방법 2: 프록시(proxyMode) 사용

보다 깔끔한 해결책은 스프링이 제공하는 프록시 객체(proxy)를 활용하는 방법이다.

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
    ...
}
@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";
    }
}
@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;

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

 

  • @Scope(proxyMode = ...)로 설정하면, 가짜 프록시 객체가 주입
  • 프록시는 HTTP 요청이 시작된 이후에 실제 MyLogger를 찾아서 동작을 위임함
  • 코드도 간결하고 테스트도 쉬움

웹 스코프 사용 시 주의점

  • 웹과 관련된 request, session 객체는 컨트롤러에서만 다루는 게 좋다
  • 서비스나 레포지토리 계층에서는 웹 기술에 종속되지 않도록 설계하는 것이 유지보수에 유리하다