커스텀 예외
기본 예제 및 설명은 여기에서 확인하실 수 있습니다.
Spring에서 커스텀 예외가 필요한 이유
- 응답 일관성: 모든 실패를 같은 JSON 스키마로 반환
- 관심사 분리: Controller/Service는 throw만, 포맷팅은 전역 핸들러가 담당
- 테스트 용이성: 케이스별 예외 타입으로 분기 테스트가 쉬움
패키지 구조 예시
1
2
3
4
5
6
7
8
9
10
11
12
| global
├─ exception
│ ├─ ErrorCode.java
│ ├─ BusinessException.java
│ ├─ NotFoundException.java
│ ├─ UnauthorizedException.java
│ └─ GlobalExceptionHandler.java
└─ dto
└─ response
└─ ErrorResponseDto.java
domain
└─ user ...
|
에러 코드 설계(HTTP + 비즈니스 코드)
ErrorCode.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| @Getter
@RequiredArgsConstructor
public enum ErrorCode {
// 4xx
USER_NOT_FOUND(404, "U001", "존재하지 않는 사용자입니다."),
INVALID_CREDENTIALS(401, "A001", "이메일 또는 비밀번호가 올바르지 않습니다."),
VALIDATION_FAILED(400, "C001", "요청 값이 유효하지 않습니다."),
// 5xx
INTERNAL_ERROR(500, "S500", "서버 오류가 발생했습니다.");
private final int status;
private final String code;
private final String message;
}
|
베이스 예외 + 구체 예외
베이스 예외
BusinessException.java
1
2
3
4
5
6
7
8
9
| @Getter
public class BusinessExcetpion extends RuntimeException {
private final ErrorCode errorCode;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
|
구체 예외1️⃣
NotFoundException.java
1
2
3
4
5
| public class NotFoundException extends BusinessException {
public NotFoundException(ErrorCode errorCode) {
super(errorCode);
}
}
|
구체 예외2️⃣
UnauthorizedException.java
1
2
3
4
5
| public class UnauthorizedException extends BusinessException {
public UnauthorizedException(ErrorCode errorCode) {
super(errorCode);
}
}
|
사용 예
1
2
3
4
5
| // Service Layer
public User getUser(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND));
}
|
전역 예외 처리
ErrorResponseDto.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| @Getter
@RequiredArgsConstructor
public class ErrorResponseDto {
private final String code; // 비즈니스 코드(U001 등)
private final String message; // 사용자 메세지
private final int status; // HTTP 상태 코드
public static ErrorResponseDto of(ErrorCode ec) {
return new ErrorResponseDto(
ec.getCode(),
ec.getMessage(),
ec.getStatus()
)
}
}
|
GlobalExceptionHandler.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| @RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponseDto> handleBusiness(BusinessException ex) {
ErrorCode ec = ex.getErrorCode();
return ResponseEntity.status(ec.getStatus()).body(ErrorResponseDto.of(ec));
}
// 마지막 안전망
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponseDto> handleAll(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ErrorResponseDto.of(ErrorCode.INTERNAL_ERROR));
}
}
|
컨트롤러단 사용 예시
1
2
3
4
5
6
7
8
| @PostMapping("/login")
public void login(
@Valid @RequestBody LoginRequestDto requestDto
) {
if (!passwordMatches(requestDto)) {
throw new UnauthorizedException(ErrorCode.INVALID_CREDENTIALS);
}
}
|
클라이언트 응답 예시
1
2
3
4
5
| {
"code": "U001",
"message": "존재하지 않는 사용자입니다.",
"status": 404
}
|