Spring Boot로 백엔드 개발을 진행하면서 가장 먼저 느낀 문제는
API마다 에러 응답 형태가 제각각이라는 점이었다.
예를 들자면,
- 어떤 API는
"error" - 어떤 API는
"잘못된 요청입니다" - 어떤 API는 HTTP 상태코드만 내려줌
이게 처음에는 별 문제 없어 보였는데,
프로젝트를 조금 진행하다 보니까 바로 문제가 터졌다.
- 프론트엔드 개발자가 에러 상황을 설명하기 어려워했고
- 결국 “이 API는 왜 이런 응답이 와요?” 같은 대화가 계속 생겼다
- 나도 에러 원인을 보려면 해당 API 코드를 직접 들어가서 다 확인해야 했다
그래서!
일관된 예외 처리와 공통 응답 구조가 필요하다고 판단했다.
기존 방식의 한계
처음에는 그냥 간단하게 try-catch로 처리했다.
try {
// 비즈니스 로직
} catch (Exceptione) {
return"에러 발생";
}
근데 이게 진짜 빠르게 한계를 드러냈다.
- 예외가 발생할 때마다 try-catch를 계속 써야 하고
- 코드가 점점 중복되고
- 응답 형태도 계속 달라지고
- HTTP 상태코드도 제대로 활용 못 했다
무엇보다 제일 큰 문제는
코드가 너무 지저분해졌다.
try-catch가 여기저기 퍼지니까
나중에는 내가 짠 코드인데도 한눈에 안 들어왔다…😩
해결 전략
그래서 “이걸 어떻게 정리해야 하지?” 고민하다가
다음과 같은 방향으로 구조를 잡아야겠다고 생각했다.
- 예외 처리는 한 곳에서 모아서 처리하자 (중앙 집중화)
- 응답 형태는 무조건 통일하자
- 비즈니스 로직에서는 예외를 “던지기만” 하자
- HTTP 상태코드는 명확하게 쓰자
즉, “로직과 예외 처리를 분리하자” 였다.
내가 구성한 방식
나는 그래서 다음과 같이 정리하기로 했다.
ErrorCode: 에러 정의 (상태코드 + 메시지 + 에러 코드)CustomException: 비즈니스 예외GlobalExceptionHandler: 예외를 한 번에 처리ExceptionResponseDto: 응답 형식 통일
1. ErrorCode – 에러를 “정의”해서 사용하기
기존에는 단순 문자열로 에러를 처리했지만,
지금은 에러를 Enum으로 관리하도록 바꿨다.
→ 단순 메시지가 아니라
- HTTP 상태코드
- 에러 코드 (ex. USER_001)
- 메시지
까지 함께 묶어서 관리한다.
USER_NOT_FOUND(404,"USER_001","유저를 찾을 수 없습니다.")
이렇게 바꾸니까 확실히 달라진 점이 있었다.
- 에러가 어디서 어떻게 정의되는지 한눈에 보이고
- 프론트에서도
code기준으로 분기 처리가 가능해졌고 - 무엇보다 “에러를 그냥 문자열로 던지지 않게” 된다
👉→에러도 하나의 데이터처럼 관리하게 된 느낌이었다.
2. CustomException – 예외에 의미를 담기
서비스 로직에서는 이제 단순 Exception이 아니라
의미 있는 예외를 던지도록 했다.
throw new CustomException(ErrorCode.USER_NOT_FOUND);
이 방식이 좋았던 이유는 하나였다.
→ 코드만 봐도 어떤 상황인지 바로 이해된다.
굳이 메시지를 보지 않아도
“아, 유저 못 찾은 상황이구나”가 바로 읽힌다.
3. GlobalExceptionHandler – 예외는 한 곳에서 처리
예외는 컨트롤러에서 처리하지 않고
전역에서 한 번에 처리하도록 구성했다.
@ExceptionHandler(CustomException.class)
public ResponseEntity<ExceptionResponseDto> handleCustomException(CustomExceptione)
여기서 핵심은
→ 예외를 “처리”하는 곳을 하나로 모았다는 것
이렇게 하니까
- 컨트롤러에서 try-catch가 거의 사라졌고
- 응답 형태가 완전히 통일되었고
- 예외 흐름을 한 곳에서 관리할 수 있게 됐다
추가로
RuntimeExceptionValidation(@Valid)에러까지 같이 처리하면서
👉 “예외 처리의 끝은 여기다” 라는 느낌으로 만들었다.
4. 응답 구조 – 프론트와 약속 맞추기
응답은 다음과 같은 구조로 통일했다.
{
"status":400,
"message":"잘못된 요청입니다.",
"code":"POST_003",
"field":"title"
}
여기서 중요하게 생각한 건 두 가지였다.
- 프론트가 일관되게 처리할 수 있어야 한다
- 에러 원인을 추적할 수 있어야 한다
그래서
code→ 프론트 분기용field→ validation 위치 확인용
까지 포함시켰다.
→ 실제로 validation 에러까지 field로 내려주니까
프론트에서 UX 처리하기 훨씬 좋아졌다.
적용하고 나서 느낀 점
이 구조를 적용하고 나서 가장 크게 느낀 건
→ “예외 처리는 기능이 아니라 설계다” 라는 점이었다.
처음에는 단순히 에러를 잘 내려주기 위한 작업이라고 생각했는데,
막상 적용해보니까
- 코드가 훨씬 읽기 쉬워졌고
- 협업이 편해졌고
- 유지보수 난이도가 확 내려갔다
정리
내가 작성한 내용이 정답이라고는 생각하지 않는다.
하지만 더 많은 코드들을 보면서 직접 적용해보면 직관적이고 유지보수하기 좋은 개발을 할 수 있게 되지 않을까라는 생각이 들었다.
예외 처리는 나중에 붙이는게 아니라 처음부터 잘 설계하는 것이 프로젝트 진행시 더 빠르게 유지보수가 가능할 것이라는 생각이 들었다.