-
[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 } }💻 코드 설명 및 쓰인 문법 정리 💻
- @RestControllerAdvice: 모든 컨트롤러에서 발생하는 예외를 이 클래스에서 집중 관리하겠다는 선언이다.
- @ExceptionHandler(...): 특정 에러(검증 실패)가 발생했을 때만 이 메서드가 작동하도록 연결한다.
- MethodArgumentNotValidException: @Valid 검증이 실패했을 때 자바가 던지는 에러의 이름이다.
- e.getBindingResult()...: 발생한 에러들 중 가장 첫 번째 에러 메시지(예: "제목은 필수입니다")를 뽑아낸다.
- ApiResponse.onFailure(...): 뽑아낸 에러 메시지를 커스텀해 만든 표준 실패 봉투에 담아 클라이언트에 보낸다.
- @ExceptionHandler(Exception.class): 위에서 처리하지 못한 예상치 못한 모든 에러를 마지막으로 받아내는 안전망이다.
- 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으로 제목을 비워서 보내면 다음과 같은 흐름이 일어난다.
- 요청: {"title": "", "content": "짧음"} 전송.
- 검문: @Valid가 제목이 비었네?라며 에러를 던짐.
- 포착: GlobalExceptionHandler가 에러를 낚아채서 메시지를 추출.
- 응답: 클라이언트는 아래와 같은 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 동기화.
'Study > SpringBoot' 카테고리의 다른 글
[SpringBoot] 6주차 페이지네이션(Pagination), N+1 문제 해결 (0) 2026.05.07 [SpringBoot] 5주차 JWT 인증/인가(회원가입 및 로그인) API 구현 (0) 2026.04.30 [SpringBoot] 4주차 게시판(수정, 삭제), 댓글(생성, 삭제) API 구현 및 Swagger 명세화 (1) 2026.04.02 [SpringBoot] 3주차 ERD 설계, Docker 연결 및 생성 조회 API 구현 (0) 2026.03.26 [SpringBoot] 1주차 JAVA 이해 및 ping API 구현 (0) 2026.03.10