[Spring] 공통 응답 처리, 예외처리 설정

icebox127·2026년 1월 11일

굿럭버디

목록 보기
2/3

API 응답 형식이 제각각이면 유지보수 시 문제가 생길 수 있고, 프론트 측에서 이해하기 힘들다. 따라서 API 응답을 통일하는것이 좋다.


내가 처음 작성했던 API 명세서..ㅎㅎ
API별로 응답 형식이 다 제각각이다.

API 응답 형식

API의 응답은 보통 아래의 형태를 가진다.

{
	"isSuccess" : false
	"code" : "MEMBER400"
	"message" : "회원 이름은 5글자 이하여야 합니다."
	"result": {
      "data": ""
    }
}
  • isSuccess : 요청 성공 여부
  • code : HTTP 상태 코드 외에 더 세부적인 결과를 알려주기 위해 사용 (예: 코드가 MEMBER400 일 시 -> 사용자 정보 관련 오류)
  • message : 상태코드에 대해 추가적 내용 설명
  • result : 결과 데이터를 반환 (GET 요청 등)

API 응답 통일


응답 통일을 위해, global 패키지 생성 후 하위에 code, exception, response 패키지를 만들었다.

1. Code

먼저 응답 코드를 생성하고 정의할 인터페이스와 클래스를 구현한다.
BaseErrorCode 인터페이스 작성

import org.springframework.http.HttpStatus;

public interface BaseErrorCode {
    HttpStatus getStatus();
    String getCode();
    String getMessage();
}

기본적으로 status, code, message 필드를 가질 예정이므로 각각의 getter 함수를 포함

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum GeneralErrorCode implements BaseErrorCode{

    BAD_REQUEST(HttpStatus.BAD_REQUEST,
            "COMMON400_1",
            "잘못된 요청입니다."),
    UNAUTHORIZED(HttpStatus.UNAUTHORIZED,
            "AUTH401_1",
            "인증이 필요합니다."),
    FORBIDDEN(HttpStatus.FORBIDDEN,
            "AUTH403_1",
            "요청이 거부되었습니다."),
    NOT_FOUND(HttpStatus.NOT_FOUND,
            "COMMON404_1",
            "요청한 리소스를 찾을 수 없습니다."),
    ;

    private final HttpStatus status;
    private final String code;
    private final String message;
}

enum 내부에 필드를 정의 후, @Getter와 @AllArgsConstructor로 필요한 함수 및 생성자를 생성할 수 있게 한다.
예: BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400_1", "잘못된 요청입니다.")와 같이 생성자를 호출하여 BAD_REQUEST라는 enum 상수를 생성

이 외에도 BaseErrorCode를 상속받는 여러 에러 파일을 도메인 별로 만들어 관리할 수 있다.
성공 응답도 이와 같은 양식으로 생성하면 된다.

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum GeneralSuccessCode implements BaseSuccessCode{

    OK(HttpStatus.OK,
            "SUCCESS200_1",
            "요청에 성공했습니다."),

    CREATED(HttpStatus.CREATED,
            "SUCCESS201_1",
            "리소스가 성공적으로 생성되었습니다."),

    NO_CONTENT(HttpStatus.NO_CONTENT,
            "SUCCESS204_1",
            "본문이 비어 있습니다.");

    private final HttpStatus status;
    private final String code;
    private final String message;
}

2. Response

이제 API 응답 형식을 구현한다.

@Getter
@AllArgsConstructor
@JsonPropertyOrder({"isSuccess", "code", "message", "result"})
public class ApiResponse <T>{
    @JsonProperty("isSuccess")
    private final Boolean isSuccess;

    @JsonProperty("code")
    private final String code;

    @JsonProperty("message")
    private final String message;

    @JsonProperty("result")
    private final T result;

    public static <T> ApiResponse<T> onSuccess(BaseSuccessCode code, T result) {
        return new ApiResponse<>(true, code.getCode(), code.getMessage(), result);
    }

    public static <T> ApiResponse<T> onFailure(BaseErrorCode code, T result) {
        return new ApiResponse<>(false, code.getCode(), code.getMessage(), result);
    }
}
  • @JsonPropertyOrder({"isSuccess", "code", "message", "result"})
    JSON 응답의 속성 순서를 지정한다.
  • @JsonProperty("isSuccess")
    JOSN 필드명을 명시적으로 저장

사용예시

controller에서 ApiResponse<result 타입>을 리턴하도록 하여 사용

@GetMapping("/test")
    public ApiResponse<TestResDTO.Testing> test() throws Exception {
        // 응답 코드 정의
        GeneralSuccessCode code = GeneralSuccessCode.OK;
        return ApiResponse.onSuccess(
                code,
                TestConverter.toTestingDTO("This is Test!")
        );
    }

3. Exception

예외 발생 시 그냥 throw new Exception으로 발생시키면 어느 컨트롤러에서 발생한 에러인지 구분하기 힘들다. 따라서 각 도메인별로 Exception을 정의하면 좋다.

@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException{
    private final BaseErrorCode code;
}

공통 Exception 양식인 GeneralException을 만든 뒤, 이를 상속하는 개별 예외를 만들면 된다.

public class TestException extends GeneralException {
    public TestException(BaseErrorCode code) {
        super(code);
    }
}

4. Error Handler

해당 에러를 발생시키면, 미리 정의해둔 code와 같은 형식으로 응답하지 않는다.
따라서 미리 만든 응답 양식인 ApiResponse를 반환하도록 에러 핸들러를 구현했다.

@RestControllerAdvice
public class GeneralExceptionAdvice {

    // 애플리케이션에서 발생하는 커스텀 예외 처리
    @ExceptionHandler(GeneralException.class)
    public ResponseEntity<ApiResponse<Void>> handleException(GeneralException ex){
        return ResponseEntity.status(ex.getCode().getStatus())
                .body(ApiResponse.onFailure(ex.getCode(), null));
    }

    // 그 외의 정의되지 않은 모든 예외 처리
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<String>> handleException(Exception ex){
        BaseErrorCode code = GeneralErrorCode.INTERNAL_SERVER_ERROR;
        return ResponseEntity.status(code.getStatus())
                .body(ApiResponse.onFailure(code, ex.getMessage()));
    }
}

앞서 커스텀 예외를 사용하기 위한 공통 양식인 GeneralException을 발생시켰으므로, 서버 내에서 설정한 예외를 GeneralException으로 찾을 수 있다.

@RestControllerAdvice

  • @ControllerAdvice
    @ExceptionHandler@ModelAttribute@InitBinder 가 적용된 메서드들에 AOP를 적용해 Controller 단에 적용하기 위해 고안된 어노테이션

  • @RestContollerAdvice
    @ControllerAdvice와 @ResponseBody를 합쳐놓은 어노테이션
    ControllerAdvice와 동일한 역할을 수행하고,  @ResponseBody를 통해 객체를 리턴 가능

기본적으로 모든 컨트롤러에서 발생한 에러에 대해 예외처리 되며, 특정 컨트롤러에서 발생한 에러에 대해서만 적용하도록 설정도 가능하다.
ApiResponse 객체를 응답해야하기 때문에 @RestControllerAdvice로 사용한다.

@ExceptionHandler

특정한 예외에 대해서만 처리하고 싶을 경우적용한다.

에러가 발생한 경우이므로 onFailure를 사용하고, 커스텀 예외의 경우 result가 없으며, 그냥 Excpetion의 경우 Internal Server Error라서 에러에 대한 정보를 추가로 표기하기 위해 result에 ex.getMessage()를 넣었다.

profile
감자의 공부기록🥔

0개의 댓글