Spring boot Response 형식 만들기

짱J·2022년 7월 20일
5

Spring Boot

목록 보기
5/7
post-thumbnail

클라이언트와 API를 이용하기 위해서는, 통일된 API response 형식이 필요하다.
당연히 이번에 하는 UMC 앱런칭 프로젝트에서도 통일된 API response 형식이 필요하다.
리드 개발자이지만 JPA 초보인 나는 Response 형식을 어떻게 구현할지 잘 몰랐지만 🥲 우리의 멋진 팀원인 메이슨이 Response 형식 관련 코드를 작성해주는 작업을 해주었다!

하지만 코드를 이해하지 못하고 가져다 쓰는건 No no...! 메이슨이 작성해준 코드를 하나씩 분석해보며 Response 형식을 어떻게 만드는지 공부해보자.

🌷 프로젝트 구조

📁 base
	ㄴ GeneralException.java
    ㄴ 📁 constant
    	ㄴ Code.java
    ㄴ 📁 dto
    	ㄴ DataResponseDto.java
        ㄴ ErrorResponseDto.java
        ㄴ ResponseDto.java
📁 config
	ㄴ ExceptionHandler.java
📁 user/controller
	ㄴ UserController.java

🌷 Response 형식

코드를 본격적으로 보기 앞서, 이해를 돕기 위해 결과를 먼저 보고 시작하자.
Response는 크게 success, code, message, data 4가지 파트로 나뉘어져있다.

  • success - Response 응답 성공 여부
    • 성공하면 true, 실패하면 false
  • code - Response 응답 코드
    • 성공하면 0, 실패하면 실패 원인을 코드 번호로 표현
    • 코드 번호는 Code.java에서 확인 및 편집 가능
  • message - Response 응답 메시지
    • 성공하면 "Ok", 실패하면 실패 원인을 텍스트로 표현
    • Code.java에서 텍스트 확인 및 편집 가능
  • data - Response 응답 결과가 담길 공간
    • 응답에 실패할 경우, data가 없는 채로 response가 돌아옴
{
    "success": true,
    "code": 0,
    "message": "Ok",
    "data": [
        1,
        2,
        3
    ]
}
{
    "success": false,
    "code": 10001,
    "message": "Validation error - Reason why it isn't valid"
}

🌷 GeneralException.java

@Getter
public class GeneralException extends RuntimeException {

    private final Code errorCode;

    public GeneralException() {
        super(Code.INTERNAL_ERROR.getMessage());
        this.errorCode = Code.INTERNAL_ERROR;
    }

    public GeneralException(String message) {
        super(Code.INTERNAL_ERROR.getMessage(message));
        this.errorCode = Code.INTERNAL_ERROR;
    }

    public GeneralException(String message, Throwable cause) {
        super(Code.INTERNAL_ERROR.getMessage(message), cause);
        this.errorCode = Code.INTERNAL_ERROR;
    }

    public GeneralException(Throwable cause) {
        super(Code.INTERNAL_ERROR.getMessage(cause));
        this.errorCode = Code.INTERNAL_ERROR;
    }

    public GeneralException(Code errorCode) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
    }

    public GeneralException(Code errorCode, String message) {
        super(errorCode.getMessage(message));
        this.errorCode = errorCode;
    }

    public GeneralException(Code errorCode, String message, Throwable cause) {
        super(errorCode.getMessage(message), cause);
        this.errorCode = errorCode;
    }

    public GeneralException(Code errorCode, Throwable cause) {
        super(errorCode.getMessage(cause), cause);
        this.errorCode = errorCode;
    }
}

🌷 Code.java

  • Response Error 코드 번호와 메시지를 담당하는 코드
  • enum을 통해 관리
@Getter
@RequiredArgsConstructor
public enum Code {

    // 충돌 방지를 위한 Code format
    // X1XXX: 제이
    // X2XXX: 셀리나
    // X3XXX: 메이슨
    // ex) 메이슨이 닉네임 중복 에러코드를 만든다면
    // USER_NICKNAME_DUPLICATED(13010, HttpStatus.BAD_REQUEST, "User nickname duplicated"),

    OK(0, HttpStatus.OK, "Ok"),

    BAD_REQUEST(10000, HttpStatus.BAD_REQUEST, "Bad request"),
    VALIDATION_ERROR(10001, HttpStatus.BAD_REQUEST, "Validation error"),
    NOT_FOUND(10002, HttpStatus.NOT_FOUND, "Requested resource is not found"),

    INTERNAL_ERROR(20000, HttpStatus.INTERNAL_SERVER_ERROR, "Internal error"),
    DATA_ACCESS_ERROR(20001, HttpStatus.INTERNAL_SERVER_ERROR, "Data access error"),

    UNAUTHORIZED(40000, HttpStatus.UNAUTHORIZED, "User unauthorized");


    private final Integer code;
    private final HttpStatus httpStatus;
    private final String message;

    public String getMessage(Throwable e) {
        return this.getMessage(this.getMessage() + " - " + e.getMessage());
        // 결과 예시 - "Validation error - Reason why it isn't valid" 
    }

    public String getMessage(String message) {
        return Optional.ofNullable(message)
            .filter(Predicate.not(String::isBlank))
            .orElse(this.getMessage());
    }

    public static Code valueOf(HttpStatus httpStatus) {
        if (httpStatus == null) {
            throw new GeneralException("HttpStatus is null.");
        }

        return Arrays.stream(values())
            .filter(errorCode -> errorCode.getHttpStatus() == httpStatus)
            .findFirst()
            .orElseGet(() -> {
                if (httpStatus.is4xxClientError()) {
                    return Code.BAD_REQUEST;
                } else if (httpStatus.is5xxServerError()) {
                    return Code.INTERNAL_ERROR;
                } else {
                    return Code.OK;
                }
            });
    }

    @Override
    public String toString() {
        return String.format("%s (%d)", this.name(), this.getCode());
    }

🌷 ResponseDto

  • DataResponseDto와 ErrorResponseDto가 ResponseDto를 상속
@Getter
@ToString
@RequiredArgsConstructor
public class ResponseDto {

    private final Boolean success;
    private final Integer code;
    private final String message;

    public static ResponseDto of(Boolean success, Code code) {
        return new ResponseDto(success, code.getCode(), code.getMessage());
    }

    public static ResponseDto of(Boolean success, Code errorCode, Exception e) {
        return new ResponseDto(success, errorCode.getCode(), errorCode.getMessage(e));
    }

    public static ResponseDto of(Boolean success, Code errorCode, String message) {
        return new ResponseDto(success, errorCode.getCode(), errorCode.getMessage(message));
    }
}

🌷 DataResponseDto

@Getter
public class DataResponseDto<T> extends ResponseDto {

    private final T data;

    private DataResponseDto(T data) {
        super(true, Code.OK.getCode(), Code.OK.getMessage());
        this.data = data;
    }

    private DataResponseDto(T data, String message) {
        super(true, Code.OK.getCode(), message);
        this.data = data;
    }

    public static <T> DataResponseDto<T> of(T data) {
        return new DataResponseDto<>(data);
    }

    public static <T> DataResponseDto<T> of(T data, String message) {
        return new DataResponseDto<>(data, message);
    }

    public static <T> DataResponseDto<T> empty() {
        return new DataResponseDto<>(null);
    }
}

🌷 ErrorResponseDto

public class ErrorResponseDto extends ResponseDto {

    private ErrorResponseDto(Code errorCode) {
        super(false, errorCode.getCode(), errorCode.getMessage());
    }

    private ErrorResponseDto(Code errorCode, Exception e) {
        super(false, errorCode.getCode(), errorCode.getMessage(e));
    }

    private ErrorResponseDto(Code errorCode, String message) {
        super(false, errorCode.getCode(), errorCode.getMessage(message));
    }


    public static ErrorResponseDto of(Code errorCode) {
        return new ErrorResponseDto(errorCode);
    }

    public static ErrorResponseDto of(Code errorCode, Exception e) {
        return new ErrorResponseDto(errorCode, e);
    }

    public static ErrorResponseDto of(Code errorCode, String message) {
        return new ErrorResponseDto(errorCode, message);
    }
}

🌷 ExceptionHandler

  • @ControllerAdvice - 모든 @Controller에 대한, 전역적으로 발생할 수 있는 예외를 잡아 처리할 수 있음
  • @RestControllerAdvice - @ControllerAdvice와 @ResponseBody를 합쳐놓은 어노테이션
    • 단순히 예외만 처리하고 싶음 - @ControllerAdvice
    • 응답으로 객체를 리턴해야 함 - @RestControllerAdvice
    • 적용 범위를 클래스나 패키지 단위로 제한할 수도 있음
  • @ExceptionHandler - @Controller, @RestController가 적용된 Bean 내에서 발생하는 예외를 잡아서 하나의 메서드에서 처리해주는 기능
@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionHandler extends ResponseEntityExceptionHandler {

    @org.springframework.web.bind.annotation.ExceptionHandler
    public ResponseEntity<Object> validation(ConstraintViolationException e, WebRequest request) {
        return handleExceptionInternal(e, Code.VALIDATION_ERROR, request);
    }

    @org.springframework.web.bind.annotation.ExceptionHandler
    public ResponseEntity<Object> general(GeneralException e, WebRequest request) {
        return handleExceptionInternal(e, e.getErrorCode(), request);
    }

    @org.springframework.web.bind.annotation.ExceptionHandler
    public ResponseEntity<Object> exception(Exception e, WebRequest request) {
        return handleExceptionInternal(e, Code.INTERNAL_ERROR, request);
    }

    @Override
    protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body,
        HttpHeaders headers, HttpStatus status, WebRequest request) {
        return handleExceptionInternal(ex, Code.valueOf(status), headers, status, request);
    }


    private ResponseEntity<Object> handleExceptionInternal(Exception e, Code errorCode,
        WebRequest request) {
        return handleExceptionInternal(e, errorCode, HttpHeaders.EMPTY, errorCode.getHttpStatus(),
            request);
    }

    private ResponseEntity<Object> handleExceptionInternal(Exception e, Code errorCode,
        HttpHeaders headers, HttpStatus status, WebRequest request) {
        return super.handleExceptionInternal(
            e,
            ErrorResponseDto.of(errorCode, errorCode.getMessage(e)),
            headers,
            status,
            request
        );
    }
}

🌷 UserController

  • Response 사용 형식에 대한 예시
@RequiredArgsConstructor
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

    // http://localhost:8080/user
    @GetMapping(path = "")
    public DataResponseDto<Object> get() {
        return DataResponseDto.of(List.of(1, 2, 3));
        /*
          {
            "success": true,
            "code": 0,
            "message": "Ok",
            "data": [
              1,
              2,
              3
            ]
          }
         */
    }


    // http://localhost:8080/user/error/custom
    @GetMapping(path = "/error/custom")
    public DataResponseDto<Object> errorWithCustomException() {
        Boolean isValid = false;
        
		// Validation 처리
        if (!isValid) {
        	// Validation을 통과하지 못할 경우 Exception을 반환
            // exception occurs
            throw new GeneralException(Code.VALIDATION_ERROR, "Reason why it isn't valid");
            /*
              {
                "success": false,
                "code": 10001,
                "message": "Validation error - Reason why it isn't valid"
              }
            */
        }
		
        return DataResponseDto.empty();
    }


    // http://localhost:8080/user/error/exception
    @GetMapping(path = "/error/exception")
    public DataResponseDto<Object> errorWithException() {
        try {
            List<Integer> list = List.of(1, 2, 3, 4, 5);

            log.debug(list.get(99999).toString()); // outofbound exception occurs

        } catch (Exception e) {
            log.trace("Exception", e);
            throw e;
                /*
                  {
                    "success": false,
                    "code": 20000,
                    "message": "Internal error - Index 9 out of bounds for length 5"
                  }
                */
        }

        return DataResponseDto.empty();
    }
}
profile
[~2023.04] 블로그 이전했습니다 ㅎㅎ https://leeeeeyeon-dev.tistory.com/

0개의 댓글