우선, 웹 커스텀 예외를 설명하기에 앞서
HTTP Status Code 를 살펴볼 필요가 있다.
해당 내용들은 전부 MDN Web Docs 를 기반으로 작성할 것이다.
( 가장 저명하며, 모두가 볼 수 있는 공식 문서이기에 )
서버가 클라이언트 오류(예: 잘못된 요청 구문, 유효하지 않은 요청 메시지 프레이밍)를 감지해
요청을 처리할 수 없거나, 하지 않는다는 것을 의미하는 상태코드
상당히 모호하며 애매하다.
클라이언트 오류? 어디까지가 클라이언트의 오류이자 잘못일까?
서버가 요청받은 리소스를 찾을 수 없다는 것을 의미하는 상태코드
( 리소스가 영구적으로 삭제되었다면 410(Gone) 상태 코드가 쓰여야 한다 )410 : Gone
원본 서버에서 대상 리소스에 대해 더 이상 접근할 수 없으며,
상태가 영구적일 가능성이 있을 때 의미
( 상태가 일시적인지, 영구적인지 알 수 없으면 404를 사용해야 한다 )
이 역시도 상당히 모호하다.
영구적으로 삭제 되더라도 복구가 될 가능성이 있으면 410 를 쓰지 못하는가?
영구히 복구 안된다고 판단해도, 나중에 복구 가능한 로직이 추가되면, 변경 해야 하는가?
서버의 현재 상태와 요청이 충돌함을 의미하는 상태코드
( PUT 요청에 대응해 발생할 가능성이 가장 높다 )
대부분의 내용은 Version 이 있다고 가정할 때
새로 들어온 PUT 요청이 현재의 Version 보다 더 낮을시 발생한다라고 설명한다.
그러면, Conflict 라는 단어에 근거해 요청 간 충돌(DB에 이미 데이터 존재)을 처리하기 위해 사용하면 안되는가?
서버가 요청을 이해했고 문법도 올바르지만 요청된 지시를 처리할 수 없음을 나타내는 상태코드
이렇게 상태 코드는 명확한 것 같으나 몹시 애매하다
더 무식하게 생각하면
왜 모든걸 400으로 생각하지 않는가
해당 내용은 잠시 멈추고 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 만 사용하는게 가장 좋은거겠네?
=> 위와 비슷하게 내 생각에는 가장 좋다.
( 예약 시간이 존재하지 않을때, 테마가 존재하지 않을 때, 예약이 존재할때,예약 시간이 존재할때 ... )
과연 모든 상황을 커스텀 예외로 잡는게 의미가 있을지 + 관리가 가능할까?
도메인 별로 분리하면, 예외도 분리되고 별로 없을텐데??
-> 어차피 @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}`);
}
이런 경우, 프론트와 서버가 서로 명확하게 인지를 할 수 있게 상태 코드를 사용할 수 있다.
또한, 서로가 주고 받을 상태를 암시적으로 기대 가능하게 해준다.
브라우저,프록시,캐시
등을 위해서도 존재한다 - 여기서는 코드적인 접근 )그렇기에, 다소 모호한 상태 코드, 필요하지 않다면 필요해질때 까지 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
관련 미션 피드백