웹 커스텀 예외 어디까지 해야할까?

영슈·2024년 5월 20일
1
post-thumbnail

우선, 웹 커스텀 예외를 설명하기에 앞서
HTTP Status Code 를 살펴볼 필요가 있다.

Status Code

해당 내용들은 전부 MDN Web Docs 를 기반으로 작성할 것이다.
( 가장 저명하며, 모두가 볼 수 있는 공식 문서이기에 )

400 : Bad Request

서버가 클라이언트 오류(예: 잘못된 요청 구문, 유효하지 않은 요청 메시지 프레이밍)를 감지해
요청을 처리할 수 없거나, 하지 않는다는 것을 의미하는 상태코드

상당히 모호하며 애매하다.
클라이언트 오류? 어디까지가 클라이언트의 오류이자 잘못일까?

404 : Not Found

서버가 요청받은 리소스를 찾을 수 없다는 것을 의미하는 상태코드
( 리소스가 영구적으로 삭제되었다면 410(Gone) 상태 코드가 쓰여야 한다 )

410 : Gone

원본 서버에서 대상 리소스에 대해 더 이상 접근할 수 없으며,
상태가 영구적일 가능성이 있을 때 의미
( 상태가 일시적인지, 영구적인지 알 수 없으면 404를 사용해야 한다 )

이 역시도 상당히 모호하다.
영구적으로 삭제 되더라도 복구가 될 가능성이 있으면 410 를 쓰지 못하는가?
영구히 복구 안된다고 판단해도, 나중에 복구 가능한 로직이 추가되면, 변경 해야 하는가?

409 : Conflict

서버의 현재 상태와 요청이 충돌함을 의미하는 상태코드
( PUT 요청에 대응해 발생할 가능성이 가장 높다 )

대부분의 내용은 Version 이 있다고 가정할 때
새로 들어온 PUT 요청이 현재의 Version 보다 더 낮을시 발생한다라고 설명한다.

그러면, Conflict 라는 단어에 근거해 요청 간 충돌(DB에 이미 데이터 존재)을 처리하기 위해 사용하면 안되는가?

422 : Unporecessable Content

서버가 요청을 이해했고 문법도 올바르지만 요청된 지시를 처리할 수 없음을 나타내는 상태코드

이렇게 상태 코드는 명확한 것 같으나 몹시 애매하다

  • 입력에 들어오는 매개 변수의 형식이 틀리다면 400인가 422인가?
  • 기존에 있는 id 나 제약조건에 대한 예외는 400인가 409인가?

더 무식하게 생각하면
왜 모든걸 400으로 생각하지 않는가

  • 로그인이 안되어 있어도 잘못된 요청 아냐?
  • 매개변수가 잘못 되어도 잘못된 요청 아냐?
  • 페이지를 잘못 찾아도 잘못된 요청 아냐?

Exception

해당 내용은 잠시 멈추고 IllegalArgument/State 와 CustomException 에 대해서 설명해보겠다.

@ExceptionHandler(value = IllegalArgumentException.class)  
public ResponseEntity<ErrorResponse> handleArgumentException(final IllegalArgumentException exception) {  
    return ResponseEntity.badRequest()  
            .body(new ErrorResponse(exception));  
}
public class ExistReservationException extends IllegalArgumentException {  
  
    public ExistReservationException(final ExceptionDomainType exceptionDomainType, final long id) {  
        super(String.format("%s ID %d에 해당하는 예약이 존재합니다.", exceptionDomainType.getMessage(), id));  
    }  
}

@ExceptionHandler(value = ExistReservationException.class)  
public ResponseEntity<ErrorResponse> handleExistReservationException(final ExistReservationException exception) {  
    return ResponseEntity.status(HttpStatus.BAD_REQUEST)  
            .body(new ErrorResponse(exception));  
}

위는 포괄적인 Illegal
아래는 예약이 존재하는 경우 삭제하는 경우를 잡기 위한 Custom Exception 이다.

커스텀예외를 통해
클래스 명을 통해 무슨 예외인지 명확히 파악 + 메시지 쉽게 생성 가능할 수 있다.

좋은 예외일까??
=> 내 생각으로는 몹시 좋지 않은 예외이다.

위에서 말한 장점들을 제외하고는 단점 밖에 존재하지 않는 코드이다.

  • 관리 포인트의 증가
  • 추가적인 로직을 하지 않음
  • 반환하는 상태 코드도 같음

그러면 결국 400 과 IllegalArgument / IllegalState Exception 만 사용하는게 가장 좋은거겠네?

=> 위와 비슷하게 내 생각에는 가장 좋다.

200
( 예약 시간이 존재하지 않을때, 테마가 존재하지 않을 때, 예약이 존재할때,예약 시간이 존재할때 ... )

과연 모든 상황을 커스텀 예외로 잡는게 의미가 있을지 + 관리가 가능할까?

도메인 별로 분리하면, 예외도 분리되고 별로 없을텐데??
-> 어차피 @ControllerAdvice 에서 관리 하니 이미 관리 포인트 증가!

상속 클래스를 통해 최상위 클래스만 catch 해서 분리하면 되는거 아니야?
-> 이미 Custom 의 의미가 퇴색되며, 모든 예외를 명확하게 그룹화 할 수 없다!

확장성을 위해 Custom Exception 을 만들면 되지 않을까 라고 생각할 수 있으나
여기서는 반대로 향한다. - 미리 생성을 하지 않았으므로 필요한 부분만 변경하지 않고 추가할 수 있게 된다.

근데 왜 상태 코드는 다양해졌는가

상태코드는 결국 웹 서버와 클라이언트의 통신에서 나오는 결과중 하나이다.

만약, 400 인데 다른 이유로 400을 발생했고, 파악을 해야 한다면?

public void deleteReservationTime(final long id) {  
    if (reservationRepository.existsByTimeId(id)) {  
        throw new IllegalArgumentException(String.format("삭제하려는 예약 시간 ID %d에 대한 예약이 존재합니다!",id));  
    }  
    if (reservationTimeRepository.deleteReservationTimeById(id) == 0) {  
        throw new IllegalArgumentException(String.format("%d에 대한 데이터가 존재하지 않습니다!"));  
    }  
}

클라이언트는 이 두개를 구분할 수 있는 방법이 없다.

if (!response.ok) {
	if (message.includes("예약이 존재합니다")) {
		console.error(`Error: ${message}`);
		alert(`Error: ${message}`);
	} else if (message.includes("데이터가 존재하지 않습니다")) {
		console.error(`Error: ${message}`);
		alert(`Error: ${message}`);
	}
}

상태코드가 같으므로, 메시지를 통해서만 구분을 하게 된다.

if (reservationTimeRepository.deleteReservationTimeById(id) == 0) {  
    throw new NotExistException(RESERVATION_TIME, id);  
}

@ExceptionHandler(value = NotExistException.class)  
public ResponseEntity<ErrorResponse> handleNotExistException(final NotExistException exception) {  
    return ResponseEntity.status(HttpStatus.NOT_FOUND)  
            .body(new ErrorResponse(exception));  
}
if (!response.ok) {
	const errorMessage = await response.text();
	if (response.status === 409) {
		console.error(`Conflict error: ${errorMessage}`);
		alert(`Error: ${errorMessage}`);
	} else if (response.status === 404) {
		console.error(`Not found error: ${errorMessage}`);
		alert(`Error: ${errorMessage}`);
}

이런 경우, 프론트와 서버가 서로 명확하게 인지를 할 수 있게 상태 코드를 사용할 수 있다.
또한, 서로가 주고 받을 상태를 암시적으로 기대 가능하게 해준다.

  • 경로에 대한 자원이 없을때는 404를 반환하겠지?
  • 제공한 매개변수의 형식이 틀릴때는 422를 반한하겠지?
    ( 물론, 추가적인 내용으로는 상태코드는 단순 서버-클라이언트를 위한게 아니라 브라우저,프록시,캐시 등을 위해서도 존재한다 - 여기서는 코드적인 접근 )

그렇기에, 다소 모호한 상태 코드, 필요하지 않다면 필요해질때 까지 400을 써도 상관없다!

결론

갑작스런 결론이지만

내 생각에는 결국 프론트가 로직 제어를 할 필요 ( 구분 편하기 위해 상태코드가 필요 ) 또는
서버가 예외에 대한 추가적인 로직 ( 로깅,Kafka 로 메시지 전송, ) 이 필요한게 아니라면 커스텀 예외는 필요없는 것 같다.

애초에, 서버-클라이언트의 기준에서
400이라는 상태코드가 나오게 된 경위를 알려줄 필요도, 알 필요도 없다.

클라이언트 단에서 필요해지면 그때 서버가 잘 생각해서 커스텀 예외를 만들고 분리를 하면 되는 것이다.
하지만, 거기서도 또 더욱 세밀한 제어가 필요해지면?

@ExceptionHandler(CustomException.class)
public ResponseEntity<ErrorResponse> handleCustomException(CustomException ex) {
	Map<String, Object> errorDetails = new HashMap<>();
	errorDetails.put("message", ex.getMessage());
	errorDetails.put("businessCode", ex.getBusinessCode());
	return ResponseEntity.status(HttpStatus.BAD_REQUEST)  
		.body(new ErrorResponse(ex,errorDetails));
}

예외에 상태코드를 담아서, 프론트 단에 전달해주면 된다!

예외에서 만큼은 과도한 오버 엔지니어링 및 확장성을 위해 미리 만들거나, 대비하지 말고
필요에 따라 생성하자

참고 자료

https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400
https://stackoverflow.com/questions/16133923/400-vs-422-response-to-post-of-data
https://stackoverflow.com/questions/77768346/what-is-the-purpose-of-using-http-status-codes-to-describe-rest-api-domain-error
관련 미션 피드백

0개의 댓글