오늘은 Spring Boot에서 API 응답을 깔끔하고 일관되며 사용하기 쉽게 구조화하는 가장 좋은 방법에 대해 이야기해보려고 합니다. 이 글을 끝까지 읽으면, 당신의 API가 더 깔끔하고 일관성 있으며, 사용자 친화적으로 바뀌는 모습을 볼 수 있을 것입니다.
API 응답 구조가 왜 중요할까?
먼저, 잘 구조화된 API 응답이 왜 중요한지 살펴봅시다. 일관된 응답 구조는 다음과 같은 장점을 제공합니다:
- 클라이언트 측 에러 처리 개선: 프론트엔드 팀에서 크게 감사할 것입니다.
- 가독성과 유지보수성 향상: 미래의 당신이나 팀이 명확함에 감동할 것입니다.
- 디버깅과 로깅 간소화: 문제를 빠르고 효율적으로 파악할 수 있습니다.
좋은 API 응답의 조건
잘 구조화된 API 응답은 다음과 같은 특징을 가져야 합니다:
- 일관성: 다양한 엔드포인트에서 동일한 형식 유지
- 정보 제공: 관련 데이터, 메시지, 상태 코드 및 에러 코드를 포함
- 단순함: 쉽게 파싱하고 이해할 수 있는 형식
이상적인 응답 구조 만들기
1. 표준 응답 형식 정의
모든 API가 따를 표준 응답 형식을 먼저 정의합니다. 다음은 간단하면서도 효과적인 형식입니다.
1 2 3 4 5 6 7 8 9
| { "success": true, "message": "요청이 성공적으로 처리되었습니다.", "data": { ... }, "errors": null, "errorCode": 0, "timestamp": 1633017600000, "path": "/api/example" }
|
각 필드의 역할
success
: (boolean
) 요청이 성공했는지 여부를 나타냅니다.
message
: (String
) 요청 처리 결과에 대한 사람이 읽을 수 있는 메시지를 제공합니다.
data
: (T
) 클라이언트가 요청한 실제 데이터를 포함합니다.
errors
: (List<String>
) 요청이 실패한 경우 발생한 에러 메시지 목록입니다.
errorCode
: (int
) 비즈니스 로직 관련 에러 유형을 나타내는 코드입니다.
timestamp
: (long
) 응답이 생성된 시간을 나타냅니다.
path
: (String
) 호출된 API 엔드포인트를 나타냅니다.
2. 응답 유틸리티 메서드 생성
코드 중복을 피하기 위해 응답을 생성하는 유틸리티 메서드를 만들어봅니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public class ResponseUtil {
public static <T> ApiResponse<T> success(T data, String message, String path) { ApiResponse<T> response = new ApiResponse<>(); response.setSuccess(true); response.setMessage(message); response.setData(data); response.setErrors(null); response.setErrorCode(0); response.setTimestamp(System.currentTimeMillis()); response.setPath(path); return response; }
public static <T> ApiResponse<T> error(List<String> errors, String message, int errorCode, String path) { ApiResponse<T> response = new ApiResponse<>(); response.setSuccess(false); response.setMessage(message); response.setData(null); response.setErrors(errors); response.setErrorCode(errorCode); response.setTimestamp(System.currentTimeMillis()); response.setPath(path); return response; }
public static <T> ApiResponse<T> error(String error, String message, int errorCode, String path) { return error(Arrays.asList(error), message, errorCode, path); } }
|
3. 전역 예외 처리 구현
전역적으로 예외를 처리하면 처리되지 않은 에러도 표준 응답 형식으로 반환할 수 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @RestControllerAdvice public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class) public ResponseEntity<ApiResponse<Void>> handleException(HttpServletRequest request, Exception ex) { List<String> errors = Arrays.asList(ex.getMessage()); ApiResponse<Void> response = ResponseUtil.error(errors, "An error occurred", 1000, request.getRequestURI()); return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); }
@ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<ApiResponse<Void>> handleResourceNotFoundException(HttpServletRequest request, ResourceNotFoundException ex) { ApiResponse<Void> response = ResponseUtil.error(ex.getMessage(), "Resource not found", 1001, request.getRequestURI()); return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); }
@ExceptionHandler(ValidationException.class) public ResponseEntity<ApiResponse<Void>> handleValidationException(HttpServletRequest request, ValidationException ex) { ApiResponse<Void> response = ResponseUtil.error(ex.getErrors(), "Validation failed", 1002, request.getRequestURI()); return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); } }
|
4. 컨트롤러에서 응답 형식 사용
표준화된 응답 형식을 컨트롤러에서도 활용합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @RestController @RequestMapping("/api/products") public class ProductController {
@GetMapping("/{id}") public ResponseEntity<ApiResponse<Product>> getProductById(@PathVariable Long id, HttpServletRequest request) { Product product = productService.findById(id); if (product == null) { throw new ResourceNotFoundException("Product not found with id " + id); } ApiResponse<Product> response = ResponseUtil.success(product, "Product fetched successfully", request.getRequestURI()); return new ResponseEntity<>(response, HttpStatus.OK); }
@PostMapping public ResponseEntity<ApiResponse<Product>> createProduct(@RequestBody Product product, HttpServletRequest request) { Product createdProduct = productService.save(product); ApiResponse<Product> response = ResponseUtil.success(createdProduct, "Product created successfully", request.getRequestURI()); return new ResponseEntity<>(response, HttpStatus.CREATED); } }
|
에러 코드의 예시
아래는 전자상거래 애플리케이션에서 사용할 수 있는 에러 코드의 예입니다.
에러 코드 |
설명 |
2000 |
재고 없음 |
2001 |
결제 수단 거부 |
2002 |
유효하지 않은 쿠폰 코드 |
2003 |
주문 취소 기간 초과 |
2004 |
계정 일시 정지 |
2005 |
동일 상품에 대한 중복 주문 |
마무리
Spring Boot에서 API 응답을 구조화하는 가장 좋은 방법을 알아보았습니다. 위 단계를 구현하면 API가 더 깔끔하고 유지보수가 쉬워질 것입니다.