에러 처리

전역 예외 처리, 사용자 정의 예외, 에러/성공 응답 형식

마지막 수정: 2026-05

에러 처리

기본 원칙

  • 전역 예외 처리: @RestControllerAdvice 사용
  • API 응답에 스택트레이스 노출 금지
  • 에러 응답에 내부 구현 정보 (클래스명, SQL 등) 노출 금지
  • 비즈니스 예외는 커스텀 Exception 클래스 작성
  • 예상 못한 예외는 ERROR 레벨 로깅 후 공통 에러 응답 반환

에러 응답 형식

{
    "success": false,
    "errorCode": "400",
    "message": "에러 메시지"
}

성공 응답 형식

{
    "success": true,
    "data": { ... }
}

목록 조회:

{
    "success": true,
    "data": [ ... ],
    "totalCount": 100
}

GlobalExceptionHandler

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    // 비즈니스 예외 (400)
    @ExceptionHandler(BusinessException.class)
    public ResponseJson<Void> handleBusiness(BusinessException e) {
        log.warn("비즈니스 예외: {}", e.getMessage());
        return ResponseJson.fail(e.getErrorCode(), e.getMessage());
    }

    // 검증 실패 (400)
    @ExceptionHandler(ValidationException.class)
    public ResponseJson<Void> handleValidation(ValidationException e) {
        log.warn("검증 실패: {}", e.getMessage());
        return ResponseJson.fail(e.getErrorCode(), e.getMessage());
    }

    // Bean Validation 실패 (400)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseJson<Void> handleMethodArgumentNotValid(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
                .map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
                .collect(Collectors.joining(", "));
        return ResponseJson.fail("400", message);
    }

    // 예상 못한 예외 — ERROR 레벨 로깅 후 공통 에러 응답
    @ExceptionHandler(Exception.class)
    public ResponseJson<Void> handleGeneral(Exception e) {
        log.error("서버 오류 발생", e);    // 로그에 스택트레이스 기록
        return ResponseJson.fail("500", "서버 오류가 발생했습니다.");
    }
}

커스텀 Exception 클래스

비즈니스 예외는 커스텀 Exception 클래스를 작성한다.

// 기본 비즈니스 예외
public class BusinessException extends RuntimeException {

    private final String errorCode;

    public BusinessException(String message) {
        super(message);
        this.errorCode = "400";
    }

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

    public String getErrorCode() {
        return errorCode;
    }
}

// 도메인별 예외
public class UserNotFoundException extends BusinessException {
    public UserNotFoundException(Long userSn) {
        super("404", "사용자를 찾을 수 없습니다: " + userSn);
    }
}

public class DuplicateUserIdException extends BusinessException {
    public DuplicateUserIdException(String userId) {
        super("400", "이미 사용 중인 아이디입니다: " + userId);
    }
}

커스텀 예외 사용 패턴

@Transactional(readOnly = true)
public UserVo get(UserVo vo) {
    UserVo result = userMapper.get(vo);
    if (result == null) {
        throw new UserNotFoundException(vo.getUserSn());
    }
    return result;
}

@Transactional(rollbackFor = Exception.class)
public void regist(UserVo vo) {
    if (userMapper.existsByUserId(vo.getUserId()) > 0) {
        throw new DuplicateUserIdException(vo.getUserId());
    }
    userMapper.regist(vo);
}

HTTP 상태코드 기준

상황상태코드설명
성공200 OK정상 처리
잘못된 요청400 Bad Request검증 실패, 비즈니스 오류
미인증401 Unauthorized토큰 없음/만료
권한 없음403 Forbidden권한 부족
리소스 없음404 Not Found존재하지 않는 리소스
서버 오류500 Internal Server Error예상치 못한 오류

운영 환경 설정

# application-prod.yml
server:
  error:
    include-stacktrace: never    # 스택트레이스 응답 포함 금지
    include-message: never       # 예외 메시지 응답 포함 금지
    include-binding-errors: never

체크리스트

  • [ ] 전역 예외 처리: @RestControllerAdvice 사용
  • [ ] 비즈니스 예외: 커스텀 Exception 클래스 작성
  • [ ] 예상 못한 예외: log.error("설명", e) ERROR 로깅 후 공통 에러 응답
  • [ ] 응답에 스택트레이스 미포함
  • [ ] 응답에 내부 클래스명/SQL 미포함
  • [ ] 운영 환경 에러 설정 (include-stacktrace: never)