[스프링 로드맵] E-2. 전역 예외 처리 - 에러 코드와 로그 기준

2026. 1. 20. 16:09·백엔드

전역 예외 처리까지 만들었으면, 이제 실제 운영에서 바로 나오는 질문을 정리할 차례다.

  • “message 문자열만으로 충분한가?”
  • “프론트에서는 에러를 어떻게 구분하지?”
  • “운영 중 장애 나면 로그를 어떻게 추적하지?”
  • “어디서 로그를 남겨야 하지?”

이번 글에서는 에러를 ‘운영 가능한 형태’로 만드는 패턴을 정리한다.


0. 이 글에서 다루는 것

이번 단계의 목표는 다음 3가지다.

  1. 에러 코드를 enum으로 관리하는 이유
  2. 요청 단위 추적 ID를 응답/로그에 남기는 패턴
  3. 로그를 어디서, 어떤 레벨로 남기는지 기준

모니터링 툴, 분산 트레이싱 같은 건 여기 범위 아님.


1. 왜 message 문자열만으로는 부족한가

현재 구조는 보통 이런 응답이다.

{
  "status": 404,
  "message": "해당 근무 기록이 없습니다."
}

이 구조의 문제는 명확하다.

  • 메시지는 사람용이다
  • 프론트/클라이언트에서 분기 처리하기 애매하다
  • 메시지가 바뀌면 클라이언트 로직도 흔들릴 수 있다

그래서 보통 사람은 message를 보고,시스템은 errorCode를 본다


2. 에러 코드 enum 도입

2-1. 에러 코드 enum 정의

public enum ErrorCode {

    INVALID_REQUEST(400, "잘못된 요청입니다."),
    NOT_FOUND(404, "리소스를 찾을 수 없습니다."),
    INTERNAL_ERROR(500, "서버 내부 오류입니다.");

    private final int status;
    private final String message;

    ErrorCode(int status, String message) {
        this.status = status;
        this.message = message;
    }

    public int status() {
        return status;
    }

    public String message() {
        return message;
    }
}

여기서 포인트:

  • HTTP 상태 코드 + 기본 메시지를 한 세트로 관리
  • 문자열을 여기저기 흩뿌리지 않음
  • 에러 종류가 늘어나도 한 곳에서 관리됨

2-2. 커스텀 예외에 에러 코드 연결

public class BusinessException extends RuntimeException {

    private final ErrorCode errorCode;

    public BusinessException(ErrorCode errorCode) {
        super(errorCode.message());
        this.errorCode = errorCode;
    }

    public ErrorCode getErrorCode() {
        return errorCode;
    }
}

그리고 사용은 이렇게 한다.

throw new BusinessException(ErrorCode.NOT_FOUND);

3. 에러 응답에 errorCode 추가

3-1. 에러 응답 DTO 확장

public record ErrorResponse(
        int status,
        String code,
        String message,
        String traceId
) {}
  • status: HTTP 상태
  • code: 시스템용 에러 코드
  • message: 사용자/개발자용 메시지
  • traceId: 요청 추적용 ID

3-2. 전역 예외 처리기 수정

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusiness(
            BusinessException ex,
            HttpServletRequest request
    ) {
        ErrorCode errorCode = ex.getErrorCode();

        ErrorResponse body = new ErrorResponse(
                errorCode.status(),
                errorCode.name(),
                errorCode.message(),
                request.getAttribute("traceId").toString()
        );

        return ResponseEntity.status(errorCode.status()).body(body);
    }
}

4. 추적 ID(traceId)는 왜 필요하냐

운영 중 장애가 나면 보통 이런 일이 생긴다.

  • 프론트: “이 요청에서 500 났어요”
  • 백엔드: “로그가 너무 많아서 못 찾겠는데요?”

그래서 요청 하나당 ID를 하나 부여한다. → “해당 에러 응답의 traceId로 로그를 찾기”

4-1. 간단한 traceId 생성 필터

@Component
public class TraceIdFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain
    ) throws ServletException, IOException {

        String traceId = UUID.randomUUID().toString();
        request.setAttribute("traceId", traceId);

        filterChain.doFilter(request, response);
    }
}
  • 요청 시작 시 traceId 생성
  • request에 저장
  • 예외 응답/로그에서 공통 사용

보통 MDC를 쓰지만, 지금은 이 정도면 충분

→ MDC는 로그에 요청 단위 컨텍스트 정보를 자동으로 포함시키기 위한 기능으로, traceId 등을 ThreadLocal에 저장해 로그를 요청 단위로 추적할 수 있게 한다


5. 로그는 어디서 남겨야 하나

컨트롤러는 최소, 서비스는 의미 있는 지점만, 예외 처리기는 에러 로그의 중심

5-1. Controller

  • 보통 로그 거의 안 남김
  • 남기더라도 요청 시작/종료 정도
log.info("POST /api/shifts 요청");

5-2. Service

  • 비즈니스 분기 지점
  • 정책적으로 중요한 판단
log.warn("이미 존재하는 근무 기록. userId={}", userId);

5-3. GlobalExceptionHandler

  • 에러 로그의 최종 집결지
log.error("예외 발생. traceId={}, code={}",
          traceId, errorCode.name(), ex);

여기서:

  • stack trace는 서버 로그에
  • 클라이언트 응답에는 최소 정보만

6. 로그 레벨 기준

레벨 언제
INFO 정상 흐름, 요청 시작/종료
WARN 정책 위반, 예상 가능한 문제
ERROR 예외 발생, 장애 원인

DEBUG/TRACE는 개발 중에만.


7. 최종 에러 응답 예시

{
  "status": 404,
  "code": "NOT_FOUND",
  "message": "리소스를 찾을 수 없습니다.",
  "traceId": "3f2a8c1e-9e6b-4c44-8c5e-1a0c2f9d8a77"
}
  • 프론트: code로 분기
  • 운영: traceId로 로그 추적
  • 사용자: message 확인

8. 지금 단계에서 가져가야 할 감각

  1. 에러는 문자열이 아니라 코드로 관리
  2. 예외는 던지고, 응답은 전역에서 통제
  3. 로그는 많이보다, 의미 있게

'백엔드' 카테고리의 다른 글

[스프링 로드맵] F-1. 테스트란  (0) 2026.01.20
[스프링 로드맵] E-1. 전역 예외 처리 기본 - 응답을 하나로 묶는 이유  (1) 2026.01.20
[스프링 로드맵] D-2. Validation 응답 설계 - BindingResult vs 예외 처리  (0) 2026.01.20
[스프링 로드맵] D-1. Validation 기본 - DTO 검증이 필요한 이유  (0) 2026.01.20
[스프링 로드맵] C-3. Repository 실전 - JpaRepository가 해주는 것들  (1) 2026.01.19
'백엔드' 카테고리의 다른 글
  • [스프링 로드맵] F-1. 테스트란
  • [스프링 로드맵] E-1. 전역 예외 처리 기본 - 응답을 하나로 묶는 이유
  • [스프링 로드맵] D-2. Validation 응답 설계 - BindingResult vs 예외 처리
  • [스프링 로드맵] D-1. Validation 기본 - DTO 검증이 필요한 이유
samsam031
samsam031
samsam031 님의 블로그 입니다.
  • samsam031
    samsam031 님의 블로그
    samsam031
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 디지털포렌식
      • 드림핵 문제풀이
      • 대외활동
      • 개발 실습
      • 컴퓨터 보안
      • 클라우드
      • 자격증
      • 자연어처리
      • 백엔드
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
samsam031
[스프링 로드맵] E-2. 전역 예외 처리 - 에러 코드와 로그 기준
상단으로

티스토리툴바