Skip to content

Commit f3744ee

Browse files
committed
✨ feat: 전역 예외 처리 기능 구현
각 도메인별 Advice에 중복으로 존재하던 예외 처리 로직을 한 곳에서 관리하고, 일관된 에러 응답을 제공하기 위해 전역 예외 처리 기능 개발 주요 변경 사항: - GlobalExceptionAdvice: @Valid 유효성 검사, IllegalArgumentException 등 애플리케이션 공통 예외를 처리하는 Advice 추가 - ApiCustomExceptionAdvice: 모든 도메인의 비즈니스 예외를 통합 처리하는 Advice 추가 - ErrorCode 인터페이스 및 CustomException 추상 클래스: 모든 커스텀 예외와 에러 코드를 표준화 - ApiResponse: ErrorCode 인터페이스를 받을 수 있는 error() 메소드를 추가
1 parent 0cf5727 commit f3744ee

File tree

5 files changed

+115
-0
lines changed

5 files changed

+115
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.grepp.spring.infra.error;
2+
3+
import com.grepp.spring.infra.error.exceptions.CustomException;
4+
import com.grepp.spring.infra.response.ApiResponse;
5+
import com.grepp.spring.infra.response.error.ErrorCode;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.core.annotation.Order;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.web.bind.annotation.ExceptionHandler;
10+
import org.springframework.web.bind.annotation.RestControllerAdvice;
11+
12+
@Slf4j
13+
@RestControllerAdvice(basePackages = "com.grepp.spring.app.controller.api")
14+
@Order(0)
15+
public class ApiCustomExceptionAdvice {
16+
17+
@ExceptionHandler(CustomException.class)
18+
public ResponseEntity<ApiResponse<Object>> handleCustomException(CustomException ex) {
19+
ErrorCode errorCode = ex.getErrorCode();
20+
log.warn("API CustomException occurred: code={}, message={}", errorCode.code(), errorCode.message());
21+
22+
return ResponseEntity
23+
.status(errorCode.status())
24+
.body(ApiResponse.error(errorCode));
25+
}
26+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.grepp.spring.infra.error;
2+
3+
import com.grepp.spring.infra.response.ApiResponse;
4+
import com.grepp.spring.infra.response.ResponseCode;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.http.ResponseEntity;
7+
import org.springframework.validation.BindException;
8+
import org.springframework.web.bind.MethodArgumentNotValidException;
9+
import org.springframework.web.bind.annotation.ExceptionHandler;
10+
import org.springframework.web.bind.annotation.RestControllerAdvice;
11+
12+
import java.util.HashMap;
13+
import java.util.Map;
14+
15+
@Slf4j
16+
@RestControllerAdvice
17+
public class GlobalExceptionAdvice {
18+
19+
@ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
20+
public ResponseEntity<ApiResponse<Map<String, String>>> validationExHandler(BindException ex) {
21+
Map<String, String> errors = new HashMap<>();
22+
ex.getBindingResult().getFieldErrors().forEach(error -> {
23+
String fieldName = error.getField();
24+
String errorMessage = error.getDefaultMessage();
25+
errors.put(fieldName, errorMessage);
26+
});
27+
log.warn("Validation error: {}", errors);
28+
return ResponseEntity
29+
.status(ResponseCode.BAD_REQUEST.status())
30+
.body(ApiResponse.error(ResponseCode.BAD_REQUEST, errors));
31+
}
32+
33+
@ExceptionHandler(IllegalArgumentException.class)
34+
public ResponseEntity<ApiResponse<String>> illegalArgumentExHandler(IllegalArgumentException ex) {
35+
log.warn("Illegal argument: {}", ex.getMessage());
36+
return ResponseEntity
37+
.status(ResponseCode.BAD_REQUEST.status())
38+
.body(ApiResponse.error(ResponseCode.BAD_REQUEST, ex.getMessage()));
39+
}
40+
41+
@ExceptionHandler(IllegalStateException.class)
42+
public ResponseEntity<ApiResponse<String>> illegalStateExHandler(IllegalStateException ex) {
43+
log.warn("Illegal state: {}", ex.getMessage());
44+
return ResponseEntity
45+
.status(ResponseCode.BAD_REQUEST.status())
46+
.body(ApiResponse.error(ResponseCode.BAD_REQUEST, ex.getMessage()));
47+
}
48+
49+
@ExceptionHandler(Exception.class)
50+
public ResponseEntity<ApiResponse<String>> allUncaughtExHandler(Exception ex) {
51+
log.error("Unhandled exception", ex);
52+
return ResponseEntity
53+
.status(ResponseCode.INTERNAL_SERVER_ERROR.status())
54+
.body(ApiResponse.error(ResponseCode.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다."));
55+
}
56+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.grepp.spring.infra.error.exceptions;
2+
3+
import com.grepp.spring.infra.response.error.ErrorCode;
4+
import lombok.Getter;
5+
6+
@Getter
7+
public abstract class CustomException extends RuntimeException {
8+
private final ErrorCode errorCode;
9+
10+
public CustomException(ErrorCode errorCode) {
11+
super(errorCode.message());
12+
this.errorCode = errorCode;
13+
}
14+
15+
public CustomException(ErrorCode errorCode, Exception e) {
16+
super(errorCode.message(), e);
17+
this.errorCode = errorCode;
18+
}
19+
}

src/main/java/com/grepp/spring/infra/response/ApiResponse.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.grepp.spring.infra.response;
22

3+
import com.grepp.spring.infra.response.error.ErrorCode;
34
import io.swagger.v3.oas.annotations.media.Schema;
45

56
@Schema(description = "공통 응답 DTO", name = "ApiResponse")
@@ -26,6 +27,10 @@ public static <T> ApiResponse<T> noContent() {
2627
return new ApiResponse<>(ResponseCode.OK.code(), ResponseCode.OK.message(), null);
2728
}
2829

30+
public static <T> ApiResponse<T> error(ErrorCode code) {
31+
return new ApiResponse<>(code.code(), code.message(), null);
32+
}
33+
2934
public static <T> ApiResponse<T> error(ResponseCode code) {
3035
return new ApiResponse<>(code.code(), code.message(), null);
3136
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.grepp.spring.infra.response.error;
2+
3+
import org.springframework.http.HttpStatus;
4+
5+
public interface ErrorCode {
6+
String code();
7+
HttpStatus status();
8+
String message();
9+
}

0 commit comments

Comments
 (0)