문제 상황: 로그가 요청마다 섞여버린다
스프링 웹 애플리케이션에서는 여러 사용자가 동시에 요청을 보낼 수 있다.
하지만 로그는 시간 순으로 출력되기 때문에
어떤 요청에서 발생한 로그인지 구분이 어렵다.
예를 들어 이런 로그가 찍혔다고 해보자:
회원 가입 처리 시작
상품 주문 시작
회원 가입 처리 시작
회원 가입 처리 완료
상품 주문 시작
상품 주문 완료
- 같은 메시지가 반복되거나,
- 서로 다른 요청의 로그가 뒤섞여 있다면,
이게 어떤 요청에서 나온 건지 디버깅이 매우 어렵다.
해결 전략: 요청마다 UUID를 붙여서 로그를 구분하자
각 HTTP 요청마다 UUID를 하나 생성하고,
그 UUID를 로그 메시지 앞에 붙이면
같은 요청에서 나온 로그끼리는 쉽게 묶어 볼 수 있다.
이를 구현하기 위해
스프링의 @RequestScope 스코프를 사용하면 된다.
웹 스코프를 사용하려면?
@RequestScope는 웹 애플리케이션 환경에서만 사용할 수 있다.
따라서 spring-boot-starter-web 의존성을 추가해야 한다:
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
이 라이브러리가 포함되면,
스프링 부트가 내장 톰캣 서버를 띄우고
웹 요청을 받을 수 있는 환경이 자동으로 구성된다.
RequestScope 로깅 빈 만들기
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestLogger {
private String uuid;
private String requestURL;
@PostConstruct
public void init() {
this.uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid + "] request scope bean created");
}
@PreDestroy
public void close() {
System.out.println("[" + uuid + "] request scope bean closed");
}
public void setRequestURL(String requestURL) {
this.requestURL = requestURL;
}
public void log(String message) {
System.out.println("[" + uuid + "][" + requestURL + "] " + message);
}
}
@RequestScope는 HTTP 요청마다 객체를 새로 만든다proxyMode = TARGET_CLASS를 설정하면
프록시 객체를 먼저 주입해 두고,
실제 요청 시점에 진짜 객체로 연결해준다
컨트롤러와 서비스 코드
LogDemoService.java
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final RequestLogger requestLogger;
public void logic(String id) {
requestLogger.log("service id = " + id);
}
}
LogDemoController.java
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final RequestLogger requestLogger;
@RequestMapping("/log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
requestLogger.setRequestURL(requestURL);
requestLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
로그 출력 예시
이제 요청을 보내면 다음과 같은 로그가 찍힌다:
[9e130bc2] request scope bean created
[9e130bc2][http://localhost:8080/log-demo] controller test
[9e130bc2][http://localhost:8080/log-demo] service id = testId
[9e130bc2] request scope bean closed
서로 다른 요청이 들어오면 UUID가 달라져서 요청별 로그 흐름이 명확하게 구분된다.
핵심 개념: 겉보기엔 싱글톤, 실제론 요청마다 생성
RequestLogger는 싱글톤처럼 주입된다.
하지만 실제로는 요청마다 새로운 객체가 생성된다.
그 비밀은 **프록시(proxy)**에 있다.
System.out.println("logger = " + requestLogger.getClass());
출력 결과:
logger = class hello.core.common.RequestLogger$$EnhancerBySpringCGLIB$$...
- 프록시 객체는 CGLIB로 생성된 "가짜 객체"
- 이 객체는 실제 요청이 들어오면
진짜RequestLogger를 찾아서 내부적으로 위임해준다
왜 좋은가?
- 컨트롤러나 서비스는 의존성 주입 구조를 바꾸지 않아도 된다
RequestLogger는 서비스 계층에서도 바로 쓸 수 있어서 파라미터 전달 없이 로깅 가능- 코드가 깔끔하고 테스트도 편하다
정리
@RequestScope는 HTTP 요청마다 객체를 새로 만든다proxyMode = TARGET_CLASS설정으로 싱글톤처럼 주입할 수 있다- 실제 주입된 객체는 프록시이고, 요청마다 내부적으로 진짜 객체에게 위임한다
- 이를 통해 요청 단위로 로그 흐름을 구분하는 구조를 간단하게 만들 수 있다
'SpringBoot' 카테고리의 다른 글
| Spring MVC 구조: 어댑터 패턴과 다형성은 왜 중요한가? (0) | 2025.05.20 |
|---|---|
| 웹 애플리케이션의 이해 (0) | 2025.05.13 |
| 스프링 DI: 생존자 주입을 권장하는 이유 (0) | 2025.05.05 |
| 스프링 빈 자동 등록과 의존성 주입 (1) | 2025.05.01 |
| 싱글톤 패턴과 스프링의 싱글톤 관리 (0) | 2025.04.30 |