Spring Boot에서 API 응답을 구조화하는 가장 좋은 방법

오늘은 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가 더 깔끔하고 유지보수가 쉬워질 것입니다.

Share