ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SpringBoot] 2주차 예외 처리 및 Vaildation 적용
    Study/SpringBoot 2026. 3. 19. 15:02

    이번 주차에는 단순히 기능을 구현하는 것을 넘어, 서버가 에러를 어떻게 처리하고 사용자에게 전달할 것인지에 대한 구조적 설계를 진행했다.

    이번 주차 공부 내용

    • 공통 에러 응답 형식 작성(ApiResponse)
    • 전역 예외 처리 작용(@RestControllerAdvice)
    • 커스텀 예외 생성(Custom Exception)
    • 게시글 생성 DTO 작성(PostRequestDto)
    • @Vaild 적용(Bean Validation) 및 400 에러 테스트(HTTP 400 Bad Request)

     


    VScode -> IntelliJ 환경 이동과 차이 🌲

    VScode는 스터디를 하는 동안에는 크게 무리 없으나 백엔드 사용성과 프론트와 IDE를 분리해서 사용하는 게 더 현명할 것 같아 IntelliJ 환경 이동을 결정했다. 정착하면서 새로운 환경에 적응하는데 살짝 애를 먹었으나 백엔드 사용성, 확장성과 여러모로 유용한 기능이 많은 것 같았다. 따라서 다음과 같이 환경 차이에 대해 간단하게 정리했다.

    불편했던 점 (VS Code) 해결된 점 (IntelliJ) 가치
    라이브러리 설정 오류로 인한 삽질 Gradle 자동 동기화 불필요한 설정 시간 단축
    브라우저 테스트의 한계 (GET만 가능) 내장 HTTP Client API 작동 원리(POST/BODY) 이해
    파일 찾기 및 패키지 이동의 번거로움 강력한 검색 (Shift-Shift) 복잡해진 구조 관리 용이
    코드의 문맥 파악 어려움 Spring Context 인식 설계 구조(Advice, DTO 등) 파악 가능


    1. 라이브러리 관리의 직관성 (Gradle Sync)

    • VS Code에서는 build.gradle에 코드를 넣어도 라이브러리가 제대로 로드됐는지, 왜 빨간 줄이 뜨는지 파악하기가 어려웠다.
    • IntelliJ에서는 우측 상단에 뜨는 코끼리 아이콘(Gradle Sync) 하나로 모든 의존성을 즉시 동기화했다. 시각적 피드백이 가장 강력했던 것 같다.

    2. 백엔드 테스트 환경 (HTTP Client)

    • VS Code에서는 외부 도구인 Postman을 따로 켜거나, 복잡한 확장 기능을 설정해야 했다. 그래서 브라우저(GET 방식)로 테스트하다가 에러를 겪었다.
    • IntelliJ에서는 .http 파일 하나로 코드 바로 옆에서 POST 요청을 날릴 수 있었다. 별도의 프로그램 없이 IDE 안에서 요청(Request)과 응답(Response)의 전체 사이클을 확인하게 된 것이 공부 효율을 높여줬다.

    3. 어노테이션 프로세싱과 롬복(Lombok)

    • VS Code에서는 롬복 설정이 꼬이면 삽질하기 쉬웠다.
    • IntelliJ에서는 Enable annotation processing 설정 하나로 롬복을 지원한다.

    4. 스마트한 에러 추적 (Global Exception Handling)

    • 이점: 인텔리제이는 스프링의 전체 맥락을 이해한다. 특히 GlobalExceptionHandler가 실제로 어떤 컨트롤러와 연결되어 에러를 가로채고 있는지, 코드 옆 아이콘을 통해 시각적으로 보여준다.

     


    주요 구현 코드 및 사용된 문법 💻

    게시글 생성 API 실행 흐름 요약

    1단계: 요청 (The Request)

     사용자가 Postman이나 HTTP Client를 통해 데이터를 실어 보낸다.
    • 행동: POST /api/posts 주소로 JSON 데이터(title, content) 전송

    2단계: 검문 (The Validation)

    데이터가 컨트롤러 입구에 도착하자마자 @Valid 보안이 작동한다.
    • 체크: PostRequestDto에 적힌 규칙(@NotBlank, @Size)을 검사
    • 분기점:
      • 통과 시: 3단계(정상 로직)로 이동.
      • 탈락 시: 즉시 에러 발생 후 4단계(예외 처리)로 점프

    3단계: 성공 처리 (The Success)

    검문을 통과한 데이터만 컨트롤러 내부 코드를 실행
    • 행동: PostController가 실행되며 ApiResponse.onSuccess()를 호출
    • 결과: isSuccess: true가 담긴 봉투에 담겨 사용자에게 돌아간다.

    4단계: 가로채기 (The Exception Handling)

    검문에서 탈락하거나 코드 실행 중 문제가 생기면 중앙 관제가 나선다.
    • 행동: @RestControllerAdvice(GlobalExceptionHandler)가 에러를 낚아챈다.
    • 처리: 에러 메시지만 뽑아서 ApiResponse.onFailure() 봉투에 담는다.

    5단계: 응답 (The Response)

    성공이든 실패든, 사용자는 항상 똑같은 모양의 ApiResponse를 받게 된다.
    • 결과: 클라이언트(프론트엔드)는 이 봉투의 code만 보고 성공했는지 혹은 제목 여부를 바로 알 수 있다.

     

    공통 에러 응답 형식 작성(ApiResponse)

     서버가 보내는 모든 응답을 일정한 규격에 담는 표준 봉투이다.

    @Getter
    @AllArgsConstructor
    public class ApiResponse<T> {
        private final boolean isSuccess;
        private final String code;
        private final String message;
        private final T result;
    
        public static <T> ApiResponse<T> onSuccess(T result) {
            return new ApiResponse<>(true, "COMMON200", "요청에 성공하였습니다.", result);
        }
    
        public static <T> ApiResponse<T> onFailure(String code, String message, T result) {
            return new ApiResponse<>(false, code, message, result);
        }
    }

     

     

    💻 코드 설명 및 쓰인 문법 정리 💻

    public class ApiResponse<T>

     제네릭(<T>)을 사용하여 어떤 형태의 데이터(게시글, 유저 정보 등)도 담을 수 있게 설계했다.

    isSuccess 클라이언트가 응답을 받자마자 성공(true)인지 실패(false)인지 바로 판단하게 한다.
    code 단순 에러를 넘어 'COMMON400'처럼 구체적인 에러 종류를 문자로 알려준다.
    message 사용자나 개발자가 읽을 수 있는 설명 문구를 담는다.
    result 실제 전달할 데이터 내용물이 들어가는 자리이다.
    onSuccess(T result) 성공했을 때 객체를 쉽고 빠르게 만들기 위한 정적 메서드이다.
    return new ApiResponse... 성공 시에는 항상 'true'와 'COMMON200' 코드를 기본으로 세팅하여 돌려준다.
    onFailure(...) 실패 상황에서 에러 코드와 메시지를 담아 객체를 생성하는 메서드이다.
    isSuccess = false 실패 시에는 항상 'false'를 담아 명확히 에러임을 알린다.

     

     


    커스텀 예외 생성 (GeneralException)

     자바의 기본 에러가 아닌, 커스텀 상황에서 에러 상황을 정의하기 위한 기본 틀이다.

    @Getter
    @AllArgsConstructor
    public class GeneralException extends RuntimeException {
        private final String errorCode;
        private final String message;
    }

     

     

    💻 코드 설명 및 쓰인 문법 정리 💻

     

    extends RuntimeException 실행 중에 발생하는 예외로 취급하여, 별도의 throws 선언 없이도 어디서든 던질 수 있게 한다.
    errorCode 에러 발생 시 출력할 커스텀 고유 코드(예: POST4001)를 저장한다.
    message 에러의 구체적인 이유를 저장하여 나중에 응답에 포함시킨다.

     


    전역 예외 처리 적용 (@RestControllerAdvice)

     서버 어디선가 에러가 터지면 이를 가로채서 ApiResponse로 포장하는 중앙 관제탑이다.

    @RestControllerAdvice // 1
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(MethodArgumentNotValidException.class) // 2
        public ApiResponse<String> handleValidationException(MethodArgumentNotValidException e) { // 3
            String errorMessage = e.getBindingResult().getAllErrors().get(0).getDefaultMessage(); // 4
            return ApiResponse.onFailure("COMMON400", errorMessage, null); // 5
        }
    
        @ExceptionHandler(Exception.class) // 6
        public ApiResponse<String> handleAllException(Exception e) {
            return ApiResponse.onFailure("COMMON500", "서버 내부 오류가 발생했습니다.", e.getMessage()); // 7
        }
    }

     

     

    💻 코드 설명 및 쓰인 문법 정리 💻

    1. @RestControllerAdvice: 모든 컨트롤러에서 발생하는 예외를 이 클래스에서 집중 관리하겠다는 선언이다.
    2. @ExceptionHandler(...): 특정 에러(검증 실패)가 발생했을 때만 이 메서드가 작동하도록 연결한다.
    3. MethodArgumentNotValidException: @Valid 검증이 실패했을 때 자바가 던지는 에러의 이름이다.
    4. e.getBindingResult()...: 발생한 에러들 중 가장 첫 번째 에러 메시지(예: "제목은 필수입니다")를 뽑아낸다.
    5. ApiResponse.onFailure(...): 뽑아낸 에러 메시지를 커스텀해 만든 표준 실패 봉투에 담아 클라이언트에 보낸다.
    6. @ExceptionHandler(Exception.class): 위에서 처리하지 못한 예상치 못한 모든 에러를 마지막으로 받아내는 안전망이다.
    7. COMMON500: 알 수 없는 서버 내부 에러는 500번대 코드로 처리하여 응답한다.

     


    게시글 생성 DTO 작성(PostRequestDto)

    사용자가 보낸 데이터를 담고, 그 데이터가 올바른지 검사하는 규칙을 정하는 상자이다.

    @Getter @Setter
    public class PostRequestDto {
    
        @NotBlank(message = "게시글 제목은 필수입니다.")
        private String title;
    
        @NotBlank(message = "내용을 입력해주세요.")
        @Size(min = 10, message = "내용은 최소 10자 이상이어야 합니다.")
        private String content;
    }

     

    💻 코드 설명 및 쓰인 문법 정리 💻

    @NotBlank 제목이 null이거나, 비어있거나(""), 공백(" ")인 경우를 모두 차단하고 메시지를 띄운다.
    @NotBlank 내용 또한 반드시 입력되어야 함을 강제한다.
    @Size(min = 10) 글자 수가 10자 미만이면 저장되지 않도록 막아 데이터의 질을 유지한다.

     

     


    Valid 적용 및 400 에러 테스트

    실제로 컨트롤러 입구에서 검문하고 에러가 나는지 확인하는 단계다.

    @RestController
    public class PostController {
    
        @PostMapping("/api/posts")
        public ApiResponse<String> createPost(@Valid @RequestBody PostRequestDto request) {
            return ApiResponse.onSuccess("게시글이 성공적으로 생성되었습니다.");
        }
    }

     

     

    💻 코드 설명 및 쓰인 문법 정리 💻

     

    @PostMapping 데이터를 저장하는 'POST' 요청만 받도록 문을 설정했습니다.
    @Valid @RequestBody 들어오는 JSON 데이터를 DTO 객체로 바꾸면서, 동시에 DTO에 적힌 검증 규칙(@NotBlank 등)을 검사합니다.
    return ApiResponse.onSuccess(...) 검증을 무사히 통과한 데이터만 이 라인에 도달하여 성공 메시지를 받게 됩니다.

     

    🚀 400 에러 테스트 결과 확인 (HTTP 400 Bad Request)

    인텔리제이의 HTTP Client나 Postman으로 제목을 비워서 보내면 다음과 같은 흐름이 일어난다.

    1. 요청: {"title": "", "content": "짧음"} 전송.
    2. 검문: @Valid가 제목이 비었네?라며 에러를 던짐.
    3. 포착: GlobalExceptionHandler가 에러를 낚아채서 메시지를 추출.
    4. 응답: 클라이언트는 아래와 같은 JSON을 받게 되며, 이것이 성공적인 400 에러 테스트이다.
    {
      "isSuccess": false,
      "code": "COMMON400",
      "message": "게시글 제목은 필수입니다.",
      "result": null
    }

     

     


    실행 결과 🖼️

    실행 성공
    에러 유도


    트러블슈팅 🛠️

    [Issue1] 405 Method Not Allowed (COMMON500)
    • 현상: 브라우저 주소창에 URL을 입력하니 COMMON500 에러 발생.
    • 원인: 브라우저 주소창 입력은 GET 방식인데, 서버 컨트롤러는 POST만 받도록 설정됨.
    • 해결: 인텔리제이 내장 HTTP Client를 사용하여 명시적으로 POST 요청을 보냄으로써 해결.
    [Issue2] jakarta.validation 라이브러리 부재
    • 현상: @Valid, @NotBlank에 빨간 줄이 뜨고 import가 안 됨.
    • 원인: 스프링 부트 3.x 이상부터는 Validation 라이브러리가 기본 포함되지 않아 별도 추가가 필요함.
    • 해결: build.gradle에 spring-boot-starter-validation 의존성을 추가하고 Gradle 동기화.

제목 없는 코딩 블로그