ResponseWrapperDto와 ErrorResponse - API 응답
프로젝트 API를 개발하다보니 API 응답에 대한 뭔가 체계적인 방법을 찾고 싶었다.
그래서 열심히 서치하며 정리를 해보았다.
일단 결론만 보자면,
- ResponseWrapperDto : 전반적인 응답에 대한 공통 포맷
- ErrorResponse : 에러 상황에 대한 구체적 응답 포맷
- ErrorCode : 에러의 종류별로 상태/코드/기본 메시지를 모아둔 Enum
- FieldError : 유효성 검증 실패 등에서 발생하는 구체적 필드단위 에러정보를 담기 위한 서브클래스
이 네가지를 구분해서 만들었다.
그 이유는 !?!??
-> API 응답을 체계적으로 분류하고, 일관된 형식으로 제공하려고 !
더 자세히 살펴보겠다.
1. ResponseWrapperDto
: 정상 응답을 감싸는 공통 DTO.
package com.example.refrig.api.common;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.*;
import java.util.List;
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class ResponseWrapperDto<T> {
private T data; // 응답 본문 데이터
private String code; // API 응답 상태코드
private String message; // 응답 메시지
private List<ErrorResponse.FieldError> errorList; // 에러 발생 시 해당 에러에 대한 상세 정보 리스트
// 에러 상황에 대한 DTO를 만들 때 사용하는 정적 메서드
public static ResponseWrapperDto<ErrorResponse> of(ErrorResponse errorResponse) {
return ResponseWrapperDto.<ErrorResponse>builder()
.code(errorResponse.getCode())
.message(errorResponse.getMessage())
.errorList(errorResponse.getErrors())
.build();
}
// 일반 성공 응답에 대한 DTO를 만들 때 사용하는 정적 메서드
public static <T> ResponseWrapperDto<T> of(T body) {
return ResponseWrapperDto.<T>builder()
.data(body)
.build();
}
// 객체를 JSON 문자열로 직렬화
public String toJson() {
try {
return new ObjectMapper().writeValueAsString(this);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to serialize ResponseWrapperDto to JSON", e);
}
}
}
2. ErrorResponse
: 에러 상황에서 응답을 전송할 때 동일한 구조를 유지하기 위한 DTO.
package com.example.refrig.api.common;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.validation.BindingResult;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ErrorResponse {
private String message; //에러 메시지
private int status; //HTTP 상태 코드
private String code; //에러 코드 (커스텀 에러코드)
private List<FieldError> errors; // 구체적인 필드 오류
/**
* - errorCode : 에러 코드 및 기본 메시지, HTTP status를 가진 Enum
* - bindingResult : 스프링 Validation 등을 통해 폼이나 dto 검증이 실패하면 검증오류 정보가 bindingResult에 쌓인다.
* - errors : 상황에 따라 직접 정의한 필드 에러를 주입할 수 있는 용도.
* - customMessage : errorCode에 정의된 기본 메시지 대신 상황에 따라 다른 메시지를 사용하고 싶을 때 주입하는 파라미터
*/
public static ErrorResponse of(
ErrorCode errorCode,
BindingResult bindingResult,
List<FieldError> errors,
String customMessage
) {
List<FieldError> resolvedErrors;
// bindingResult가 존재하면 FieldError를 추출.
// 없으면 파라미터로 받은 errors 사용
if (bindingResult != null) {
resolvedErrors = FieldError.of(bindingResult);
} else if (errors != null) {
resolvedErrors = errors;
} else {
resolvedErrors = Collections.emptyList();
}
return ErrorResponse.builder()
// customMessage가 있으면 우선 사용, 없으면 errorCode의 기본메시지
.message(customMessage != null ? customMessage : errorCode.getMessage())
.status(errorCode.getStatus())
.code(errorCode.getCode())
.errors(resolvedErrors)
.build();
}
// 중첩 클래스.
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class FieldError {
// ...
}
}
3. ErrorCode
: 프로젝트 전역에서 발생할 수 있는 에러 유형을 Enum 형태로 묶어 각 에러별로 HTTP 상태코드(status), 내부 식별 코드(code), 기본 에러 메시지(message)를 정의해둔다.
-> 에러 관리가 체계화되고, 새 에러 유형을 추가하거나 메시지를 변경하는 것도 간편해짐.
package com.example.refrig.api.common;
import lombok.Getter;
@Getter
public enum ErrorCode {
NO_JWT_TOKEN(401, "AUT000", "JWT 토큰이 존재하지 않습니다.");
// ...
private final int status;
private final String code;
private final String message;
ErrorCode(int status, String code, String message) {
this.status = status;
this.code = code;
this.message = message;
}
}
4. FieldError
: 유효성 검증(Validation) 오류 등에서 구체적으로 어느 필드가, 무슨 이유로 잘못됐는가를 나타낼 수 있는 하위 클래스
- ErrorResponse 내 erros 필드에 리스트 형태로 보관하여 필드 단위 에러 정보를 리스트로 제공할 수 있게 된다.
- 위의 ErrorResponse 클래스 내부에 중첩 클래스로 넣어주었다. FieldError는 Validation 오류 발생 시 어떤 필드가 잘못되었는지를 설명하는 역할인데, 이는 ErrorResponse 내부에서만 사용되는 개념이므로, 별도로 분리하기보다 연관성이 높은 ErrorResponse 내부에서 정의하는 것이 더 자연스럽다.
package com.example.refrig.api.common;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ErrorResponse {
private String message; //에러 메시지
private int status; //HTTP 상태 코드
private String code; //에러 코드 (커스텀 에러코드)
private List<FieldError> errors; // 구체적인 필드 오류
// ...
// 중첩 클래스.
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class FieldError {
private String field; //에러 발생한 필드 이름
private String value; //서버가 받은 잘못된 값
private String reason; //에러 사유
public static List<FieldError> of(BindingResult bindingResult) {
// 스프링의 bindingResult.getFieldErrors 목록을 순회하며 우리 정의의 FieldError 객체로 변환.
// 일관된 에러 응답 구조
return bindingResult.getFieldErrors().stream()
.map(err -> new FieldError(
err.getField(),
err.getRejectedValue() == null ? "" : err.getRejectedValue().toString(),
err.getDefaultMessage() == null ? "" : err.getDefaultMessage()
))
.collect(Collectors.toList());
}
}
}
왜 이런 구조를 쓰는가?
1) 일관된 응답 형식
2) 가독성과 유지보수성
3) 표준화된 예외 처리 로직
4) 확장성
서버-클라이언트 간 통신할 때 예측 가능하고 표준화된 형식으로 성공/실패 응답을 주고받도록 설계한 것 !
사담)
이렇게 정리하고 나니 속이 시원한 J....
'Programming > Java' 카테고리의 다른 글
JShell (0) | 2023.12.14 |
---|---|
프로그래밍을 통한 문제해결 (2) | 2023.12.13 |
String | StringBuffer | StringBuilder (1) | 2023.12.04 |
File I/O (1) | 2023.12.03 |
Stream API (1) | 2023.11.30 |