Spring Boot
[springboot 예외 처리 안티패턴 정리] Exception Handling Worst Practice
sendkite
2024. 12. 30. 01:05
- 개발자로 회사를 4곳을 거치며 다양한 예외처리 방식을 경험했다.
- 예외처리 방식의 정답은 없지만 틀린 답은 있었다. 해서는 안되는 안티패턴을 정리 해본다.
- 선행 지식 java error : https://yeoon.tistory.com/135
1. 클라이언트에게 검증 위임하기 (X)
- 서버의 입력 유효성 검사는 반드시 서버에서 이루어 져야한다.
- 실무에서 클라이언트에서 검증을 이미 진행한다는 이유로, 서버에서 입력 값 검증을 하지 않는 경우가 있었다.
- 유효성 검사를 넣으면, 개발 속도가 느려진다는 이유 였는데 이는 굉장히 위험하다.
- 사용자는 반드시 실수한다. 사용자는 알고도 잘못된 파라미터를 보낸다.
- 900만원 9000원으로 조작하여 구매한 이야기 : https://www.seoul.co.kr/news/newsView.php?id=20120419008045
- 개발자가 page size 1억 입력해서 DB 죽인 이야기
- 꼭 읽어보면 좋은 내용 - 개발자의 머피법칙 : https://techblog.woowahan.com/2645/
2. Exception, RuntimeException, Throwable, Error 직접 사용하기 (X)
- Java에서 제공하는 Error 객체를 그대로 사용하면 안된다.


- Exception 선언은 하위 RuntimeException, CheckedException이 섞이며, IDE에서 식별해야할 중요한 에러를 놓치게 만드는 안티패턴이다. (Throwable도 마찬가지)
// 메서드 레벨에 throws Exception이 선언 == 컴파일러가(IDE) checked exception 구분 못하게 만듬
@PostMapping("/products")
public void createProduct(@RequestBody CreateProductRequest request) throws Exception {
productService.productRegister(request);
}
- Checked Exception은 모던 개발에서 잘 사용하지 않는다.
- 분산환경에서 외부시스템이나 수백, 수천의 라이브러리의 checked exception이 구현 계층 메서드에 throws, try ~ catch 같은 의존성을 만드는데, 무수히 많은 checked exception으로 관리 불가
- 정말 필요한 경우에만 사용한다. 또는 RuntimeException으로 치환하고 공통 error handler을 구성한다. (ex. ControllerAdvice)

- Error 객체는 JVM의 low-level exception으로 시스템을 멈추게 한다. 구현 레벨에서 사용할 일이 없다. (ex. OutOfMemoryError, StackOverflowError)
- RuntimeException 역시 그대로 사용하지 않는다.
- HttpStatus 500으로 자동으로 치환된다.
- 어떤 문맥에서의 error인지, call stack 정보 추적이 어려운 문제가 있다.
- 사용할때는 추상 클래스로써 상속하여 사용하자.
class BusinessException(
val errorCode: ErrorCode,
val httpStatus: HttpStatus
) : RuntimeException(errorCode.message)
3. Exception 연쇄로 사용하지 않기 (x)
- Checked Exception을 Unchecked Exception을 치환 구현할때 필요시 cause를 넘기도록 만든다.
- 그래야 dedug할때 caused by … 로 코딩에 중요한 정보를 식별할 수 있다.
- 에러가 중요하다면 아래처럼 cause를 넘기는 것을 잊지 말자.

- 위 방법으로 Exception을 재귀적으로 만들지 않았다면 반드시 Apache Common Lang3의 ExceptionUtils.rootCause() 기능을 사용해서 root exception을 식별할 수 있는 조치를 취해놔야한다. (참고 : https://stackoverflow.com/questions/75512456/how-to-only-print-the-caused-by-line-of-a-java-exception)
4. HttpStatus 200으로 에러 알림하기 (x)
- REST API로 Error 상황에 httpStauts 200 OK로 응답하는 것은 최악의 실수 이다.
- (통신 자체는 성공이니 200으로 응답하고, error response로 error 정보를 return하는 방식)
- 프론트, 앱과 결합도가 생기며 변경하지 못하는 안티패턴이다.

- 200 OK와 함께 body error 객체를 넘기는 순간부터 HttpStatus를 활용하지 못한다.
- 2xx, 3xx, 4xx, 5xx 코드별 알림 보내기, 지표 집계 불가능
- 5xx 에러, CPU, 메모리 문제 같은 비즈니스에 영향 주는 애러, 배치 시스템 에러
- 비즈니스 지표에 이상이 생긴 경우 알림하기 어렵다.(ex, 주문에서 400대 error 임계점 초과시 알림 같은 것 구분 불가)
- 알림을 보내고자 한다면 보낼 수는 있겠지만, Advice 한곳에 작업하면 되는 코드를 수십, 수백곳에 적성해야한다.
- 2xx, 3xx, 4xx, 5xx 코드별 알림 보내기, 지표 집계 불가능
- 적어도 아래 5개의 HttpStatus Code는 구분해서 API를 설계하자. (HttpStatus Code 참고 : https://www.rfc-editor.org/rfc/rfc9110#section-15)

5. Exception 응답 형식으로 null, -9999, boolean 사용하기 (x)
- 2023년 7월 기준 RFC 9457(https://www.rfc-editor.org/rfc/rfc9457#name-members-of-a-problem-detail)에 따르면, HTTP API 실패시 응답에 대한 표준이 정의되어 있다. (표준이기 때문에 대부분 프레임워크들이 차용한다.)
- 아래와 같은 error 응답은 error detail 설명이 없는 안티패턴이다.
200 OK
{
"success": false, # 성공과 실패 여부만 알려주는 불친절한 flag. httpCode로 표현가능
"detail": null, # null을 응답하는 것은 프론트에서 null 처리를 하게 만드는 안티패턴 (과거 프로그래밍 언어의 유산)
"errorCode": -9999, # int 상수로 error에 대한 표준을 정하는 방식, 코드 정하는 시간이 추가적으로 들어가고 무엇을 의미하는지 가독성이 없다(표준화가 가능한 대기업만 적절한 방식)
}
- int, boolean 대신에 Enum을 사용해서 가독성 좋은 Error 응답을 구성한다. (enum이라 프론트에서 받아 처리하기도 좋다.)
400 Bad Request
{
"errorCode": "BAD_REQUEST", # Enum으로 구현 -9999 같은 int 상수 보다 가독성이 훨씬 좋다.
"detail": "Bad Request",
"errors": [ // 마틴파울러의 Notification 패턴
{
"field": "password",
"value": "",
"reason": "password is required"
},
...
- (구현 참고)
// Service 계층에서 RuntimeException 사용
val member = memberRepository.findByLoginId(loginId)
?: throw BusinessException(ErrorCode.MEMBER_NOT_FOUND, HttpStatus.NOT_FOUND)
// RuntimeException 상속하여 만든 BusinessException class
class BusinessException(
val errorCode: ErrorCode,
val httpStatus: HttpStatus
) : RuntimeException(errorCode.message) // 예시 exception은 cause가 필요 없어서 생략
// ErrorCode Enum
enum class ErrorCode(val message: String) {
BAD_REQUEST("Bad Request"),
UNAUTHORIZED("Unauthorized"),
FORBIDDEN("Forbidden"),
MEMBER_NOT_FOUND("Member not found"),
// Add other error codes as needed
}

결론
- 예외 처리에 정답은 없다.
- 입력 값은 반드시 검증하자. (값, type, range)
- int 상수, boolean 대신에 Enum을 클라이언트에게 넘겨서 가독성 좋고, 분기하기 좋게 만들자.
- 중요한 예외는 재귀적으로 cause를 넘기도록 만든다. 되어있지 않아면 root exception 식별 도움주는 도구를 도입하자.
- 표준을 만들고 따르자.
반응형