eatthefrog
[GraphQL] 설계도: 배열 필드 페이징 본문
GraphQL의 가장 큰 장점은 클라이언트가 원하는 데이터를 깊게(Nested) 파고들며 한 번에 가져올 수 있다는 것입니다. 하지만 이 장점은 양날의 검과 같습니다. 특히 배열을 반환하는 중첩 필드에서 페이징이 빠져 있다면, 이는 서버를 마비시킬 수 있는 '데이터 폭탄'이 됩니다.
1. 문제의 발단: "뒷문이 열려 있다"
보통 우리는 getItems(limit: 10)와 같은 탑레벨(Top-level) 쿼리에는 페이징을 엄격히 적용합니다. 하지만 다음과 같은 중첩 쿼리는 어떨까요?
query AttackQuery {
getClass {
id
students { # ⚠️ 여기서 페이징이 없다면?
id
name
}
}
}
만약 특정 사용자가 3년간 기록한 2,000개의 학생 데이터를 가지고 있다면, 위 쿼리는 단 한 번의 호출로 2,000개의 레코드를 DB에서 읽어 메모리에 올리고 JSON으로 변환하게 만듭니다. 이는 전형적인 데이터량 공격(Data Volume Attack)의 통로가 됩니다.
2. 배열 필드 페이징이란?
배열 필드 페이징(Array Field Pagination)은 리스트를 반환하는 필드에 직접 인자를 두어, 클라이언트가 가져올 데이터의 범위를 제한하도록 강제하는 기법입니다.
핵심 구현 방식
- Offset-based: limit와 offset을 사용하여 페이지 단위로 끊어 가져옵니다. (가장 직관적)
- Cursor-based (Relay): 특정 포인터(Cursor)를 기준으로 first, after를 사용하여 가져옵니다. (무한 스크롤 및 대용량 처리에 최적화)
- 페이징이 없을 때: 서버는 meals 필드의 크기를 알 수 없어 대략적인 점수만 매깁니다.
- 페이징이 있을 때: stuent(limit: 50)라는 요청을 받으면, 서버는 50 * (학생 1개당 점수)로 동적인 복잡도를 계산하여 1,000점(최대 한도)이 넘으면 실행 전 차단할 수 있습니다.
3. 보안과 복잡도(Complexity)의 시너지
배열 필드 페이징은 단순한 편의 기능을 넘어, 서버 보안의 핵심인 쿼리 복잡도 검사를 완성시킵니다.
4. 예제 스키마 개선 가이드 (Before & After)
[개선 전]
GraphQL
type Class {
id: ID!
students: [Students!]! # ⚠️ 위험: 전체 데이터 반환
}
[개선 후]
GraphQL
type User {
id: ID!
# 기본값을 설정하여 클라이언트가 인자를 누락해도 서버를 보호합니다.
students(limit: Int = 10, offset: Int = 0): [Student!]!
}
5. 결론: "모든 배열은 페이징되어야 한다"
GraphQL 서버 보안의 황금률은 "바운더리가 없는 리스트(Unbounded Lists)를 허용하지 않는 것"입니다.
- 깊이 제한(Depth Limit)으로 쿼리의 층수를 막고,
- 배열 필드 페이징으로 각 층의 데이터 개수를 통제하며,
- 동적 복잡도 검사로 최종적인 실행 권한을 결정해야 합니다.
지금 바로 여러분의 스키마를 확인해 보세요. 혹시 [Type!]! 형태의 필드 뒤에 페이징 인자가 빠져 있지는 않나요? 그곳이 바로 공격자가 노리는 '열린 뒷문'일 수 있습니다.
'백엔드 노트' 카테고리의 다른 글
| [Security] Access Token과 Refresh Token, 왜 둘 다 써야 할까? (0) | 2026.01.04 |
|---|---|
| [GraphQL] 보안 : 쿼리 깊이 제한, 일괄 요청 제한 (0) | 2026.01.03 |
| [GraphQL] 스키마 폴링과 Introspection 보안 (0) | 2026.01.03 |
| MongoDB 연결 옵션 Deprecation 경고 해결하기 (0) | 2025.12.18 |
| MongoDB를 잘 사용했을 때 얻는 이점 (0) | 2025.11.20 |