클라이언트와 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는 크게 success
, code
, message
, data
4가지 파트로 나뉘어져있다.
success
- Response 응답 성공 여부code
- Response 응답 코드message
- 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"
}
@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;
}
}
@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());
}
@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));
}
}
@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);
}
}
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);
}
}
@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
);
}
}
@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();
}
}