팀 프로젝트를 진행할 때, API마다 응답 형식이 다르다면, 프론트 측에서 혼란스러울 것이다. 이를 방지하기 위해 API 응답 통일을 해준다.
API 응답은 다음과 같은 형식을 따른다
{
isSuccess : Boolean
code : String
message : String
result : {응답으로 필요한 또 다른 json} //실패한 경우(isSuccess : false) null값 반환
}
파일 구조는 프로젝트 파일 안에 위와 같은 형식으로 만들어주었다.
status(ErrorStatus, SuccessStatus) : 응답 상세enum 작성
BaseCode, BaseErrorCode : 인터페이스, status에 있는 파일들은 이 메소드를 반드시 override할 것
ErrorReasonDTO, ReasonDTO : 응답형식 DTO
ApiResponse : API응답에 대한 클래스 (응답 형식 지정)
*exception은 아래 에러핸들링에서 다룰 예정이다.
API 응답 형식을 지정해주는 클래스이다.
@Getter
@AllArgsConstructor
@JsonPropertyOrder({"isSuccess", "code", "message", "result"}) //JSON 속성의 순서 지정
public class ApiResponse<T> {
@JsonProperty("isSuccess") //JSON에서 해당 속성의 이름 정의
private final Boolean isSuccess;
private final String code;
private final String message;
@JsonInclude(JsonInclude.Include.NON_NULL) //NULL이 아닐때만 result 반환, JSON에 포함 여부 결정(ALWAYS, NON_NULL, ABSENT, NON_EMPTY)
private T result;
// 성공한 경우
public static <T> ApiResponse<T> onSuccess(T result) {
return new ApiResponse<>(true, SuccessStatus._OK.getCode(), SuccessStatus._OK.getMessage(), result);
}
//특정 성공 상태 코드가 필요할 때
public static <T> ApiResponse<T> of(BaseCode code, T result){
return new ApiResponse<>(true, code.getReasonHttpStatus().getCode() , code.getReasonHttpStatus().getMessage(), result);
}
// 실패한 경우
public static <T> ApiResponse<T> onFailure(String code, String message, T data) {
return new ApiResponse<>(false, code, message, data);
}
public static <T> ApiResponse<T> ofFailure(BaseErrorCode code, T result) {
return new ApiResponse<>(false, code.getReasonHttpStatus().getCode(), code.getReasonHttpStatus().getMessage(), result);
}
}
@Getter
@AllArgsConstructor
public enum ErrorStatus implements BaseErrorCode {
//일반적인 응답
_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
_BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."),
_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다. 로그인 정보를 확인해주세요."),
_FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."),
_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMON404", "요청한 리소스를 찾을 수 없습니다"),
//Member 에러
MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "해당하는 사용자를 찾을 수 없습니다.");
private final HttpStatus httpStatus;
private final String code;
private final String message;
@Override
public ErrorReasonDTO getReason() {
return ErrorReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(false)
.build();
}
@Override
public ErrorReasonDTO getReasonHttpStatus() {
return ErrorReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(false)
.httpStatus(httpStatus)
.build();
}
}
errorCode를 설정할 때, 어떤 파트의 에러인지 작성해주면 프론트측에서 알아보기 쉽다. 예를 들어 멤버와 관련된 에러이면, MEMBER4000,MEMBER4001...이런식으로 뒤에 숫자를 순서대로 붙여주었다.
@Getter
@AllArgsConstructor
public enum SuccessStatus implements BaseCode {
//일반적인 응답
_OK(HttpStatus.OK, "COMMON200", "성공입니다.");
private final HttpStatus httpStatus;
private final String code;
private final String message;
@Override
public ReasonDTO getReason() {
return ReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(true)
.build();
}
@Override
public ReasonDTO getReasonHttpStatus() {
return ReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(true)
.httpStatus(httpStatus)
.build();
}
}
@Getter
@Builder
public class ReasonDTO {
private HttpStatus httpStatus;
private final boolean isSuccess;
private final String code;
private final String message;
}
ReasonDTO, ErrorReasonDTO 둘 다 코드가 똑같아서 그냥 ReasonDTO 하나로 통일시켜주어도 될 것 같다. (아래 ErrorReasonDTO를 ReasonDTO로 모두 변경)
public interface BaseCode {
public ReasonDTO getReason(); //일반적인 이유 반환
public ReasonDTO getReasonHttpStatus(); //HTTP 상태 코드와 함께 이유를 반환
}
public interface BaseErrorCode {
public ErrorReasonDTO getReason();
public ErrorReasonDTO getReasonHttpStatus();
}
이렇게 하면 ApiResponse는 완성이다.
SpringBoot에서 발생하는 예외와 위 ErrorStatus처럼 커스텀 해준 예외를 처리해준다.

GeneralException : exception의 종류를 지정해줌
ExceptionAdvice : Spring Boot 애플리케이션에서 발생하는 예외를 전역적으로 처리하기 위한 클래스
ExceptionHandler : 특정한 예외를 처리하기 위한 커스텀 예외 클래스
@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException{
private BaseErrorCode code; //Api 응답 통일 시 작성해주었던 BaseErrorCode
public ErrorReasonDTO getErrorReason() {
return this.code.getReason();
}
public ErrorReasonDTO getErrorReasonHttpStatus(){
return this.code.getReasonHttpStatus();
}
}
@RestControllerAdvice
: Spring의 어노테이션으로, RestController에서 발생하는 예외를 처리하는 데 사용. annotations = {RestController.class}는 이 어드바이스가 @RestController가 붙은 클래스에서만 작동하도록 지정(@RestController는 컨트롤러에 붙음)
@Slf4j
@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler{
//유효성 검사에서 제약 조건이 위반되었을 때 발생하는 예외
@org.springframework.web.bind.annotation.ExceptionHandler
public ResponseEntity<Object> validation(ConstraintViolationException e, WebRequest request) {
String errorMessage = e.getConstraintViolations().stream()
.map(constraintViolation -> constraintViolation.getMessage())
.findFirst()
.orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생"));
return handleExceptionInternalConstraint(e, ErrorStatus.valueOf(errorMessage), HttpHeaders.EMPTY,request);
}
//메서드 인자가 유효하지 않을 때 발생
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException e, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
Map<String, String> errors = new LinkedHashMap<>();
e.getBindingResult().getFieldErrors().stream()
.forEach(fieldError -> {
String fieldName = fieldError.getField();
String errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse("");
errors.merge(fieldName, errorMessage, (existingErrorMessage, newErrorMessage) -> existingErrorMessage + ", " + newErrorMessage);
});
return handleExceptionInternalArgs(e,HttpHeaders.EMPTY,ErrorStatus.valueOf("_BAD_REQUEST"),request,errors);
}
//일반적인 예외 처리
@org.springframework.web.bind.annotation.ExceptionHandler
public ResponseEntity<Object> exception(Exception e, WebRequest request) {
e.printStackTrace();
return handleExceptionInternalFalse(e, ErrorStatus._INTERNAL_SERVER_ERROR, HttpHeaders.EMPTY, ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(),request, e.getMessage());
}
@ExceptionHandler(value = GeneralException.class)
public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) {
ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
return handleExceptionInternal(generalException,errorReasonHttpStatus,null,request);
}
//예외 처리 기본 로직
private ResponseEntity<Object> handleExceptionInternal(Exception e, ErrorReasonDTO reason,
HttpHeaders headers, HttpServletRequest request) {
ApiResponse<Object> body = ApiResponse.onFailure(reason.getCode(),reason.getMessage(),null);
WebRequest webRequest = new ServletWebRequest(request);
return super.handleExceptionInternal(
e,
body,
headers,
reason.getHttpStatus(),
webRequest
);
}
private ResponseEntity<Object> handleExceptionInternalFalse(Exception e, ErrorStatus errorCommonStatus,
HttpHeaders headers, HttpStatus status, WebRequest request, String errorPoint) {
ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorPoint);
return super.handleExceptionInternal(
e,
body,
headers,
status,
request
);
}
private ResponseEntity<Object> handleExceptionInternalArgs(Exception e, HttpHeaders headers, ErrorStatus errorCommonStatus,
WebRequest request, Map<String, String> errorArgs) {
ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorArgs);
return super.handleExceptionInternal(
e,
body,
headers,
errorCommonStatus.getHttpStatus(),
request
);
}
private ResponseEntity<Object> handleExceptionInternalConstraint(Exception e, ErrorStatus errorCommonStatus,
HttpHeaders headers, WebRequest request) {
ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), null);
return super.handleExceptionInternal(
e,
body,
headers,
errorCommonStatus.getHttpStatus(),
request
);
}
}
public class ExceptionHandler extends GeneralException {
public ExceptionHandler(BaseErrorCode code) {
super(code);
}
}
BaseErrorCode 객체를 매개변수로 받아, 상위 클래스인 GeneralException의 생성자를 호출한다. super(code);를 통해 ExceptionHandler클래스는 GeneralException과 동일한 방식으로 예외를 처리한다.
사용 예시
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new ExceptionHandler(MEMBER_NOT_FOUND))
//new ExceptionHandler(ErrorStatus에 있는 에러enum값)
이렇게 API 응답형식 통일과 예외 처리를 해주었다 !
<참고사이트>
https://velog.io/@ddeo99/Spring-Boot-API-응답-통일-Error-Handling
https://velog.io/@koojun99/SpringBoot-API-응답-통일과-예외-처리