스프링 API 공통 응답 처리하기

Ssol·2023년 1월 17일
0
post-thumbnail

API 공통 응답 포맷의 필요성

스프링에서 API 응답 방식은 보통 ResponseEntity 방식을 많이 사용하곤 한다. 데이터가 있을 때에는 문제 없이 잘 내려가지만 예외가 발생한다던가 하면 json으로 내려가지 않는 것을 본 적 있을 것이다.

일반 ResponseEntity 형식으로 반환하면 예외가 발생하는 경우 body가 json이 아닌 PlainText로 내려간다.

즉, 예외가 발생했을 때 응답의 모양이 달라지는 것!!

그리고 API의 작동 성공/실패 여부나 예외 에러메시지를 알 수 없이 DTO만 반환하면 서버 로그를 확인하기 전까진 아무 정보도 알 수 없을 것이다.

응답 데이터를 전달받는 주체가 사용하기 편하도록 성공했을 때와 실패했을 때 어떠한 처리 결과에도 동일한 포맷의 응답을 리턴하도록 통일시킬 필요가 있다.
또 API의 성공/실패에 따른 상태 메시지나 코드를 내려주게 되면 더욱 편하겠지?(HTTP 상태코드 말고 프로젝트 내에서 정한 약속 코드 같은 것 말이다.)

공통 응답 필드 만들기

개발자마다 선호하는 공통 응답 필드는 다를 수 있다.
이번 포스트에선 status, data, message를 사용해보겠다.

status: 응답 상태를 String으로 표시(Success, error 둘 중 하나 리턴)
data: 응답 결과 json(success일 경우 응답 데이터, error일 경우엔 null)
message: 예외일 경우 에러메시지 표시

HTTP 상태코드는 헤더에서 가져올 수 있으므로 따로 넣지 않았다.

공통 응답 클래스 예시

package com.test.apihandler.utils;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ApiResponse<T> {

    private static final String SUCCESS_STATUS = "success";
    private static final String FAIL_STATUS = "fail";
    private static final String ERROR_STATUS = "error";

    private String status;
    private T data;
    private String message;

    public static <T> ApiResponse<T> successResponse(T data) {
        return new ApiResponse<>(SUCCESS_STATUS, data, null);
    }

    public static ApiResponse<?> successWithNoContent() {
        return new ApiResponse<>(SUCCESS_STATUS, null, null);
    }

    public static ApiResponse<?> failResponse(BindingResult bindingResult) {
        Map<String, String> errors = new HashMap<>();

        List<ObjectError> allErrors = bindingResult.getAllErrors();
        for (ObjectError error : allErrors) {
            if (error instanceof FieldError) {
                errors.put(((FieldError) error).getField(), error.getDefaultMessage());
            } else {
                errors.put(error.getObjectName(), error.getDefaultMessage());
            }
        }
        return new ApiResponse<>(FAIL_STATUS, errors, null);
    }

    public static ApiResponse<?> errorResponse(String message) {
        return new ApiResponse<>(ERROR_STATUS, null, message);
    }

    private ApiResponse(String status, T data, String message) {
        this.status = status;
        this.data = data;
        this.message = message;
    }
}

공통 응답 클래스인 ApiResponse를 만들었다.
data 필드에는 어떠한 타입이라도 들어갈 수 있도록 제네릭을 사용해주자.

예외처리 핸들링 클래스 예시

package com.test.apihandler.utils.exception;

import com.openeg.openegscts.utils.ApiResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@RestControllerAdvice(basePackages = {"com.test.apihandler"})
public class ApiExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<?>> handleExceptions(RuntimeException exception) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResponse.errorResponse(exception.getMessage()));
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse<?>> handleValidationExceptions(BindingResult bindingResult) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResponse.failResponse(bindingResult));
    }
}

컨트롤러 사용 예시

@PutMapping("/info")
public ApiResponse<UserDto> editUserInfo(@RequestBody EditUserInfoDto editUserInfoDto) throws Exception {
    iAdminUserService.editUserInfo(editUserInfoDto);
    return ApiResponse.successResponse(UserDto.of(iAdminUserService.findUserInfo(editUserInfoDto.getUserId())));
}

서비스 사용 예시

@Override
public boolean editUserInfo(EditUserInfoDto editUserInfoDto) throws Exception {
    boolean result = iAdminUserMapper.editUserInfo(editUserInfoDto);
    if (!result) {
        throw new BindingException("id:'" + editUserInfoDto.getId + "' 업데이트 실패");
    }
    return true;
}

응답 결과 테스트

위와 같은 유저 수정 API를 만들어서 공통 응답을 테스트 해보자.
유저 수정이 성공했을 경우엔 수정된 UserDto가 공통 응답 클래스의 data에 싸여 나가게 될 것이고, 유저 수정이 실패했을 경우 공통응답 클래스의 message에 RuntimeException 에러메시지가 리턴된다.

성공 응답 결과

{
    "status": "success",
    "data": {
        "userId": "student01",
        "name": "김두한",
        "email": "fourdollars@yain.com",
        "phone": "010-4444-4444",
        "type": 1,
        "regDate": "2022-12-21 01:33:01",
        "groupId": 13,
        "classId": 6,
        "status": 1,
        "expireYn": "N",
        "cloudAccount": {
            "id": 178,
            "username": "am01-001",
            "password": "RRpJ024'iia+b=G",
            "url": "https://1234.signin.aws.amazon.com/console",
            "accessKeyId": "1234567890",
            "clientSecret": "-",
            "regDate": "2023-01-11 04:13:17"
        }
    },
    "message": null
}

실패 응답 결과

{
    "status": "error",
    "data": null,
    "message": "id: 'student01' 업데이트 실패"
}
profile
Junior Back-end Developer 🫠

0개의 댓글