모든 요청 흐름을 추적하고, 유저 인증 정보를 로그에 구조화하여 담는 방법에 대해 정리
특히 인증 기반 API를 운영하면서 실시간 대응과 문제 분석을 위해 로그 체계를 정리
1️⃣ 전체 로그 흐름 설계 목표
- traceId 기반으로 요청 → 응답 → 예외 흐름 추적
- 인증 정보 기반의 핵심 userId, userEmail 로그에 포함
- 인증 실패나 예외 발생 시에도 동일한 맥락 유지
- /admin/** 엔드포인트에 대해서는 별도 AOP 로그 확보
2️⃣ 필터 기반 MDC 구조화 처리
Spring의 OncePerRequestFilter를 활용하여 요청 시점에 traceId, IP, 인증 정보 등을 MDC에 세팅하고,
응답 및 예외 시에도 해당 정보를 그대로 사용
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String uri = request.getRequestURI();
String ip = getClientIP(request);
String traceId = UUID.randomUUID().toString().replace("-", "");
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated() && !"anonymousUser".equals(auth.getPrincipal())) {
Object principal = auth.getPrincipal();
if (principal instanceof User user) {
MDC.put("userId", String.valueOf(user.getId()));
MDC.put("userEmail", user.getEmail());
} else {
MDC.put("userId", principal.toString());
MDC.put("userEmail", "");
}
} else {
MDC.put("userId", "비회원");
MDC.put("userEmail", "");
}
MDC.put("traceId", traceId);
MDC.put("ip", ip);
log.info("[REQUEST] {} {} | IP={} | traceId={} | userId={} | userEmail={}",
request.getMethod(), uri, ip, traceId, MDC.get("userId"), MDC.get("userEmail"));
try {
chain.doFilter(request, response);
} finally {
log.info("[RESPONSE] {} {} | status={} | IP={} | traceId={} | userId={} | userEmail={}",
request.getMethod(), uri, response.getStatus(), ip, traceId, MDC.get("userId"), MDC.get("userEmail"));
MDC.clear();
}
}
Logback 설정 예시
<pattern>
%d{yyyy‑MM‑dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg
| userId=%X{userId} | userEmail=%X{userEmail} | traceId=%X{traceId} | ip=%X{ip}%n
</pattern>
- MDC.clear()를 finally에서 호출하여 메모리 누수 방지
- 필터에서 세팅한 MDC는 컨트롤러, 서비스, 예외 핸들러 전 영역에서 자동 포함
3️⃣ AOP 기반 관리자 API 요청 로깅
관리자 API에 대해서는 LoggingAspect를 통해 HTTP 요청 시점에 별도 로그를 남김
@Aspect
@Slf4j
@Component
public class LoggingAspect {
@Pointcut("within(@org.springframework.web.bind.annotation.RestController *)")
public void restController() {}
@Before("restController()")
public void logRequestInfo(JoinPoint joinPoint) {
var attr = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attr == null) return;
HttpServletRequest req = attr.getRequest();
String uri = req.getRequestURI();
if (!uri.startsWith("/admin")) return;
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (!isAdmin(auth)) return;
String method = req.getMethod();
String className = joinPoint.getSignature().getDeclaringTypeName();
String methodName = joinPoint.getSignature().getName();
String userInfo = getUserInfo(auth);
log.info("[ADMIN API 요청] {} {} | Controller={}.{} | 유저={}",
method, uri, className, methodName, userInfo);
}
private boolean isAdmin(Authentication auth) {
return auth != null && auth.isAuthenticated() && auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
}
private String getUserInfo(Authentication auth) {
Object principal = auth.getPrincipal();
if (principal instanceof User user) {
return String.format("id=%d, email=%s", user.getId(), user.getEmail());
}
return principal.toString();
}
}
예시 출력 로그
[ADMIN API 요청] POST /admin/posts/1/restore
| Controller=PostAdminController.restore | 유저=id=2, email=admin@example.com
- 인증된 ROLE_ADMIN 사용자에 한해 /admin/** 요청이 로깅됩니다.
- 분리된 감사 로그를 Logstash 등 외부 시스템으로 전송할 수 있습니다.
4️⃣ 다음 작업
- 이 구조를 FileBeat → Logstash → Elasticsearch → Kibana에 연동
- 로그스태시를 활용해서 로그 가공 및 각각 output
- Alert 설정/이상 패턴 감지
- Prometheus + Grafana를 활용해 메트릭 모니터링 구축
'SPRING' 카테고리의 다른 글
| [250602 트러블 슈팅]Playwright 병렬 크롤링 중 TargetClosedError 해결기 (4) | 2025.06.03 |
|---|---|
| [250602 트러블 슈팅] 크롤링 도중에 IP밴 (3) | 2025.06.02 |
| [SPRING] Lettuce 분산 락 트러블슈팅 (1) | 2025.05.28 |
| [SPRING] Lettuce 분산 락 구현 (1) | 2025.05.27 |
| [SPRING] 동시성 제어 분산 락 (2) | 2025.05.26 |