왜 필요한가 @ExceptionHandler @ControllerAdvice 검증(Validation) 정리 📝 문제풀이
◀ 이전 📋 목차 CH 10 ▶
🛡️ CHAPTER 09 · 고급

에러를 "친절한 응답"으로 바꾸기

API는 항상 성공만 하지 않아요. 잘못된 요청·없는 데이터엔 적절한 상태코드와 메시지로 답해야 해요. 이번 장은 예외 처리(@ExceptionHandler·@ControllerAdvice)검증(@Valid)을 다뤄요. CH05의 HTTP 상태코드와 자연스럽게 이어져요.

🎯 이 장을 끝내면
📖
왜 필요한가
에러도 "설계"해야 해요
잘못된 요청과 없는 데이터에 어떻게 답할지 정하는 일이에요.

API는 항상 성공하지 않아요. 없는 회원을 조회하거나, 필수 값이 빠진 요청이 들어올 수 있어요. 이럴 때 서버는 적절한 에러 응답(상태코드 + 메시지)을 줘야 클라이언트가 무엇이 잘못됐는지 알 수 있어요.

🔤
예외(Exception) · 예외 처리 — 예외는 프로그램 실행 중 생긴 "문제 상황"이에요(없는 데이터 조회, 잘못된 값 등). 예외 처리는 이 문제를 잡아서(catch) 프로그램이 그냥 죽지 않고 알맞게 대응하도록 만드는 일이에요.
🔤
체크 예외 · 언체크 예외 — 자바의 예외는 두 갈래예요. 체크 예외는 컴파일 단계에서 "이건 꼭 처리하라"고 강제되는 예외(예: IOException), 언체크 예외는 처리를 강제하지 않는 예외(예: IllegalArgumentException·RuntimeException 계열)예요. 스프링 웹에서 우리가 직접 만드는 예외는 대개 언체크 예외로 두고, 아래에서 배울 방법으로 잡아서 응답으로 바꿔요. 지금은 "예외에도 종류가 있구나" 정도만 알면 충분해요.
💡
왜 굳이 예외를 "처리"할까요? 식당에서 주문이 잘못 들어왔을 때, 주방이 그냥 멈춰버리면(=서버가 500으로 죽으면) 손님은 영문을 몰라요. 대신 홀 직원이 "이 메뉴는 품절이에요"라고 알맞게 안내하면 손님도 다음 행동을 정할 수 있죠. 예외 처리는 바로 이 친절한 안내를 코드로 만드는 일이에요.
🔤
HTTP 상태코드 — 응답에 붙는 세 자리 숫자로 요청 결과를 알려줘요(200 성공 · 400 잘못된 요청 · 404 없음 · 409 충돌 · 500 서버 오류 등). CH05에서 자세히 다뤘어요.
😵 예외를 그냥 두면 — 불친절한 500 코드에서 예외가 터졌는데 아무도 잡지 않으면, 스프링은 기본적으로 500 Internal Server Error를 뱉어요. 클라이언트 입장에선 "서버가 고장났나?" 싶을 뿐, 무엇이 잘못됐는지 알 수 없어요. 예: 없는 회원을 조회했는데 500이 나오면, 클라이언트는 "이게 내 잘못(잘못된 id)인지, 서버 버그인지" 구분할 수 없어요. 이건 404(없음)여야 맞아요.
예외 처리 @ExceptionHandler @ControllerAdvice @Valid @NotBlank @Size 400 404
🧭
핵심 그림. "예외 → 적절한 상태코드로 변환", "잘못된 입력 → 검증으로 걸러 400". 이 두 가지만 잡으면 이번 장은 끝이에요. CH05에서 배운 상태코드가 여기서 실제로 쓰여요.
🛡️
예외 처리 — "사고 수습반"
이미 터진 문제(예외)를 잡아서 알맞은 상태코드로 바꿔 응답해요. (예: 없는 회원 → 404)
🚧
검증 — "입구 검문소"
잘못된 입력을 안으로 들어오기 전에 걸러내요. 규칙을 어기면 곧장 400을 돌려줘요.
🎯
@ExceptionHandler
특정 예외를 원하는 응답으로 변환
"이 예외가 나면 이렇게 응답해줘"를 정하는 거예요.

앞에서 "예외를 잡아 알맞게 응답하자"고 했죠. 그 "잡는 손"이 바로 @ExceptionHandler예요. 특정 예외가 터졌을 때 "그 예외는 내가 맡을게" 하고 나서서, 우리가 정한 상태코드와 메시지로 바꿔 돌려줘요.

🔤
@ExceptionHandler — 컨트롤러 안 메서드에 붙이는 어노테이션(표시)이에요. @ExceptionHandler(예외.class)라고 적으면 "이 예외가 나면 이 메서드를 대신 실행해줘"라는 뜻이 돼요. 놀이공원에서 "미아 발생 시 → 미아보호소 담당자가 대응"처럼, 특정 상황에 나설 전담 담당자를 지정해두는 셈이에요.
🎯 @ExceptionHandler — 예외를 잡아 응답으로 컨트롤러에서 특정 예외가 발생하면 그걸 잡아서, 원하는 상태코드와 메시지로 변환해요. 예를 들어 UserNotFoundException이 나면 404를 반환하도록 만들 수 있어요.
@RestController
public class UserController {

    @GetMapping("/users/{id}")
    public User find(@PathVariable Long id) {
        // 없으면 예외 발생
        return userService.findById(id);
    }

    // UserNotFoundException이 나면 → 404로 변환
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<String> handleNotFound(UserNotFoundException e) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND) // 404
                             .body(e.getMessage());
    }
}
🔤
커스텀 예외(Custom Exception) — 위의 UserNotFoundException처럼 상황을 콕 집어 우리가 직접 만든 예외예요. 이름만 봐도 "회원을 못 찾음"을 알 수 있어, 어떤 상태코드로 바꿀지 정하기 쉬워요.
🔤
ResponseEntity — 상태코드 + 헤더 + 본문(body)을 하나로 감싸서 응답을 직접 조립할 수 있게 해주는 타입이에요. ResponseEntity.status(404).body("...")처럼 원하는 코드와 메시지를 정확히 지정해 돌려줘요.
💥
예외 발생
없는 id 조회 →
UserNotFoundException
🎯
핸들러가 잡음
@ExceptionHandler가
그 예외를 가로챔
📤
에러 응답
ResponseEntity로
404 + 메시지 반환
💡
포인트. @ExceptionHandler(예외.class)가 붙은 메서드는 그 예외가 발생했을 때만 실행돼요. 여기서 ResponseEntity상태코드(404)메시지를 직접 정해 돌려줘요.
🌐
@ControllerAdvice
전역 예외 처리를 한 곳에 모으기
모든 컨트롤러의 공통 예외 처리를 중복 없이 관리해요.

그런데 컨트롤러가 수십 개라면, 매번 같은 @ExceptionHandler를 복사해 붙여야 할까요? 그러면 코드가 중복되고, 규칙을 바꿀 때 수십 군데를 다 고쳐야 해요. 그래서 예외 처리만 모아두는 전용 창구를 하나 만드는데, 그게 @ControllerAdvice예요.

🔤
전역 예외 처리 · @ControllerAdvice — 전역(全域)은 "앱 전체에 두루 적용된다"는 뜻이에요. 큰 백화점을 떠올려봐요. 매장(컨트롤러)마다 사고를 따로 처리하는 대신, 어느 매장에서 사고가 나든 한 곳(고객센터)에서 접수·처리하면 훨씬 깔끔하죠. @ControllerAdvice를 붙인 클래스가 바로 그 고객센터예요. 모든 컨트롤러의 예외를 이 한 곳으로 모아 처리해요.
🔤
@RestControllerAdvice — @ControllerAdvice"응답을 JSON으로 만들어 줘"(@ResponseBody)가 더해진 버전이에요. 요즘처럼 화면(HTML) 대신 데이터(JSON)를 주고받는 REST API에서는 이걸 주로 써요.
🌐 @ControllerAdvice — 전역(공통) 예외 처리 @ExceptionHandler를 컨트롤러마다 똑같이 쓰면 중복이 심해져요. @ControllerAdvice(또는 @RestControllerAdvice)를 붙인 클래스에 모아두면, 모든 컨트롤러에 공통으로 적용되는 전역 예외 처리기가 돼요.
@RestControllerAdvice   // 전역 예외 처리 (모든 컨트롤러 공통)
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<String> notFound(UserNotFoundException e) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); // 404
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> badRequest(IllegalArgumentException e) {
        return ResponseEntity.badRequest().body(e.getMessage()); // 400
    }
}
이렇게 하면 컨트롤러마다 예외 처리를 반복하지 않아요. 한 곳에서 정의하면 앱 전체에 적용돼 중복이 사라지고 일관성이 생겨요.
어노테이션적용 범위
@ExceptionHandler해당 컨트롤러 안에서만 예외 처리.
@ControllerAdvice모든 컨트롤러 공통(전역). 뷰(HTML) 반환에 적합.
@RestControllerAdvice전역 + JSON 응답용(@ControllerAdvice + @ResponseBody). REST API에 적합.
검증(Validation)
잘못된 입력은 들어오기 전에 걸러요
@Valid로 요청 객체를 검사하고, 실패하면 400을 돌려줘요.

지금까지는 이미 터진 문제(예외)를 수습하는 이야기였어요. 이제는 방향을 바꿔서, 아예 문제가 될 입력을 입구에서 막는 이야기예요. 공연장 입구의 검표원을 떠올려봐요. 표가 없거나 좌석이 틀린 사람을 입장 전에 걸러내면, 공연장 안은 늘 질서가 유지되죠. 검증(Validation)이 바로 이 검표원 역할이에요.

💡
예외 처리 vs 검증, 뭐가 다를까요? 검증은 잘못된 입력을 미리(입구에서) 걸러내는 예방이고, 예외 처리는 그래도 안에서 터진 문제를 사후에 수습하는 대응이에요. 둘 다 결국 클라이언트에게 알맞은 상태코드를 돌려주려고 존재해요.
✅ @Valid — 요청 객체 검증 요청으로 들어온 객체가 규칙에 맞는지 검사해요. 필드에 @NotNull·@NotBlank·@Size·@Email 같은 제약을 붙이고, 컨트롤러 파라미터에 @Valid를 붙이면 스프링이 자동으로 검증해줘요.
public class UserRequest {
    @NotBlank                     // 빈 문자열/공백 불가
    private String name;

    @Email                        // 이메일 형식이어야 함
    private String email;

    @Size(min = 8, max = 20)      // 길이 제약
    private String password;
}
🔤
검증(Validation) · Bean Validation — 검증은 들어온 값이 규칙에 맞는지 검사하는 일이에요. Bean Validation은 그 규칙을 필드 위에 @NotNull·@NotBlank·@Size·@Email 같은 어노테이션으로 선언하는 표준 방식이고, @Valid는 "이 규칙들로 검사해줘"라고 스프링에게 시키는 스위치예요.
🚦 검증 실패 → 400(Bad Request) 컨트롤러 메서드 파라미터에 @Valid를 붙이면, 요청이 규칙을 어겼을 때 스프링이 검증 실패로 처리하고 400 Bad Request를 돌려줘요.
@PostMapping("/users")
public User create(@Valid @RequestBody UserRequest request) {
    // 여기 도달하면 이미 검증을 통과한 상태
    return userService.save(request);
}
// name이 비었거나 email 형식이 틀리면 → 400 (Bad Request)
잘못된 입력은 비즈니스 로직에 닿기 전에 걸러지고, 클라이언트는 "요청이 잘못됐다"는 400을 받아요.
🔤
BindingResult — 검증 결과를 담아두는 상자예요. @Valid 뒤에 BindingResult 파라미터를 함께 두면, 스프링이 바로 400을 던지지 않고 "어떤 필드가 왜 틀렸는지"를 이 상자에 모아 넘겨줘요. 그러면 개발자가 그 내용을 꺼내 직접 응답 메시지를 꾸밀 수 있어요. 검표원이 "왜 입장 불가인지" 사유를 적어 건네주는 메모지라고 생각하면 돼요.
제약 어노테이션의미
@NotNullnull이면 안 됨.
@NotBlanknull·빈 문자열·공백이면 안 됨(문자열용).
@Size(min, max)길이(문자열·컬렉션)가 범위 안이어야 함.
@Email이메일 형식이어야 함.
🧩
정리
예외·검증 → 상태코드로 매핑
CH05에서 배운 상태코드가 여기서 실제로 쓰여요.
📥
요청
클라이언트가 API 호출
🛡️
검증·예외 처리
@Valid · @ExceptionHandler · @ControllerAdvice
📤
적절한 응답
200 / 400 / 404 / 409
상황돌려줄 상태코드
정상 처리200 OK / 201 Created
검증 실패(필수 값 누락 등)400 Bad Request
없는 자원 조회404 Not Found
중복 등 충돌409 Conflict
🔗
CH05와 연결. 예외 처리와 검증의 목적은 결국 "상황에 맞는 HTTP 상태코드"를 정확히 돌려주는 거예요. 예외 → 404/400/409, 검증 실패 → 400. CH05에서 배운 상태코드 지식이 여기서 실전으로 이어져요.
🧠 이 장 핵심 요약
📝
문제풀이 · 점검
배운 걸 가볍게 점검해봐요
시험이 아니라 "내가 이해했나" 확인용이에요. 틀려도 바로 해설이 나와요.
🧪
문제를 풀면 즉시 정답과 해설이 나오고, 위쪽 바에 점수가 쌓여요. 네 가지 유형(객관식 · O/X · 빈칸 · 개념)을 섞어 두었어요.
CH 10 🚀 실무 개념 — 프로파일·설정·로깅·배포까지 큰 그림 잡기