
API 응답의 양식이 항상 일정해야 프론트 입장에서 개발이 편리해진다.
따라서 매우 중요한 작업이라고 할 수 있다!
{
isSuccess : Boolean
code : String
message : String
result : {응답으로 필요한 또 다른 json}
}
보통 위와 같은 양식을 따른다.
실패한 경우에는 result에 null을 주고 null일때는 표시를 하지 않는 설정 추가
API 응답 통일을 위한 ApiResponse 클래스 생성하겠다.
@Getter
@AllArgsConstructor
@JsonPropertyOrder({"isSuccess", "code", "message", "result"})
public class ApiResponse<T> {
private final Boolean isSuccess;
private final String code;
private final String message;
@JsonInclude(JsonInclude.Include.NON_NULL)
private final 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);
}
}
필수적으로 알고 있어야할 상태 코드 몇가지만 알고 넘어가겠다.
- 200번 대: 문제 없음
a. 200: OK
b. 201: Created: 너가 준 데이터로 새로운 리소스를 만들었다는 뜻- 400번 대: 클라이언트 측 잘못으로 인한 에러
a. 400: Bad Request: 필수한 정보 누락 등 요청이 이상할 때
b. 401: Unauthorized : 인증이 안됨(로그인이 안되는 상황)
c. 403: Forbidden : 권한 x(로그인은 o 접근만 x)
d. 404: NotFound : 요청한 정보가 그냥 없음- 500번대: 서버 측 잘못으로 인한 에러
a. 500: Internal Server Error : 서버 터졌을 때
b. 504: Gateway Timeout : 서버가 응답을 안 줌(터진 것과 마찬가지..)
상태코드는 이렇게 몇가지 정해진 상황에 대한 정보들만 알려줄 수 있기 때문에 더 세부적인 커스텀 code를 만들어서 넘겨주는 것이다.
먼저 BaseCode와 BaseErrorCode 두 인터페이스의 역할은 이를 구체화 하는 Status에서 두 개의 메소드를 반드시 Override할 것을 강제한다.
BaseCode
public interface BaseCode {
public Reason getReason();
public Reason getReasonHttpStatus();
}
BaseErrorCode
public interface BaseCode {
public ErrorReason getReason();
public ErrorReason getReasonHttpStatus();
}
ErrorStatus
@Getter
@AllArgsConstructor
public enum CommonErrorStatus implements BaseErrorCode {
// 가장 일반적인 응답
_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
_BAD_REQUEST(HttpStatus.BAD_REQUEST,"COMMON400","잘못된 요청입니다."),
;
// ~~~ 무엇에 관한 응답
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()
;
}
}
큰 묶음으로 클래스를 만들고, 내부적으로 static 클래스를 만드는 것이 좋다.
DTO는 재사용이 많이 될 수 있기에 static class로 만들게 되면, 매번 class 파일을 만들지 않고, 범용적으로 사용할 수 있다.
DTO에도 빌더 패턴을 쓰자
우리가 만드는 인스턴스들은 모두 빌더 패턴을 사용한다고 생각하면 된다.
참고로, RequestDTO는 값을 받아오는 역할이므로 ResponseDTO에만 Builder패턴을 적용하면 된다.
@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException {
private BaseErrorCode code;
public ErrorReasonDTO getErrorReason() {
return this.code.getReason();
}
public ErrorReasonDTO getErrorReasonHttpStatus() {
return this.code.getReasonHttpStatus();
}
}
@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
public 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);
// e.printStackTrace();
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
);
}
}
전역 예외 처리: RestControllerAdvice는 모든 REST 컨트롤러에서 발생하는 예외를 한 곳에서 처리할 수 있게 해준다. 이를 통해 중복 코드를 줄이고, 예외 처리 로직을 중앙 집중화할 수 있다.
일관된 에러 응답: 클라이언트에게 반환되는 에러 메시지와 코드의 형식을 일관되게 유지할 수 있다. 이는 API의 사용성을 높이고, 클라이언트 측에서 에러를 처리하는 데 도움을 준다.
코드 간결성: RestControllerAdvice를 사용하지 않으면 각 컨트롤러에서 예외를 개별적으로 처리해야 하므로 코드가 복잡해지고 지저분해질 수 있다. 반면, RestControllerAdvice를 사용하면 모든 예외 처리를 하나의 클래스에서 관리할 수 있어 코드가 훨씬 깔끔해진다.
유효성 검사와 통합: @Valid 어노테이션과 함께 사용하면 유효성 검사에서 발생하는 예외도 쉽게 처리할 수 있다. 이를 통해 클라이언트에게 적절한 에러 메시지를 제공할 수 있다.
public class TempHandler extends GeneralException {
public TempHandler(BaseErrorCode errorCode) {
super(errorCode);
}
}