본문 바로가기
강의 실습/스프링 핵심 원리 - 기본편

request 스코프 예제 만들기

by jint 2026. 5. 10.

1. 웹 환경 추가
웹 스코프는 웹 환경에서만 동작하므로 라이브러리 추가

- build.gradle

...
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    // web 라이브러리 추가
    implementation 'org.springframework.boot:spring-boot-starter-web'

    ...

}
...


CoreApplication 에서 main() 메서드 실행시, 웹 애플리케이션이 실행되는 것을 확인할 수 있다.

- 콘솔

...
14:11:32.874 [main] INFO  o.a.coyote.http11.Http11NioProtocol --Starting ProtocolHandler ["http-nio-8080"]
14:11:32.887 [main] INFO  o.s.boot.tomcat.TomcatWebServer --Tomcat started on port 8080 (http) with context path '/'
14:11:32.890 [main] INFO  hello.core.CoreApplication --Started CoreApplication in 0.974 seconds (process running for 1.29)


* spring-boot-starter-web 라이브러리 추가시, 스프링 부트는 내장 톰캣 서버를 활용해 웹 서버와 스프링을 함께 실행

* 스프링 부트는 웹 라이브러리가 없으면 AnnotationConfigApplicationContext 기반으로 애플리케이션을 구동한다.
웹 라이브러리가 추가되면 웹 관련 추가 설정과 환경들이 필요하므로 AnnotationConfigServletWebServerApplicationContext 기반으로 애플리케이션을 구동한다.

만약 기본 포트인 8080 포트를 다른 곳에서 사용중이면 오류가 발생하므로 포트 변경이 필요하다.

- src/main/resources/application.properties

spring.application.name=core
server.port=9090



2. request 스코프 예제 개발
동시에 여러 HTTP 요청이 오면 어떤 요청이 남긴 로그인지 구분이 힘들다. 이 때 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


공통 포멧 : UUID {message}
UUID 를 사용해 HTTP 요청을 구분한다.
requestURL 정보도 추가로 넣어서 어떤 URL을 요청해서 남은 로그인지 확인한다.

1) MyLogger (로그 출력을 위한 클래스)
- src/main/java/hello/core/common/MyLogger.java

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;

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

}


· @Scope(value = "request") 를 사용해 request 스코프로 지정한다.
이제 이 빈은 HTTP 요청당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸된다.
· 이 빈이 생성되는 시점에 @PostConstruct 초기화 메서드를 사용해 UUID 를 생성하여 저장한다.
이 빈은 HTTP 요청당 하나씩 생성되므로, UUID 를 저장하면 다른 HTTP 요청과 구분할 수 있다.
· 이 빈이 소멸되는 시점에 @PreDestroy 소멸 메서드를 사용해 종료 메시지를 남긴다.
· 이 빈이 생성되는 시점에 requestURL 을 알 수 없으므로, 외부에서 setter 로 입력 받는다.

2) LogDemoController (MyLogger 가 잘 작동하는지 확인하는 테스트 컨트롤러)
- src/main/java/hello/core/web/LogDemoController.java

package hello.core.web;

import hello.core.common.MyLogger;
import hello.core.logdemo.LogDemoService;
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";
    }

}


· HttpServletRequest 를 통해서 요청 URL을 받아온다.
-> requestURL 값 : http://localhost:8080/log-demo
이렇게 받은 requestURL 값을 MyLogger 에 저장한다.
· MyLogger 는 HTTP 요청당 각각 구분되므로 다른 HTTP 요청 때문에 값이 섞이는 걱정이 사라진다.
· 컨트롤러에서 "controller test"라는 로그를 남긴다.

* requestURL 값을 MyLogger 에 저장하는 부분은 컨트롤러 보다 공통 처리가 가능한 스프링 인터셉터나 서블릿 필터를 활용하는 것이 좋다.
스프링 웹에 익숙하다면, 인터셉터를 활용해 구현해본다.

* 인터셉터
HTTP 요청이 컨트롤러 호출 직전에 어떤 작업을 공통화해서 처리할 수 있게 해주는 인터셉터 클래스

3) LogDemoService (비즈니스 로직이 있는 서비스 계층에서 로그 출력)
- src/main/java/hello/core/logdemo/LogDemoService.java

package hello.core.logdemo;

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

}


· 만약 request scope를 사용하지 않고 파라미터로 모든 정보를 서비스 계층에 넘기면, 파라미터가 많이 지저분해진다.
requestURL 같은 웹 관련 정보는 웹과 관련없는 서비스 계층까지 넘어오면 안 되고, 컨트롤러까지만 사용해야 한다.
서비스 계층은 웹 기술에 종속되지 않고 순수하게 유지하는 것이 유지보수 관점에서 좋다.
· request scope의 MyLogger 덕분에 웹 관련 정보를 서비스 계층에 파라미터로 넘기지 않고 MyLogger 필드(멤버 변수)에 저장해서 코드와 계층을 깔끔하게 유지할 수 있다.

4) 실행
애플리케이션 실행 시점에 오류 발생)
- 콘솔

13:40:39.812 [main] ERROR o.s.boot.SpringApplication --Application run failed
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'logDemoService' defined in file [C:\Users\admin\IdeaProjects\core\build\classes\java\main\hello\core\logdemo\LogDemoService.class]: Unsatisfied dependency expressed through constructor parameter 0: 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
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:804)
    at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1382)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1221)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:565)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:525)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:333)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:371)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:331)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:196)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.instantiateSingleton(DefaultListableBeanFactory.java:1218)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingleton(DefaultListableBeanFactory.java:1184)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:1121)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:994)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:621)
    at org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:143)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:756)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:445)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:321)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1365)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1354)
    at hello.core.CoreApplication.main(CoreApplication.java:10)
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
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:381)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:201)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:1225)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1704)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1651)
    at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:912)
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
    ... 21 common frames omitted
Caused by: java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
    at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:130)
    at org.springframework.web.context.request.AbstractRequestAttributesScope.get(AbstractRequestAttributesScope.java:43)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:369)
    ... 27 common frames omitted


스프링 애플리케이션 실행시 오류가 발생한다.

핵심 에러 메시지 : 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 스코프 빈은 아직 생성되지 않는다. 이 빈은 실제 고객의 요청이 와야 생성할 수 있기 때문이다!


참고링크 : https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8?cid=325969

 

스프링 핵심 원리 - 기본편| 김영한 - 인프런 강의

현재 평점 5.0점 수강생 49,600명인 강의를 만나보세요. 스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다. 스프링 기본 기능, 스프

www.inflearn.com

댓글