오버페칭은 응답이 클라이언트에 필요한 것보다 많은 데이터를 담는 API 문제이고, 언더페칭은 필요한 데이터를 채우려고 추가 요청을 강제하는 API 문제입니다. 둘은 고정된 응답 형태가 낳는 쌍둥이 실패 모드입니다. 하나는 바이트를 낭비하고 다른 하나는 왕복을 낭비하며, 대부분의 API는 같은 화면에서 둘 다 저지릅니다. 응답 하나하나는 너무 비대하고, 응답의 수는 너무 많습니다.
핵심 요약
| 질문 | 답변 |
|---|---|
| 오버페칭 | 응답당 너무 많음: 대역폭, 파싱, 배터리, 노출 |
| 언더페칭 | 응답당 너무 적음: 추가 왕복, 워터폴, N+1 |
| 근본 원인 | 고정된 엔드포인트 형태가 요구 사항이 다른 화면과 만남 |
| REST의 해결책 | 희소 필드셋 · include/expand 파라미터 · 페이지네이션 · 복합 엔드포인트 |
| GraphQL의 답 | 선택 집합. 단, 리졸버 계층에는 솔직한 단서가 붙음 |
한 화면, 세 가지 가져오기 방식
각 제목과 작성자 이름을 렌더링하는 게시물 목록입니다:
오버페칭 언더페칭
GET /posts GET /posts (작성자 ID만)
→ 게시물 20개 × 필드 40개 GET /users/11 ┐
→ ~160 KB 전송, GET /users/12 │ 추가 호출 20회 –
~6 KB 렌더링 (96% 낭비) … │ N+1 워터폴
GET /users/30 ┘
형태를 맞춘 쿼리
GET /posts?fields=title,summary,author&include=author&limit=20
→ 게시물 20개 × 필드 3개 + 작성자 – 왕복 1회, ~7 KB
같은 형태를 맞춘 쿼리를 SDK 코드로 쓰면, 프로젝션과 관계 조회가 한 번의 요청에 담깁니다:
// JavaScript / Node.js — Back4app JS SDK
// One shaped query: no overfetch, no underfetch
const query = new Parse.Query('Post');
query.select('title', 'summary', 'author'); // only what the screen renders
query.include('author'); // related object, same response
query.limit(20); // bounded page
const posts = await query.find();
// 1 round trip — not 1 list call + 20 author calls (N+1) // Flutter / Dart — Back4app Flutter SDK
// One shaped query: no overfetch, no underfetch
final query = QueryBuilder<ParseObject>(ParseObject('Post'))
..keysToReturn(['title', 'summary', 'author']) // only what the screen renders
..includeObject(['author']) // related object, same response
..setLimit(20); // bounded page
final response = await query.query();
// 1 round trip — not 1 list call + 20 author calls (N+1) // iOS / Swift — Back4app Swift SDK
// One shaped query: no overfetch, no underfetch
let query = Post.query()
.select("title", "summary", "author") // only what the screen renders
.include("author") // related object, same response
.limit(20) // bounded page
query.find { result in
// 1 round trip — not 1 list call + 20 author calls (N+1)
if case .success(let posts) = result { render(posts) }
} // Android / Kotlin — Back4app Android SDK
// One shaped query: no overfetch, no underfetch
val query = ParseQuery.getQuery<ParseObject>("Post")
query.selectKeys(listOf("title", "summary", "author")) // only what the screen renders
query.include("author") // related object, same response
query.limit = 20 // bounded page
query.findInBackground { posts, e -> if (e == null) render(posts) }
// 1 round trip — not 1 list call + 20 author calls (N+1) 오버페칭이란?
오버페칭은 API 계층판 SELECT *입니다. 엔드포인트는 호출자가 무엇을 렌더링하든 고정된 전체 표현을 반환합니다. 비용은 층층이 쌓입니다. 서버는 아무도 읽지 않는 필드를 직렬화하고, 네트워크가 그것을 실어 나릅니다. 모바일이 고통받는 곳이 여기입니다. 전송 시간은 제한된 대역폭 위의 바이트 수에 비례하고, 불필요한 킬로바이트마다 종량제 데이터와 무선 통신 배터리가 소모됩니다. 그다음 클라이언트가 전체를 파싱합니다. 압축 해제는 렌더링보다 먼저 일어나므로, 전송 구간에서 압축이 크기를 감춰 주더라도 비대한 응답은 비대한 파싱 작업입니다.
더 조용한 비용은 노출입니다. UI가 한 번도 보여 주지 않는 응답 필드도 개발자 도구 클릭 한 번이면 읽힙니다. 내부 플래그, 다른 사용자의 이메일 주소, 마진 데이터가 그렇습니다. OWASP API Security Top 10은 이를 과도한 데이터 노출(broken object property level authorization)로 추적합니다. 최소 권한 원칙은 응답 본문에도 적용되며, 어떤 클라이언트도 보면 안 되는 필드는 애초에 직렬화되지 않아야 합니다.
언더페칭이란?
언더페칭은 반대 방향의 결핍입니다. 엔드포인트의 고정된 형태가 너무 적게 담고 있어서, 클라이언트가 통합자가 됩니다. 목록을 가져오고, 각 항목의 작성자를 가져오고, 어쩌면 각 작성자의 아바타까지 가져옵니다. 추가 호출 하나하나가 완전한 왕복이며, 왕복은 모바일 네트워크가 가장 부족한 자원입니다. 요청당 현실적인 100ms를 가정하면, 항목 20개짜리 목록을 순차적으로 해결하는 데 페이로드 계산을 하기도 전에 지연 시간만으로 2초를 씁니다.
규모가 커지면 이 워터폴에는 이름이 붙습니다. 바로 N+1 요청 문제입니다. N개 항목을 위한 호출 한 번, 그 상세 정보를 위한 호출 N번입니다. 이 형태는 프랙털처럼 반복됩니다. 고정된 인터페이스가 관계형 데이터를 만나는 곳마다 다시 나타납니다. REST 엔드포인트를 호출하는 HTTP 클라이언트, 데이터베이스를 호출하는 GraphQL 리졸버, 루프를 돌며 지연 로딩하는 ORM이 그렇습니다. 그리고 치료법은 언제나 N을 하나로 배칭하는 어떤 형태입니다.
오버페칭 vs. 언더페칭
| 오버페칭 | 언더페칭 | |
|---|---|---|
| 증상 | 렌더링되지 않는 필드로 가득한 응답 | 여러 호출로 조립되는 화면 |
| 낭비의 단위 | 바이트(와 파싱 시간) | 왕복(과 지연 시간) |
| 가장 심한 곳 | 종량제, 저속, 배터리에 묶인 네트워크 | 고지연 네트워크: 워터폴이 누적됨 |
| 탐지 | 반환된 필드와 렌더링된 필드를 비교 | 네트워크 탭에서 화면당 요청 수를 셈 |
| 직접적인 해결책 | 희소 필드셋 / 프로젝션 | 확장 파라미터, 복합 엔드포인트 |
| 악화 시 | 과도한 데이터 노출(보안) | N+1 요청 폭주(규모) |
다행히 진단은 기계적이며, 상위에 노출되는 설명 글 어디에도 이 이야기는 없습니다. 한 화면에서 네트워크 탭을 열면 됩니다. 한 뷰에 요청이 많으면 언더페칭이고, UI에서 찾을 수 없는 필드로 가득한 큰 응답은 오버페칭입니다. 엔드포인트 분석은 이 점검을 일반화합니다. 엔드포인트별 p95 페이로드 크기, 화면별 세션당 요청 수를 보면 됩니다.
REST를 떠나지 않고 둘 다 고치기
GraphQL 마이그레이션이 첫 번째 수단은 아닙니다. 성숙한 REST 관례가 격차의 대부분을 메웁니다:
- 희소 필드셋: 표현을 프로젝션하는
fields파라미터입니다. JSON:API sparse fieldsets로 표준화되어 있고,$select형식의 쿼리 옵션과 SDK의select()빌더가 같은 일을 합니다. 원천에서 오버페칭을 고치는 방법입니다. - 확장 파라미터:
include=author,comments는 관련 리소스를 같은 응답에 포함시켜(복합 문서) N+1 워터폴을 요청 하나로 바꿉니다. 원천에서 언더페칭을 고치는 방법입니다. - 페이지네이션: 오버페칭의 목록 차원에 상한을 둡니다. 상한 없는 컬렉션은 사용자가 늘수록 커지는 페이로드 버그입니다.
- 목적에 맞춘 엔드포인트와 복합 엔드포인트: 한 화면이 언제나 같은 집계를 필요로 한다면, 정확히 그 집계를 반환하는 엔드포인트를 주는 것이 좋습니다. 조립은 서버 측에서 이루어지며, 그곳의 서비스 간 지연 시간은 모바일 왕복이 아니라 마이크로초 단위입니다.
- BFF(backend-for-frontend): 같은 움직임의 아키텍처판입니다. 내부적으로는 넉넉한 API와 통신하고 각 프론트엔드에는 정확히 그 형태만 제공하는 클라이언트별 얇은 계층입니다(Sam Newman의 BFF 패턴).
- 압축: 솔직히 마지막 순위입니다. 전송 크기는 줄이지만 낭비는 줄이지 못하며, 파싱 비용과 노출은 그대로 남습니다.
GraphQL은 이 문제를 해결합니까?
대부분은 해결합니다. 그리고 그 “대부분”은 알아 둘 가치가 있습니다. 선택 집합은 클라이언트의 필드 목록을 요청 그 자체로 만들어 고전적인 오버페칭을 없애고, 중첩 쿼리는 관련 데이터를 한 번의 왕복으로 조립해 고전적인 언더페칭을 없앱니다. GraphQL vs. REST 논쟁이 바로 이 두 단어에서 시작하는 이유입니다.
단서는 한 계층 아래에 있습니다. 넉넉한 쿼리를 복사해 붙여 넣는 클라이언트는 습관적으로 오버페칭합니다. 팀이 컴포넌트별 프래그먼트를 도입하지 않는 한, 쿼리가 컴포넌트가 렌더링하는 것과 일치하도록 강제하는 장치는 없습니다. 그리고 단순한 리졸버 체인은 데이터베이스를 상대로 언더페칭합니다. 작성자를 포함한 게시물 20개 쿼리는 리졸버가 로딩 계층을 통해 배칭하지 않는 한 데이터베이스 읽기 1 + 20회가 됩니다. 같은 N+1이 자리만 옮긴 것입니다. GraphQL은 문제를 개발자가 통제하는 계층으로 옮기며, 이는 진정한 진전입니다. 하지만 문제를 지우지는 않습니다.
일반적인 사용 사례
두 문제(와 그 해결책)가 가장 먼저 나타나는 곳입니다:
- 모바일 목록 화면: 전형적인 오버페칭입니다. 셀마다 필드 세 개를 렌더링하려고 전체 행을 보냅니다.
- 관계가 있는 상세 화면: 게시물 + 작성자 + 댓글입니다. 확장하거나 복합 엔드포인트를 쓰지 않으면 언더페칭 워터폴이 생깁니다.
- 저속 네트워크 시장: 두 문제 모두 가장 비싼 대가를 치릅니다. 형태를 맞춘 쿼리는 곧 접근성입니다.
- 대시보드: 원시 행을 오버페칭하거나 다섯 개 서비스에 걸쳐 언더페칭하는 집계 화면입니다. BFF의 영역입니다.
- 소비자가 다양한 공개 API: 하나의 고정된 형태로는 스마트워치 화면과 관리자 콘솔을 모두 맞출 수 없습니다. 프로젝션과 확장 파라미터로 각 호출자가 직접 조정합니다.
어떤 문제를 겪고 있습니까? 결정 매트릭스
| 네트워크 탭의 증거 | 진단 | 첫 번째 해결책 |
|---|---|---|
| 요청 하나, 큰 응답, 렌더링되는 필드는 적음 | 오버페칭 | 희소 필드셋 / select() |
| 화면당 순차 요청이 많음 | 언더페칭 | include / 확장 파라미터 |
| 요청 수가 목록 길이에 비례함 | N+1 | 배칭: 확장 또는 복합 엔드포인트 |
| 크기도 크고 수도 많음 | 둘 다(흔함) | 형태를 맞춘 쿼리 또는 GraphQL 선택 집합 |
| 보이지 않았으면 하는 응답 필드가 서버를 떠남 | 노출 | 클라이언트 측이 아니라 서버 측에서 직렬화를 줄임 |
한계와 트레이드오프
- 프로젝션은 클라이언트를 필드 목록에 결합시킵니다. UI와 어긋난
fields파라미터는 데이터 누락 버그를 일으킵니다. 생성된 타입과 코드 리뷰가 선택한 필드를 UI와 어긋나지 않게 지켜 줍니다. - 확장은 과잉 교정이 될 수 있습니다. 자주 호출되는 목록에
include=comments를 붙이면 포함된 관계 데이터가 메가바이트 단위로 전송될 수 있습니다. 확장된 응답에는 자체 페이지네이션과 깊이 제한이 필요합니다. - 목적에 맞춘 엔드포인트는 늘어납니다. 화면별 엔드포인트는 가져오기 문제를 고치는 대신 엔드포인트 난립이라는 유지보수 청구서를 만듭니다. BFF는 그 난립을 소유한 계층 하나로 모으지만, 그 계층을 운영하는 비용이 듭니다.
- 서버 측 유연성에는 대가가 있습니다. 임의의 프로젝션과 확장은 캐싱(형태마다 캐시 키가 됨)과 인가(모든 조합이 안전하게 제공될 수 있어야 함)를 복잡하게 만듭니다.
- 두 문제는 모델링 신호이기도 합니다. 깊은 다듬기나 include 다섯 개가 필요한 화면은 리소스 형태가 잘못되었다고 말하고 있는지도 모릅니다. 때로는 가져오기가 아니라 데이터 모델을 고쳐야 합니다.
Back4app에서의 오버페칭과 언더페칭
Back4app은 관리형 데이터베이스, 자동 생성되는 REST 및 GraphQL API, 인증, 파일 스토리지, Cloud Code 서버리스 함수를 결합한 오픈소스 Backend as a Service(BaaS) 플랫폼입니다. 두 해결책 모두 모든 SDK에 쿼리 기본 요소로 제공되며, 위의 코드 탭이 패턴의 전부입니다. select()가 희소 필드셋이고 include()가 확장 파라미터이며, 둘을 함께 쓰면 자동 생성되는 REST API를 상대로 1 + N 워터폴이 형태를 맞춘 왕복 한 번으로 바뀝니다. 클라이언트가 응답 형태를 완전히 통제하고 싶다면 같은 데이터를 GraphQL 선택 집합으로 조회할 수 있고, 화면에 서버 측 집계가 필요하다면 Cloud Code 함수가 파일 하나로 작성하는 복합 엔드포인트가 됩니다. 엔드포인트를 다시 설계해서가 아니라 관용적인 쿼리로 가져오기가 화면에 맞춰집니다.
자주 묻는 질문
오버페칭이란 무엇입니까?
API가 당장의 작업에 클라이언트가 필요로 하는 것보다 많은 데이터를 반환하는 것입니다. 필드 세 개를 렌더링하는 프로필 화면이 필드 마흔 개를 받는 식입니다. 그 낭비는 네 번 치러집니다. 서버의 직렬화, 네트워크 전송, 클라이언트의 파싱, 그리고 모바일에서는 배터리와 종량제 데이터입니다. 어떤 클라이언트도 보면 안 되는 필드가 노출될 수도 있습니다.
언더페칭이란 무엇입니까?
하나의 엔드포인트가 화면을 렌더링하기에 충분한 데이터를 반환하지 않아, 클라이언트가 추가 요청을 보내 화면을 조립하는 것입니다. 추가 호출 하나하나가 완전한 네트워크 왕복입니다. 목록을 가져온 뒤 항목마다 상세 정보를 가져오는 식으로 호출이 순차적이면 지연 시간이 누적되어 N+1 요청 문제가 됩니다.
오버페칭과 언더페칭의 차이는 무엇입니까?
방향입니다. 오버페칭은 응답마다 너무 많이 담는 것이고 비용은 바이트입니다. 언더페칭은 응답마다 너무 적게 담는 것이고 비용은 왕복입니다. 둘 다 같은 뿌리에서 자랍니다. 한 번 설계된 고정된 응답 형태를 요구 사항이 서로 다른 화면들이 소비한다는 점입니다. 많은 API가 같은 화면에서 두 문제를 동시에 겪습니다.
GraphQL은 오버페칭과 언더페칭을 해결합니까?
HTTP 계층에서는 대체로 해결합니다. 선택 집합(selection set)은 요청한 필드만 가져오고, 중첩 쿼리는 관련 데이터를 한 번의 요청으로 모읍니다. 하지만 자동은 아닙니다. 필드를 넉넉하게 요청하는 클라이언트는 오버페칭을 다시 만들고, 단순한 리졸버는 데이터베이스를 상대로 리졸버 수준의 N+1이라는 언더페칭을 다시 만듭니다. 배칭 로더는 바로 이것을 고치기 위해 존재합니다.
REST API에서 오버페칭을 어떻게 피합니까?
직접적인 해결책은 희소 필드셋(sparse fieldset)입니다. 화면이 렌더링하는 열만 프로젝션하는 fields 파라미터(또는 SDK 쿼리 빌더의 select())입니다. 페이지네이션은 목록 크기에 상한을 두고, 목적에 맞춘 엔드포인트는 응답을 실제 화면에 맞추며, 압축은 남은 것을 줄입니다. 다만 비대한 응답을 압축하는 것은 완화책이지 치료법이 아닙니다.
GraphQL로 전환하지 않고 언더페칭을 어떻게 고칩니까?
관련 객체를 하나의 응답에 포함시키는 확장 파라미터(include 또는 expand), 리소스를 연관 데이터와 함께 보내는 복합 문서(compound document), 한 화면에 필요한 것을 서버 측에서 집계하는 복합 엔드포인트, 그리고 아키텍처 차원에서는 모바일 네트워크 너머가 아니라 데이터 가까이에서 조립을 수행하는 BFF(backend-for-frontend) 계층입니다.
N+1 문제는 언더페칭과 어떤 관계입니까?
N+1은 규모가 커진 언더페칭입니다. N개 항목의 목록을 한 번 요청한 뒤, 각 항목의 상세 정보를 위해 후속 요청을 N번 보냅니다. 같은 형태가 모든 계층에서 반복됩니다. REST 엔드포인트를 호출하는 HTTP 클라이언트, 데이터베이스를 호출하는 GraphQL 리졸버, 관계를 지연 로딩하는 ORM이 그렇습니다. 해결책은 언제나 같은 발상으로, N을 하나로 배칭하는 것입니다.
오버페칭은 왜 보안 위험입니까?
화면이 렌더링하지 않는 필드도 네트워크를 건너가며, 누구나 개발자 도구를 열어 읽을 수 있습니다. 내부 플래그, 이메일 주소, 원가 데이터가 이렇게 유출됩니다. 보안 분류 체계는 이를 과도한 데이터 노출(excessive data exposure)로 분류하며, 최소 권한 원칙은 권한과 마찬가지로 응답 본문에도 적용됩니다.