본문 바로가기
카테고리 없음

[Spring] 예외 처리 CustomException 구조

by sujupark54 2026. 2. 22.

개발을 하다 보면 예외는 반드시 발생한다. 문제는 예외 자체가 아니라, 그 예외를 어떻게 처리하느냐이다. try~catch를 곳곳에 흩뿌려두면 코드는 빠르게 지저분해지고 비즈니스 로직의 흐름도 한눈에 들어오지 않는다.

특히 게시글 조회, 수정, 삭제 같은 도메인에서는 존재하지 않는 데이터, 권한이 없는 사용자 접근과 같은 예외 상황이 반복적으로 등장한다. 이런 경우를 try~catch로 매번 처리하는 대신, 한 곳에서 일관되게 관리하는 것이 훨씬 낫다.

이 글에서는 CustomException과 ErrorCode, 그리고 ExceptionHandler를 활용해 예외를 도메인 중심으로 정리하고 컨트롤러를 깔끔하게 유지하는 방법을 살펴본다.


예외 처리 CustomException과 ErrorCode

가장 먼저 해야 할 일은 예외를 도메인 언어로 정의하는 것이다. 게시글이 없을 때, 권한이 없을 때, 이런 상황을 단순히 RuntimeException으로 던지면 의도가 코드에 드러나지 않는다.

그래서 모든 커스텀 예외의 최상위로 AppException을 두고, 에러의 의미를 ErrorCode로 분리한다. 예외는 “무슨 일이 발생했는지”를 표현하고, ErrorCode는 “클라이언트에게 무엇을 알려줄지”를 책임진다.

PostNotFoundByIdException처럼 구체적인 예외를 만들면 서비스 코드는 읽기 쉬워진다. postRepository.findById에서 값이 없으면 바로 의미 있는 예외를 던지고, 이후 로직은 성공 케이스만 신경 쓰면 된다.

ErrorCode를 enum으로 관리하면 메시지와 HTTP 상태 코드를 한 곳에서 통제할 수 있다. 메시지가 중복되지 않고, 에러 정책을 변경할 때도 영향 범위를 쉽게 파악할 수 있다. 이 구조의 핵심은 예외를 기술적인 문제가 아니라 도메인 사건으로 다룬다는 점이다.


예외 처리 Service 계층에서의 역할

예외를 던지는 위치는 컨트롤러가 아니라 서비스 계층이다. 컨트롤러는 요청을 받고 응답을 돌려주는 역할만 해야 한다. 비즈니스 규칙을 아는 곳은 서비스다.

게시글을 수정하려면 유저가 존재하는지, 게시글이 존재하는지, 권한이 있는지를 차례대로 검증해야 한다. 이 검증 과정에서 실패하면 즉시 예외를 던지고 흐름을 중단한다.

loadUserFrom, loadPostFrom 같은 메서드로 검증 로직을 분리하면 중복 코드도 줄어들고 실수할 여지도 줄어든다. 예외를 반환값으로 처리하지 않고 throw로 바로 끊어주는 것이 핵심이다.

이 방식의 장점은 정상 흐름과 예외 흐름이 명확히 분리된다는 점이다. 성공 케이스 로직을 읽을 때 예외 처리를 신경 쓰지 않아도 되고, 실패 케이스는 예외 타입만 봐도 어떤 문제가 발생했는지 알 수 있다.


예외 처리 ExceptionHandler의 역할

예외를 던졌다면 어딘가에서는 반드시 받아야 한다. 그 역할을 담당하는 것이 ExceptionHandler다. 컨트롤러마다 try~catch를 두는 대신, @RestControllerAdvice로 전역에서 처리한다.

ExceptionHandler는 예외를 HTTP 응답으로 변환하는 계층이다. 어떤 예외인지에 따라 상태 코드와 메시지를 결정하고, 클라이언트에게 전달한다. 이 과정에서 로깅도 함께 처리한다.

PostNotFoundByIdException처럼 구체적인 예외를 캐치하면 메시지를 조합하거나 추가 정보를 포함시키기도 쉽다. 에러 응답 포맷을 통일하면 프론트엔드와의 협업도 수월해진다.

결국 이 구조의 목적은 단순하다. 비즈니스 로직은 깔끔하게, 예외 처리는 일관되게. try~catch를 줄이고 도메인 중심의 예외 구조를 만들면 코드는 훨씬 읽기 쉬워지고 유지보수 비용도 크게 줄어든다.