티스토리 뷰

java & spring

[Java/Spring] 예외 처리를 어떻게 할 것인가

신입사원 성장기 2024. 7. 24. 23:51

“예외를 어떻게 처리해야 효율적일까”에 대한 고민을 했습니다.

고려 사항은 다음과 같습니다.

  1. 어떤 예외가 발생하였는지 쉽게 알아볼 수 있을 것
  2. 너무 많은 Custom예외 클래스를 생성하여 관리가 복잡하지 않을 것
  3. 추가적으로 작성하는 예외에 대해 확장이 용이할 것
  4. 예외 처리를 전역적으로 관리할 수 있으면 좋겠다.
  5. 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의 정보를 사용하여 던지게 됩니다.

 

결론

처음 고려 사항을 리마인드 해보겠습니다.

  1. 어떤 예외가 발생하였는지 쉽게 알아볼 수 있을 것
  2. 너무 많은 Custom예외 클래스를 생성하여 관리가 복잡하지 않을 것
  3. 추가적으로 작성하는 예외에 대해 확장이 용이할 것
  4. 예외 처리를 전역적으로 처리할 수 있으면 좋겠다.
  5. 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의 필드를 이용하여 클라이언트에게 예외를 던질 때 상태코드, 에러 메시지, 에러를 유발한 필드와 값을 식별할 수 있도록 하였습니다.

최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday