API 애플리케이션의 예외 설계 — Server/Client, ErrorCode, Resolver 기반 구조
· 16 min read
API 애플리케이션을 개발하다 보면, 예외 설계는 항상 뒤로 미루기 쉬운 주제다.
하지만 한 번 잘못 설계되기 시작하면, 이후 기능이 늘어날수록 예외 구조는 걷잡을 수 없게 복잡해지고,
결국 API의 응답 구조 · 로깅 · 모니터링까지 모두 영향을 받게 된다.
이 글에서는 먼저 _자주 범하는 예외 설계 패턴_을 살펴보고,
그 다음에 Server / Client 축과 ErrorCode enum, Resolver, Factory Method를 활용한
좀 더 견고한 예외 설계 방식을 정리해본다.
자주 범하는 예외 구조
실제 코드베이스에서 자주 볼 수 있는 구조는 대략 이런 모습이다.
// 모든 API 예외의 상위라고 주장하는 추상 클래스
public class ApiException extends RuntimeException {
public ApiException(String message) {
super(message);
}
}
// Auth 관련 예외
public class AuthException extends RuntimeException {
public AuthException(String message) {
super(message);
}
}
// User 관련 예외
public class UserException extends RuntimeException {
public UserException(String message) {
super(message);
}
}
처음에는 나쁘지 않아 보인다.
하지만 실제 구현이 진행되면 이런 식으로 세분화되기 쉽다.
public class InvalidLoginAccountException extends AuthException {
public InvalidLoginAccountException() {
super("invalid login account");
}
}
public class DeactivatedUserException extends AuthException {
public DeactivatedUserException() {
super("deactivated user");
}
}
public class NoUserException extends UserException {
public NoUserException(Long userId) {
super("no user. id=" + userId);
}
}
1) 예외가 도메인별로만 분기되어 있다
AuthException, UserException, OrderException처럼
“어느 도메인에서 발생했는지”에 따라 예외를 나누는 구조는 한눈에 보기에는 이해하기 쉽다.
하지만 HTTP 응답 관점에서는 곧 문제가 드러난다.
- 어떤
AuthException은 401(Unauthorized)로 내려가야 하고 - 어떤
AuthException은 403(Forbidden)일 수도 있고 UserException중 어떤 것은 400(Bad Request)이고- 어떤 것은 404(Not Found)일 수 있다.
즉, 도메인별 예외 구조만으로는 HTTP Status를 일관성 있게 매핑하기 어렵다.
글로벌 예외 핸들러에는 instanceof와 if/else가 쌓이기 시작한다.
