토이프로젝트

[투두메이트 프로젝트] 공통 응답(Response) 객체 만들기

민석삼 2025. 5. 2. 03:33

4/9 작성

 

투두메이트 프로젝트를 진행하던 중, 요청을 보냈을 때 성공이든 실패든 너무 단순한 응답만 받으니 직접 로그를 확인하는 등 디버깅에 불편을 느꼈다.

  • 에러가 나면 항상 같은 에러 메세지만 출력되고, 어떤 문제가 발생했는지 알기 위해선 로그를 직접 뒤져야 했다.
  • 요청이 성공해도 200OK만 떠서, 어떤 요청이 성공한 건지 명확하지 않았다. 

혼자 개발하는데도 이렇게 불편한데, 협업 환경에서는 더 불편하겠다 싶었다. 남이 짠 코드를 테스트하거나 디버깅할 때 불필요하게 시간을 낭비하게 될 것 같다는 생각이 들었다...

그래서 응답 객체를 통일해서 클라이언트와 서버 간의 소통을 더 명확히 하자는 생각이 들었다.

이 글에서는 그렇게 만들게 된 공통 응답 Response 객체에 대해 정리해보겠다.

 

기존 postman 응답

실패

같은 이메일로 만들려할때

성공

성공과 실패 시의 응답 통일?

나중에 프론트엔드와 협업하게 될 때 성공 메세지와 실패 메세지를 받아 화면에 보여주게 될 경우를 고려하여 성공과 실패 시의 응답 구성 모두 통일했다.

 

https://velog.io/@qotndus43/%EC%8A%A4%ED%94%84%EB%A7%81-API-%EA%B3%B5%ED%86%B5-%EC%9D%91%EB%8B%B5-%ED%8F%AC%EB%A7%B7-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0

 

스프링 API 공통 응답 포맷 개발하기

클라이언트 ↔︎ 서버 구조에서클라이언트는 서버에 요청을 보내고 서버는 요청에 대한 결과를 응답합니다.예를 들어 클라이언트가 1번 상품을 요청하는 경우 서버는 1번 상품을 조회해 응답하

velog.io

 

해당 블로그를 참고해서 응답을 status, message, data의 세 가지 항목으로 구성해주었고, 성공과 실패 모두 같은 포맷으로 응답하기 위해 ApiResponse 클래스를 만들었다.

 

공통 응답 객체 ApiResponse

@Getter
@AllArgsConstructor
@NoArgsConstructor
public class ApiResponse<T> {

    private static final String SUCCESS_STATUS = "success";
    private static final String ERROR_STATUS = "error";

    private String status;
    private String message;
    private T data;

    //데이터와 함께 성공 반환
    public static <T> ApiResponse<T> createSuccess(T data) {
        return new ApiResponse<>(SUCCESS_STATUS, "성공", data);
    }

    public static <T> ApiResponse<T> createSuccess(String message, T data) {
        return new ApiResponse<>(SUCCESS_STATUS, message, data);
    }

    //데이터 없이 성공 반환
    public static ApiResponse<?> createSuccessWithNoContent() {
        return new ApiResponse<>(SUCCESS_STATUS, "성공", null);
    }

    public static ApiResponse<?> createSuccessWithNoContent(String message) {
        return new ApiResponse<>(SUCCESS_STATUS, message, null);
    }

    //유효성 검증
    public static ApiResponse<?> createFail(BindingResult bindingResult) {
        Map<String, String> errors = new HashMap<>();
        for(ObjectError error : bindingResult.getAllErrors()) {
            if (error instanceof FieldError) {
                errors.put(((FieldError) error).getField(), error.getDefaultMessage());
            } else {
                errors.put(error.getObjectName(), error.getDefaultMessage());
            }
        }

        return new ApiResponse<>(ERROR_STATUS, "입력값 오류", errors);
    }

    //예외
    public static ApiResponse<?> createError(String message) {
        return new ApiResponse<>(ERROR_STATUS, message, null);
    }

}

위의 블로그 글을 참고해서 성공, 유효성 검증에서 발생시킬 오류, 예외에 대한 에러처리라는 세 경우에 대해 함수를 만들었다. 성공의 경우 Get처럼 다른 데이터와 함께 반환해야 하는 경우도 있기에, createSuccess()와 createSuccessWithNoContent()라는 두 함수로 만들어주었다..


실제 적용 예시

        return ResponseEntity.status(HttpStatus.CREATED)
                .body(ApiResponse.createSuccessWithNoContent("루틴 생성 완료"));
        return ResponseEntity.ok(ApiResponse.createSuccess("투두 보여주기 완료", todoResponses));

실제 응답은, 이 ApiResponse를 한 번 더 ResponseEntity를 감싸, ResponseEntity<ApiResponse<?>> 의 형태로 반환했다.
이렇게 하면 다음과 같은 장점이 있다.

  • HTTP 상태 코드 설정 가능 (200 OK, 201 Created 등)
  • 본문(body)은 공통 포맷으로 유지
  • <?>는 어떤 응답의 데이터든 허용함 (예: List<TodoResponse>, String, null 등)
    @PatchMapping("/api/v1/todos/order")
    public ResponseEntity<ApiResponse<?>> updateTodoOrders(@RequestBody UpdateTodoOrdersRequest request) {
        todoService.updateTodoOrders(AuthUtil.getLoginUserId(), request);

        return ResponseEntity.ok(ApiResponse.createSuccessWithNoContent("투두 순서 변경 완료"));
    }
실제 사용 예시
 

*ResponseEntity<T>란?

스프링에서 HTTP 응답을 만들 때 사용하는 클래스

status, header, body를 직접 지정해줄 수 있다.

return new ResponseEntity<>(body, status) 이런식으로 활용 가능

 

이 body에 ApiResponse를 담아줄 수 있는 것!

함수의 반환값을 ResponseEntity<ApiResponse<?>> 로 하여
HTTP 응답 전체(상태, 바디 등)를 감싸고
ApiResponse<?> 는 실제 응답 body 포맷 (message, data, success 등)을 포장
 

유효성 검증 실패, 예외 경우

유효성 검증을 실패하거나 예외가 발생하는 경우는 GlobalExceptionHandler에서 처리하도록 했다.

예외 발생 시 각 예외를 처리하는 ExceptionHandler로 넘어가고, 해당 ExceptionHandler에서 ApiResponse의 createError()를 발생시키고 응답을 반환하도록 했다.

유효성 검증이 실패한 경우에도 MethodArgumentNotValidException이 발생하여 해당 ExceptionHandler로 넘어가고, ApiResponse의 createFail()을 실행하여 응답을 반환한다.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<?>> handleUnknownException(Exception e) {
        e.printStackTrace();
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.createError("서버에 문제가 발생했습니다."));
    }

    @ExceptionHandler(CustomException.class)
    public ResponseEntity<ApiResponse<?>> handleCustomException(CustomException e) {
        return ResponseEntity
                .badRequest()
                .body(ApiResponse.createError(e.getMessage()));
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ApiResponse<?>> handleIllegalArgumentException(IllegalArgumentException e) {
        e.printStackTrace();
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(ApiResponse.createError(e.getMessage()));
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse<?>> handleValidationException(MethodArgumentNotValidException e) {
        BindingResult bindingResult = e.getBindingResult();

        return ResponseEntity.badRequest().body(ApiResponse.createFail(bindingResult));
    }
}

 

 

 

참고 블로그

https://velog.io/@qotndus43/%EC%8A%A4%ED%94%84%EB%A7%81-API-%EA%B3%B5%ED%86%B5-%EC%9D%91%EB%8B%B5-%ED%8F%AC%EB%A7%B7-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0

 

스프링 API 공통 응답 포맷 개발하기

클라이언트 ↔︎ 서버 구조에서클라이언트는 서버에 요청을 보내고 서버는 요청에 대한 결과를 응답합니다.예를 들어 클라이언트가 1번 상품을 요청하는 경우 서버는 1번 상품을 조회해 응답하

velog.io

https://velog.io/@kseysh/ResponseEntity-vs-ApiResponse

 

ResponseEntity vs ApiResponse

ResponseEntity vs ApiResponse 각각 어떤 장점이 있고 나는 어떤 응답을 선택해야 할까?

velog.io

 

'토이프로젝트' 카테고리의 다른 글

[투두메이트 클론코딩] ERD 및 주요 기능  (0) 2025.04.05