java
CHAPTER 75 / 99
읽기 약 2분
FUNCTION
예외 처리 전략
핵심 개념
@ControllerAdvice·@ExceptionHandler·커스텀 예외·RFC 7807 — 글로벌 예외 처리.
본문
커스텀 예외 계층
// 베이스
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 — 글로벌 핸들러
@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 응답 형식
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"detail": "사용자 ID 123를 찾을 수 없음",
"instance": "/api/users/123",
"code": "USER_NOT_FOUND"
}Validation
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 통합
@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