성능 최적화 20편 - API 응답 압축과 페이로드 최소화로 네트워크 비용 줄이기
18편에서는 프론트엔드 번들의 전송 용량을 gzip/brotli로 줄이는 방법을 다뤘습니다. 이번 글에서는 그 반대편, 서버가 클라이언트로 내려주는 API 응답 자체의 용량을 줄이는 방법을 정리합니다. 쿼리도 빠르고 캐싱도 잘 되어 있는데 응답이 느리다면, 전송되는 데이터 양 자체가 원인인 경우가 있습니다.
1. 응답 압축이 필요한 이유
목록 조회 API가 JSON으로 1000건의 데이터를 내려준다고 가정하면, 압축 없이는 그 텍스트 그대로 네트워크를 통해 전송됩니다.
압축 전 응답 크기 : 850KB
압축 후(gzip) 응답 크기 : 95KB (약 89% 감소)
JSON은 같은 키("productName", "price" 등)가 반복되는 텍스트 구조라 압축 효율이 특히 좋습니다. 압축 자체는 서버 CPU를 약간 더 쓰는 대신, 네트워크 전송 시간을 크게 줄여줘서 대부분의 경우 이득이 훨씬 큽니다.
2. Spring Boot에서 Gzip 압축 활성화하기
server:
compression:
enabled: true
mime-types: application/json, application/xml, text/html, text/plain
min-response-size: 1024 # 1KB 미만 응답은 압축하지 않음
min-response-size를 설정해두는 이유는, 아주 작은 응답까지 압축하면 오히려 압축 처리 자체의 오버헤드가 이득보다 커질 수 있기 때문입니다. 보통 1KB 전후를 기준으로 잡습니다.
3. 압축이 적용됐는지 확인하기
클라이언트가 압축을 지원한다는 것은 요청 헤더로 알립니다.
Accept-Encoding: gzip, deflate, br
서버가 이를 확인하고 압축된 응답을 내려주면, 응답 헤더에 다음이 포함됩니다.
Content-Encoding: gzip
브라우저 개발자 도구의 Network 탭에서 응답 헤더에 Content-Encoding: gzip이 있는지, 그리고 실제 전송 크기(Size)와 압축 해제 후 크기(Content)를 비교해보면 압축이 잘 적용되고 있는지 확인할 수 있습니다.
4. 압축만으로는 부족한 경우 - 페이로드 자체를 줄이기
압축은 "같은 데이터를 더 작게 전송"하는 방법이고, 페이로드 최소화는 애초에 "불필요한 데이터를 아예 내려주지 않는 것"입니다. 둘은 함께 적용해야 효과가 극대화됩니다.
// 목록 화면에는 실제로 몇 개 필드만 필요한데, Entity 전체를 그대로 반환
@GetMapping("/products")
public List<Product> getProducts() {
return productRepository.findAll(); // 상세 설명, 옵션, 재고 이력 등 모든 필드 포함
}
// 목록 화면에 필요한 필드만 담은 DTO로 응답
public record ProductSummaryDto(Long id, String name, int price, String thumbnailUrl) {
}
@GetMapping("/products")
public List<ProductSummaryDto> getProducts() {
return productService.getProductSummaries();
}
목록 화면에서는 상품명, 가격, 썸네일 정도만 필요한데 상세 설명, 리뷰 목록, 옵션 정보까지 전부 내려주고 있었다면, 이것만 DTO로 분리해도 응답 크기가 크게 줄어듭니다.
5. 불필요한 null 필드와 중첩 구조 줄이기
{
"id": 1,
"name": "상품명",
"price": 10000,
"description": null,
"detailImages": [],
"seller": {
"id": 5,
"name": "판매자명",
"businessNumber": null,
"address": null,
"phoneNumber": null
}
}
목록 화면에는 필요 없는 seller의 상세 정보까지 중첩되어 내려오고 있다면, 목록용 DTO에서는 sellerName 하나만 평평하게(flat) 내려주는 방식으로 구조 자체를 단순화할 수 있습니다.
{
"id": 1,
"name": "상품명",
"price": 10000,
"sellerName": "판매자명"
}
6. 페이지네이션과 함께 고려하기
12편에서 다룬 No Offset 페이지네이션과 함께, 한 번에 내려주는 목록의 개수 자체도 적절히 제한되어야 합니다. 한 페이지에 100건씩 내려주는 것보다, 화면에 실제로 보이는 20건 정도로 제한하고 무한 스크롤이나 페이지 이동으로 나머지를 요청하도록 설계하는 것이 전체 전송량 관리에 유리합니다.
7. gzip과 브라우저 캐싱을 함께 활용하기
자주 바뀌지 않는 응답이라면, 압축뿐 아니라 HTTP 캐시 헤더를 함께 설정해서 아예 재요청 자체를 줄일 수 있습니다.
@GetMapping("/categories")
public ResponseEntity<List<CategoryDto>> getCategories() {
List<CategoryDto> categories = categoryService.getAllCategories();
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(Duration.ofHours(1)))
.body(categories);
}
Cache-Control: max-age=3600을 설정하면, 클라이언트는 1시간 동안 같은 요청을 서버에 다시 보내지 않고 브라우저에 저장된 응답을 그대로 사용합니다. 압축과 캐싱은 서로 다른 층위에서 작동하지만, 함께 적용하면 네트워크 비용을 이중으로 줄일 수 있습니다.
8. 적용 전후 비교
[적용 전]
목록 API 응답 크기 : 850KB (압축 없음, Entity 전체 반환)
평균 응답 시간(모바일 3G 기준) : 2.1초
[적용 후]
목록 API 응답 크기 : 32KB (gzip 압축 + DTO 최소화)
평균 응답 시간(모바일 3G 기준) : 210ms
특히 모바일 환경이나 네트워크 상태가 좋지 않은 사용자일수록, 이런 응답 크기 최적화의 체감 효과가 훨씬 크게 나타납니다.
9. 정리
- Gzip 압축은 서버 설정만으로 적용 가능하며, JSON처럼 반복되는 텍스트 구조에서 압축 효율이 특히 높다
- 압축과 별개로, Entity 전체가 아닌 화면에 실제 필요한 필드만 담은 DTO로 응답을 최소화해야 한다
- 불필요한 null 필드나 과도한 중첩 구조도 응답 크기를 키우는 요인이므로, 목적에 맞게 평탄화한다
- 자주 바뀌지 않는 응답은 HTTP 캐시 헤더를 함께 적용해 재요청 자체를 줄일 수 있다
18편의 번들 축소가 "브라우저가 받는 코드"를 줄이는 것이었다면, 이번 편은 "브라우저가 받는 데이터"를 줄이는 것입니다. 결국 클라이언트-서버 사이를 오가는 모든 것에 "정말 필요한 만큼만 주고받는다"는 원칙이 동일하게 적용됩니다.
'성능 최적화' 카테고리의 다른 글
| 성능 최적화 STEP 22 - 샤딩(Sharding) (1) | 2026.07.20 |
|---|---|
| 성능 최적화 STEP 21 - Read Replica (1) | 2026.07.20 |
| 성능 최적화 STEP 19 - 트랜잭션 격리 레벨 (0) | 2026.07.15 |
| 성능 최적화 STEP 18 - 번들 사이즈 분석과 축소 (0) | 2026.07.14 |
| 성능 최적화 STEP 17 - 이미지 최적화 (0) | 2026.07.14 |