📆 작성일: 2025.07.16
✍️ 주제: Spring Boot에서
ResponseEntity
,ApiResponse
,전역 예외 처리
를 이용한 응답 통일 구조 설계
프로젝트를 진행하면서 하나의 API는 ResponseEntity<ApiResponse<...>>
를 사용하고,
다른 API는 그냥 ResponseEntity<List<...>>
로 반환하고 있는 모습을 봤다.
// 내가 만든 API
return ResponseEntity.ok(ApiResponse.success(data));
// 기존 API
return ResponseEntity.ok(menuCategories);
이런 구조는 클라이언트에서 응답 파싱 시 조건 분기를 유발하고,
결과적으로 유지보수와 협업이 어려워지는 문제로 이어질 수 있다.
우리가 도입한 응답 구조는 다음과 같다:
// 성공 시
{
"success": true,
"data": { ... }, // 혹은 리스트
"message": "요청이 성공적으로 처리되었습니다.",
"timestamp": "2025-07-16T15:30:00"
}
// 실패 시
{
"success": false,
"data": {
"errorCode": "NOT_FOUND",
"message": "해당 고객을 찾을 수 없습니다.",
"path": "/api/visitors/123"
},
"message": "해당 고객을 찾을 수 없습니다.",
"timestamp": "2025-07-16T15:31:00"
}
ApiResponse<T>
ApiResponse
는 모든 응답을 감싸는 공통 DTO다.
@Getter
@AllArgsConstructor
public class ApiResponse<T> {
private final boolean success;
private final T data;
private final String message;
private final LocalDateTime timestamp;
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, data, "요청이 성공적으로 처리되었습니다.", LocalDateTime.now());
}
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(false, null, message, LocalDateTime.now());
}
public static <T> ApiResponse<T> error(String message, T data) {
return new ApiResponse<>(false, data, message, LocalDateTime.now());
}
}
이렇게 하면 Controller에서는 항상 일관되게 작성할 수 있다:
@GetMapping("/customers")
public ResponseEntity<ApiResponse<List<CustomerResponse>>> getCustomers() {
List<CustomerResponse> result = service.getCustomers();
return ResponseEntity.ok(ApiResponse.success(result));
}