ResponseWrapperDto와 ErrorResponse - API 응답

2025. 3. 9. 15:40
728x90

프로젝트 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....

728x90

'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

BELATED ARTICLES

more