티스토리 뷰
“예외를 어떻게 처리해야 효율적일까”에 대한 고민을 했습니다.
고려 사항은 다음과 같습니다.
- 어떤 예외가 발생하였는지 쉽게 알아볼 수 있을 것
- 너무 많은 Custom예외 클래스를 생성하여 관리가 복잡하지 않을 것
- 추가적으로 작성하는 예외에 대해 확장이 용이할 것
- 예외 처리를 전역적으로 관리할 수 있으면 좋겠다.
- HTTP 상태 코드 별로 예외를 분류하여 클라이언트에게 알릴 수 있으면 좋겠다.
위 5가지를 생각하였습니다.
이번 프로젝트에서 DDD(Domain Driven Design)을 적용하기로 하였기 때문에, 도메인 로직에 대한 예외 처리를 잘 해놓으면 좋겠다고 생각하였습니다.
따라서 도메인 로직을 수행하며 발생할 수 있는 예외 상황들에 대해 Custom 예외를 작성하고 싶은 마음이 컸습니다.
하지만 모든 Custom 예외를 별개의 클래스로 만들면 어떤 예외가 발생하였는지 쉽게 알아볼 수 있지만, 관리하기가 힘들겠다고 생각했습니다.
그래서 하나의 BusinessException 클래스를 다음과 같이 생성했습니다.
@Getter
public class BusinessException extends RuntimeException {
private final String invalidValue;
private final String fieldName;
private final HttpStatus httpStatus;
private final String message;
public BusinessException(Object invalidValue, String fieldName, ErrorCode errorCode) {
super(errorCode.getMessage());
this.invalidValue = invalidValue != null ? invalidValue.toString() : null;
this.fieldName = fieldName;
this.httpStatus = errorCode.getHttpStatus();
this.message = errorCode.getMessage();
}
}
멤버 변수로 예외가 발생한 필드명, 필드 값, 상태 코드, 예외 메시지를 가지도록 하였습니다.
BusinessException을 생성할 때, ErrorCode를 매개변수로 받도록 설계하였습니다.
ErrorCode는 enum으로 작성하였고, 다음과 같습니다.
@Getter
public enum ErrorCode {
// MultiGameRoom
GAME_NOT_START("게임이 아직 시작되지 않았습니다.", HttpStatus.BAD_REQUEST),
INSUFFICIENT_PLAYER("충분한 플레이어가 없어, 게임을 시작할 수 없습니다.", HttpStatus.BAD_REQUEST),
INVALID_PROBLEM("추가된 문제가 유효하지 않습니다.", HttpStatus.BAD_REQUEST),
PASSWORD_MISMATCH("방 비밀번호가 일치하지 않습니다.", HttpStatus.BAD_REQUEST),
USER_NOT_FOUND("방에서 해당 유저를 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
USER_NOT_HOST("방장만 게임을 시작할 수 있습니다.", HttpStatus.BAD_REQUEST),
// MultiGameService
ROOM_NOT_FOUND("방을 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
GAME_ALREADY_STARTED("게임이 이미 시작되었습니다.", HttpStatus.BAD_REQUEST),
PROBLEM_NOT_FOUND("문제를 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
MAX_PLAYERS_EXCEEDED("최대 참가자 수를 초과했습니다.", HttpStatus.BAD_REQUEST),
SUBMIT_TIME_EXCEEDED("제출 시간이 초과되었습니다.", HttpStatus.BAD_REQUEST);
private final String message;
private final HttpStatus httpStatus;
ErrorCode(String message, HttpStatus httpStatus) {
this.message = message;
this.httpStatus = httpStatus;
}
}
비즈니스 로직에 대한 예외를 정의해두고, 예외가 발생하는 상황에 미리 정의한 ErrorCode의 정보를 이용하여 BusinessException을 던지면 됩니다.
예시로는 다음과 같습니다.
if (this.password != null && !this.password.equals(roomPassword)) {
throw new BusinessException(this.password, "roomPassword", ErrorCode.PASSWORD_MISMATCH);
}
위 코드는 방 비밀번호가 틀릴 경우 예외가 발생하는 부분입니다.
BusinessException을 던질 때 ErrorCode의 PASSWORD_MISMATCH의 정보를 사용하여 던지게 됩니다.
결론
처음 고려 사항을 리마인드 해보겠습니다.
- 어떤 예외가 발생하였는지 쉽게 알아볼 수 있을 것
- 너무 많은 Custom예외 클래스를 생성하여 관리가 복잡하지 않을 것
- 추가적으로 작성하는 예외에 대해 확장이 용이할 것
- 예외 처리를 전역적으로 처리할 수 있으면 좋겠다.
- HTTP 상태 코드 별로 예외를 분류하여 클라이언트에게 알릴 수 있으면 좋겠다.
이렇게 하면 고려사항 1, 2, 3, 5를 만족할 수 있다고 생각합니다.
이제 4번을 만족하기 위해 RestControllerAdvice를 통해 전역적으로 예외 처리 하였습니다.
@RestControllerAdvice
public class BasicExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<String> handleBusinessException(final BusinessException e) {
return ResponseEntity.status(e.getHttpStatus())
.body(e.getMessage() + " " + e.getFieldName() + " : " + e.getInvalidValue());
}
}
BusinessException의 필드를 이용하여 클라이언트에게 예외를 던질 때 상태코드, 에러 메시지, 에러를 유발한 필드와 값을 식별할 수 있도록 하였습니다.
'java & spring' 카테고리의 다른 글
| [Spring/JPA] DTO 필드에 Entity를 사용하면 안되는 이유 (0) | 2023.08.02 |
|---|