예외 처리 가이드
서비스 계층은 발생할 수 있는 모든 예외를 BizException 하나로 감싸 던지고, 전역 예외 처리기가 이를 표준
ResponseDto 포맷(successYn/messageCd)으로 변환해 응답한다.
결과 코드 (CodeApiResult)
Section titled “결과 코드 (CodeApiResult)”HTTP 상태 코드와 1:1로 매핑되는 enum이다. messageCd(클라이언트가 분기 처리하는 안정적인 코드값)로 응답에
실린다.
@Getter@AllArgsConstructorpublic enum CodeApiResult { SUCCESS(HttpStatus.OK), FAILED(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_PARAMETER(HttpStatus.BAD_REQUEST), SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR), NOT_FOUND(HttpStatus.NOT_FOUND),
PASSWORD_NOT_MATCH(HttpStatus.UNAUTHORIZED), NOT_LOGIN(HttpStatus.UNAUTHORIZED), ACCOUNT_LOCKED(HttpStatus.UNAUTHORIZED), FORBIDDEN(HttpStatus.FORBIDDEN), ;
private final HttpStatus httpStatus;}새 오류 상황이 필요하면 여기에 상수 하나를 추가하고, 어떤 HTTP 상태와 매핑할지만 정하면 된다.
공통 예외 (BizException)
Section titled “공통 예외 (BizException)”- 서비스 계층에서 발생하는 모든 비즈니스 예외를 감싸는 공통 예외 — 항상
CodeApiResult를 함께 들고 있다.common/exception/BizException.java @Getterpublic class BizException extends RuntimeException {@JsonIgnore private final Exception origin;private final CodeApiResult codeApiResult;private String message = "";/** 원인 예외 없이 SERVER_ERROR로 초기화한다 — 방어적 기본값. */public BizException() {origin = this;this.codeApiResult = CodeApiResult.SERVER_ERROR;}/** 다른 예외를 감싼다. 이미 BizException이면 그대로 이어받고, 아니면 SERVER_ERROR로 감싼다. */public BizException(final Exception e) {if (e instanceof BizException) {origin = ((BizException) e).getOrigin();codeApiResult = ((BizException) e).getCodeApiResult();message = e.getMessage();} else {origin = e;codeApiResult = CodeApiResult.SERVER_ERROR;if (StringUtils.isNotBlank(e.getMessage())) {this.message = e.getMessage();}}if (origin != this) {initCause(origin);}}/** 원인 예외 없이 지정한 결과 코드로 초기화한다 — 서비스 코드가 검증 실패 등을 직접 알릴 때 사용한다. */public BizException(final CodeApiResult error) {origin = this;this.codeApiResult = error;}} - 서비스 메소드에서 쓰는 두 가지 패턴
// 1) 조건 실패를 직접 알림boardRepository.findById(id).orElseThrow(() -> new BizException(CodeApiResult.NOT_FOUND));// 2) 그 외 모든 예외를 감쌈try {return boardRepository.save(entity);} catch (final Exception e) {throw new BizException(e);}
전역 예외 처리기 (RestApiExceptionAdvice)
Section titled “전역 예외 처리기 (RestApiExceptionAdvice)”@RestControllerAdvice(annotations = RestController.class)—@RestController전체에 적용되는 전역 예외 처리기다. 마지막exceptionHandler가 나머지 모든 예외를 잡는 catch-all이라, 이 어드바이스 범위 밖(필터 등)에서 발생하지 않는 한 Spring Boot 기본 에러 응답이 클라이언트로 새어나가지 않는다.common/exception/advice/RestApiExceptionAdvice.java @Slf4j@RestControllerAdvice(annotations = RestController.class)public class RestApiExceptionAdvice {@ExceptionHandler(BizException.class)public ResponseEntity<ResponseDto<Void>> bizExceptionHandler(final BizException e) {final CodeApiResult codeApiResult =Objects.requireNonNullElse(e.getCodeApiResult(), CodeApiResult.SERVER_ERROR);if (codeApiResult.getHttpStatus().is5xxServerError()) {log.error("### BizException Occurred. codeApiResult={}", codeApiResult, e.getOrigin());} else {log.warn("### BizException Occurred. codeApiResult={}", codeApiResult);}return ResponseEntity.status(codeApiResult.getHttpStatus()).body(new ResponseDto<>(codeApiResult));}@ExceptionHandler(MethodArgumentNotValidException.class)public ResponseEntity<ResponseDto<Void>> methodArgumentNotValidExceptionHandler(final MethodArgumentNotValidException e) {return ResponseEntity.badRequest().body(new ResponseDto<>(CodeApiResult.INVALID_PARAMETER));}@ExceptionHandler(Exception.class)public ResponseEntity<ResponseDto<Void>> exceptionHandler(final Exception e) {log.error("### Unhandled Exception Occurred.", e);return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ResponseDto<>(CodeApiResult.SERVER_ERROR));}}- 4xx(
NOT_FOUND/PASSWORD_NOT_MATCH등)는 정상적인 클라이언트 흐름이라WARN으로, 실제 서버 오류인 5xx만ERROR+ 원인 스택트레이스로 남겨 로그 노이즈를 줄인다. - 이 외에도
HttpMessageNotReadableException(JSON 파싱 실패),ConstraintViolationException(파라미터 제약 위반),MethodArgumentTypeMismatchException(@PathVariable/@RequestParam타입 불일치)을 각각 400 +INVALID_PARAMETER로 처리하는 핸들러가 있다.
응답 포맷 (ResponseDto)
Section titled “응답 포맷 (ResponseDto)”예외가 응답으로 바뀔 때는 successYn: "N" + messageCd(원인 예외가 BizException이면 그 codeApiResult
이름, 아니면 FAILED)로 실린다.
{ "successYn": "N", "messageCd": "NOT_FOUND"}public ResponseDto(final Exception e) { successYn = "N"; if (e instanceof BizException biz) { messageCd = biz.getCodeApiResult().name(); } else { messageCd = CodeApiResult.FAILED.name(); }}필터 단계 예외 (JsonErrorController)
Section titled “필터 단계 예외 (JsonErrorController)”ErrorController를 직접 구현한 빈을 등록하면ErrorMvcAutoConfiguration이@ConditionalOnMissingBean으로 물러나 기본BasicErrorController가 아예 등록되지 않는다 — 이를 이용해 필터 단계 예외도 표준ResponseDtoJSON으로 응답하게 만든다.- 로그인 여부와 무관하게 항상 원래 상태 코드로 응답해야 하므로
@PublicApi를 붙인다.