4/9 작성
투두메이트 프로젝트를 진행하던 중, 요청을 보냈을 때 성공이든 실패든 너무 단순한 응답만 받으니 직접 로그를 확인하는 등 디버깅에 불편을 느꼈다.
- 에러가 나면 항상 같은 에러 메세지만 출력되고, 어떤 문제가 발생했는지 알기 위해선 로그를 직접 뒤져야 했다.
- 요청이 성공해도 200OK만 떠서, 어떤 요청이 성공한 건지 명확하지 않았다.
혼자 개발하는데도 이렇게 불편한데, 협업 환경에서는 더 불편하겠다 싶었다. 남이 짠 코드를 테스트하거나 디버깅할 때 불필요하게 시간을 낭비하게 될 것 같다는 생각이 들었다...
그래서 응답 객체를 통일해서 클라이언트와 서버 간의 소통을 더 명확히 하자는 생각이 들었다.
이 글에서는 그렇게 만들게 된 공통 응답 Response 객체에 대해 정리해보겠다.
기존 postman 응답
실패


성공

성공과 실패 시의 응답 통일?
나중에 프론트엔드와 협업하게 될 때 성공 메세지와 실패 메세지를 받아 화면에 보여주게 될 경우를 고려하여 성공과 실패 시의 응답 구성 모두 통일했다.
스프링 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를 담아줄 수 있는 것!
유효성 검증 실패, 예외 경우
유효성 검증을 실패하거나 예외가 발생하는 경우는 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));
}
}
참고 블로그
스프링 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 |
|---|