본문 바로가기
  • Let's study
BackEnd/Spring

[Spring] 예외 처리 | ServiceException과 전역 예외 처리

by 코딩고수이고파 2026. 9. 1.

1. 예외가 많아질 때 발생하는 문제

서비스를 구현하다 보면 다양한 예외 상황이 발생한다.

예를 들어 로그인만 하더라도 다음과 같은 경우가 있을 수 있다.

상황 예외
아이디가 존재하지 않음 NotFoundUsername
비밀번호가 일치하지 않음 MismatchPassword
블랙리스트 사용자 BlacklistUsername
인증 대기 상태 NotAuthentication

예외 상황마다 별도의 클래스를 만드는 것이 정석적인 방법이 될 수 있다.

하지만 작은 프로젝트에서 모든 예외 케이스마다 클래스를 만드는 것은 오히려 복잡도를 높일 수 있다.

따라서 프로젝트 규모에 따라 적절한 예외 처리 방식을 선택할 필요가 있다.

2. RsData를 이용한 응답 형식

RsData는 API 응답을 일정한 형식으로 전달하기 위해 사용하는 클래스이다.

public class RsData<T> {

    private String resultCode;
    private String msg;
    private T data;

    public RsData(String resultCode, String msg) {
        this.resultCode = resultCode;
        this.msg = msg;
        this.data = null;
    }
}
 

T를 제네릭으로 사용하면 RsData를 사용하는 시점에 data의 타입을 결정할 수 있다.

예외 응답처럼 별도의 데이터를 반환할 필요가 없는 경우에는 RsData<Void>로 사용할 수 있다.

3. ServiceException으로 서비스 예외 통일하기

작은 프로젝트에서는 서비스나 도메인에서 발생하는 예외를 ServiceException 하나로 통일하고, resultCode와 message를 통해 구체적인 예외 상황을 구분할 수 있다.

예를 들어 비밀번호가 일치하지 않는 경우 다음과 같이 예외를 발생시킬 수 있다.

if (!actor.getPassword().equals(password)) {
    throw new ServiceException(
        "401-1",
        "비밀번호가 일치하지 않습니다."
    );
}
 

이렇게 하면 별도의 MismatchPasswordException 클래스를 만들지 않아도 된다.

ServiceException은 다음과 같이 작성할 수 있다.

public class ServiceException extends RuntimeException {

    private RsData rsData;

    public ServiceException(String resultCode, String message) {
        super(message);

        this.rsData = new RsData(
            resultCode,
            message
        );
    }

    public String getResultCode() {
        return rsData.getResultCode();
    }

    public String getMsg() {
        return rsData.getMsg();
    }
}
 

예외 객체가 응답에 필요한 resultCode와 message를 함께 가지고 있도록 구성한 것이다.

4. GlobalExceptionHandler에서 예외 처리하기

컨트롤러에서 발생한 ServiceException을 각각 처리하지 않고, 전역 예외 처리기에서 한 번에 처리할 수 있다.

@ExceptionHandler(ServiceException.class)
@ResponseBody
public RsData<Void> handleException(ServiceException e) {
    return new RsData<>(
        e.getResultCode(),
        e.getMsg()
    );
}
 

@ExceptionHandler(ServiceException.class)를 통해 ServiceException이 발생했을 때 해당 메서드가 실행되도록 한다.

따라서 컨트롤러에서는 예외를 직접 응답으로 변환할 필요 없이 throw만 하면 된다.

Controller
    ↓
ServiceException 발생
    ↓
GlobalExceptionHandler
    ↓
RsData<Void> 형태로 응답

5. resultCode를 이용한 응답 코드 관리

resultCode를 "401-1"과 같은 형태로 관리하면 앞부분을 HTTP 상태 코드로 활용할 수 있다.

@JsonIgnore
public int getStatusCode() {
    return Integer.parseInt(
        resultCode.split("-")[0]
    );
}
 

예를 들어 다음과 같이 구분할 수 있다.

resultCode HTTP 상태 코드 의미
200-1 200 성공
201-1 201 생성 성공
400-1 400 잘못된 요청
401-1 401 인증 실패
404-1 404 리소스를 찾을 수 없음
500-1 500 서버 오류

-1, -2와 같은 뒷부분을 이용하면 같은 HTTP 상태 코드 안에서도 구체적인 상황을 구분할 수 있다.

6. 왜 모든 예외마다 클래스를 만들지 않을까?

예외 상황이 많아질수록 예외 클래스도 계속 증가한다.

NotFoundUsername
MismatchPassword
BlacklistUsername
NotAuthentication
...
 

작은 프로젝트에서는 이런 구조가 오히려 관리하기 어려울 수 있다.

따라서 프로젝트 규모가 작다면 다음과 같이 구성할 수 있다.

서비스/도메인 예외
        ↓
ServiceException 하나로 통일
        ↓
resultCode + message로 세부 상황 구분
        ↓
GlobalExceptionHandler에서 일괄 처리
 

반대로 프로젝트가 커지고 예외마다 서로 다른 처리 로직이나 추가 정보가 필요해진다면 구체적인 예외 클래스를 분리하는 것이 적절하다.

댓글