OPEN HYPER STEP
← 목록으로 (Java+Spring)
JAVA · 75 / 99
java
CHAPTER 75 / 99
읽기 약 2
FUNCTION

예외 처리 전략


핵심 개념

@ControllerAdvice·@ExceptionHandler·커스텀 예외·RFC 7807 — 글로벌 예외 처리.

본문

커스텀 예외 계층

JAVA📋 코드 (30줄)
// 베이스
public abstract class BusinessException extends RuntimeException {
    public abstract HttpStatus getStatus();
    public abstract String getCode();

    public BusinessException(String message) {
        super(message);
    }
}


// 도메인별
public class UserNotFoundException extends BusinessException {
    public UserNotFoundException(Long id) {
        super("사용자 ID " + id + "를 찾을 수 없음");
    }

    @Override public HttpStatus getStatus() { return HttpStatus.NOT_FOUND; }
    @Override public String getCode() { return "USER_NOT_FOUND"; }
}


public class DuplicateEmailException extends BusinessException {
    public DuplicateEmailException(String email) {
        super("이메일이 이미 등록됨: " + email);
    }

    @Override public HttpStatus getStatus() { return HttpStatus.CONFLICT; }
    @Override public String getCode() { return "DUPLICATE_EMAIL"; }
}

@ControllerAdvice — 글로벌 핸들러

JAVA📋 코드 (42줄)
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    // 비즈니스 예외
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ProblemDetail> handleBusiness(BusinessException e) {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(
            e.getStatus(), e.getMessage()
        );
        problem.setProperty("code", e.getCode());
        return ResponseEntity.status(e.getStatus()).body(problem);
    }


    // Validation 실패
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ProblemDetail> handleValidation(MethodArgumentNotValidException e) {
        Map<String, String> errors = new HashMap<>();
        e.getBindingResult().getFieldErrors().forEach(err ->
            errors.put(err.getField(), err.getDefaultMessage())
        );

        ProblemDetail problem = ProblemDetail.forStatusAndDetail(
            HttpStatus.BAD_REQUEST, "유효성 검사 실패"
        );
        problem.setProperty("errors", errors);
        return ResponseEntity.badRequest().body(problem);
    }


    // 그 외 모든 예외 — 마지막 방어선
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ProblemDetail> handleAll(Exception e) {
        log.error("Unexpected error", e);
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(
            HttpStatus.INTERNAL_SERVER_ERROR,
            "서버 오류가 발생했습니다."
        );
        return ResponseEntity.internalServerError().body(problem);
    }
}

RFC 7807 응답 형식

JSON📋 코드 (8줄)
{
    "type": "about:blank",
    "title": "Not Found",
    "status": 404,
    "detail": "사용자 ID 123를 찾을 수 없음",
    "instance": "/api/users/123",
    "code": "USER_NOT_FOUND"
}

Validation

JAVA📋 코드 (28줄)
public record SignupRequest(
    @NotBlank(message = "이름 필수")
    @Size(min = 2, max = 50)
    String name,

    @Email(message = "이메일 형식 오류")
    @NotBlank
    String email,

    @NotBlank
    @Pattern(regexp = "^(?=.*[A-Z])(?=.*\d).{8,}$",
             message = "8자 이상, 대문자+숫자 포함")
    String password
) {}


@RestController
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;

    @PostMapping("/users")
    public ResponseEntity<UserDto> signup(@Valid @RequestBody SignupRequest req) {
        // @Valid 실패 시 자동으로 MethodArgumentNotValidException
        // GlobalExceptionHandler에서 처리
        return ResponseEntity.ok(userService.signup(req));
    }
}

로깅 + Sentry 통합

JAVA📋 코드 (24줄)
@RestControllerAdvice
@Slf4j
@RequiredArgsConstructor
public class GlobalExceptionHandler {
    // ... 위 핸들러들

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ProblemDetail> handleAll(Exception e, HttpServletRequest req) {
        // MDC에 요청 컨텍스트 추가
        MDC.put("requestId", UUID.randomUUID().toString());
        MDC.put("path", req.getRequestURI());
        MDC.put("method", req.getMethod());

        log.error("Unexpected error: {}", e.getMessage(), e);

        // Sentry 통합
        Sentry.captureException(e);

        return ResponseEntity.internalServerError()
            .body(ProblemDetail.forStatusAndDetail(
                HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류"
            ));
    }
}

다음 챕터

CH.7 "캐싱: Redis + Spring Cache" — 성능 최적화.


AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude

무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6

내 Spring 코드의 예외 처리 부분을 분석해서
에러 응답 일관성와 개선 우선순위를 알려줘.
ChatGPT

무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro

예외 처리 vs 다른 패턴 비교를
실전 사례 5개로 보여주고 try-catch vs @ControllerAdvice를 알려줘.
Gemini

무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro

내 코드베이스 전체를 분석해서
예외 처리 관련 예외 처리 누락 위치를 보고해줘.
Grok

무료: Grok 4.1 / SuperGrok $30/mo

2026년 한국 기업의 예외 처리 채택률과
한국 백엔드 에러 코드 표준를 솔직히 알려줘.

⭐ 이것만 기억하세요
예외 처리 전략 이 3가지만 확실히 잡으세요
1.@RestControllerAdvice + 커스텀 예외 계층 = 일관된 에러 응답
2.RFC 7807 ProblemDetail은 Spring 6 표준 — 클라이언트가 동일 형식 파싱
3.다음 챕터 CH.7에서 캐싱 — Redis로 응답 속도 10배+


공유하기
진행도 75 / 99