대규모 처리 환경에서 자료구조를 고르는 기준
대규모 데이터 처리 환경에서 해시, 트리, 힙, 그래프 등 자료구조를 연산 특성·메모리·동시성·저장 계층 기준으로 선택하는 방법
2026-08-14 · 최초 발행 2024-04-29
처리 경로가 자료구조 선택을 결정한다
대규모 데이터를 다루는 시스템에서 자료구조는 알고리즘의 시간·공간 복잡도와 함께 성능, 안정성, 운영 특성을 좌우한다. 단순히 탐색 속도만 비교할 일이 아니다. 데이터가 메모리에 어떻게 배치되는지, 동시 접근이 얼마나 잦은지, 저장 장치에서 어떤 I/O가 발생하는지까지 구조 선택에 포함된다.
자료구조는 데이터를 저장·조회·수정·삭제하기 위한 조직 방식과 연산 집합의 결합이다. 이때 Map이나 Set 같은 추상 자료형(ADT)과 Hash Table, B-Tree 같은 구현체를 분리해 두면 요구사항 변화에 대응하기 쉬워진다.
평균·최악 시간 복잡도, 공간 복잡도, 상수 항, 메모리 지역성은 함께 봐야 한다. 배열처럼 연속된 메모리를 쓰는 구조는 CPU 캐시와 SIMD 최적화에 유리한 반면, 연결 리스트와 트리는 삽입·삭제의 유연성을 제공하지만 단편화 위험을 안고 있다. 휘발성 메모리용 구조와 B+트리·LSM 트리처럼 저장 장치에 맞춘 구조도 같은 기준으로 볼 수 없다.
메모리와 동시성까지 포함한 판단 기준
연속 메모리 기반 구조는 캐시 적중률이 높고 SIMD 최적화에 유리하다. 연결 구조는 변경이 잦은 데이터에 유연하지만 포인터 오버헤드와 메모리 단편화를 고려해야 한다. 64비트 포인터 1개를 노드마다 두고 1억 노드를 사용하면 약 0.8 GB의 추가 메모리가 필요하다.
탐색, 삽입, 삭제, 스캔, 범위 질의는 각기 다른 비용을 갖는다. 빅오 표기만으로는 충분하지 않다. 리사이즈나 리밸런스처럼 간헐적으로 발생하는 비용은 지연을 크게 흔들 수 있으므로 별도로 관리해야 한다. 포인터와 패딩, 로드 팩터, 압축 키와 비트셋 같은 압축 인덱스도 공간 효율에 영향을 준다.
동시성에서는 전역 락, 버킷 락, 노드 단위 락, 락 프리, RCU, Copy-on-Write 중에서 경합 수준과 일관성 요구에 맞는 방식을 골라야 한다. 쓰기 경쟁, ABA 문제, 메모리 장벽은 구현 단계에서 피할 수 없는 저수준 이슈다.
저장 계층도 구분한다. 해시 테이블과 스킵 리스트는 메모리 사용에 적합하고, B+트리와 LSM 트리는 디스크 특성을 반영한다. LSM은 순차 쓰기, B+트리는 랜덤 읽기에 각각 강점이 있지만 서로 다른 트레이드오프를 가진다.
구조별 성능과 운용 특성
| 구조 | 성능(탐색/삽입) | 확장성 | 일관성 | 안정성 | 운영 편의 |
|---|---|---|---|---|---|
| 배열 | O(1)/O(n) | 중 | 높음(불변 시) | 높음 | 매우 높음 |
| 연결 리스트 | O(n)/O(1) | 중 | 중 | 중 | 보통 |
| 해시 테이블 | 평균 O(1)/O(1) | 높음(리사이즈 필요) | 낮음(순서/범위 부재) | 중 | 높음 |
| 균형 트리(RB/B+) | O(log n)/O(log n) | 높음 | 높음(정렬/범위) | 높음 | 보통 |
| 힙 | O(n)/O(log n) | 중 | 낮음(순서 제한) | 높음 | 높음 |
| 그래프(인접 리스트) | 알고리즘 의존 | 중 | 모델 의존 | 모델 의존 | 보통 |
해시 테이블은 로드 팩터 관리가 필요하다. B+트리는 저장 장치 페이지에 맞춘 최적화에 강점이 있고, 그래프의 비용은 적용하는 알고리즘에 따라 크게 달라진다.
접근 패턴에 맞춰 구조를 조합하는 경우
키-값 검색과 캐시는 해시 테이블에 LRU를 결합하는 방식이 일반적이다. 링 버퍼나 링크드 리스트를 함께 두면 평균 O(1) 조회가 가능하지만, 재해시와 LRU 유지 비용은 따로 관리해야 한다.
순위나 피드를 정렬할 때는 힙 또는 균형 트리와 오더 통계 트리를 사용할 수 있다. 상위 k를 유지하는 비용은 O(n log k)이며, 범위 페이징에서는 안정적인 정렬이 필요하다.
경로 탐색과 추천 그래프는 인접 리스트에 BFS 또는 힙 기반 다익스트라를 결합한다. 희소 그래프에서는 메모리 효율을 확보할 수 있고, 가중치 최단 경로의 비용은 O((V+E) log V)다.
데이터베이스 인덱스는 OLTP에 B+트리, 쓰기 집약 로그·시계열에는 LSM 트리를 배치할 수 있다. B+트리는 범위 질의와 순차 스캔에, LSM은 대량 쓰기와 압축에 강점이 있다. 분산 키 파티셔닝에서는 일관 해싱 링을 사용해 재배치를 줄이고, 노드 증감 시 키 이동을 O(n/k) 수준으로 관리한다.
요구사항에서 검증까지 이어지는 선택 흐름
먼저 읽기·쓰기·스캔 비율, 지연과 처리량 SLO, 데이터 규모와 성장률을 정리한다. 이어 메모리 한도, 디스크·네트워크 비용, 동시성 수준, RC·SC·최종일관성 같은 일관성 모델을 제약으로 둔다.
후보 구조는 연산 복잡도뿐 아니라 캐시 친화성, 리사이즈·리밸런스 비용, 가비지와 압축 전략으로 비교한다. 마지막에는 실제 워크로드를 재현해 벤치마크하고, 최악 조건을 유도하는 테스트와 관측치를 바탕으로 파라미터를 조정한다.
스트림의 상위 k를 유지하는 힙
전역 정렬 대신 힙을 사용하면 상위 k 항목을 O(n log k), O(k) 메모리로 유지할 수 있다.
# Python 3.11+
import heapq
def top_k(stream, k):
heap = []
for x in stream:
if len(heap) < k:
heapq.heappush(heap, x)
else:
if x > heap[0]:
heapq.heapreplace(heap, x)
return sorted(heap, reverse=True)
if __name__ == "__main__":
data = [5, 1, 7, 3, 9, 2, 8]
print(top_k(data, 3)) # [9, 8, 7]
무가중치 그래프에서 최단 경로 찾기
인접 리스트와 BFS를 결합하면 무가중치 그래프의 최단 경로를 O(V + E), O(V + E) 메모리로 구할 수 있다.
# Python 3.11+
from collections import deque, defaultdict
def bfs_shortest_path(edges, start, goal):
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
q = deque([start])
prev = {start: None}
while q:
u = q.popleft()
if u == goal:
break
for v in graph[u]:
if v not in prev:
prev[v] = u
q.append(v)
if goal not in prev:
return None
path = []
cur = goal
while cur is not None:
path.append(cur)
cur = prev[cur]
return list(reversed(path))
if __name__ == "__main__":
E = [(1,2),(2,3),(3,4),(4,5),(2,5)]
print(bfs_shortest_path(E, 1, 5)) # [1, 2, 5]
구조 선택이 남기는 운영상 차이
해시 기반 점조회를 도입하면 선형 검색의 평균 탐색 복잡도를 O(n)에서 O(1)로 전환할 수 있다. 전역 정렬로 상위 k를 유지하던 로직도 O(n log n) 대신 힙 기반 O(n log k)로 줄일 수 있다.
배열이나 슬라이스를 채택하면 같은 데이터를 링크드 구조로 유지할 때보다 메모리 사용량을 줄일 수 있다. 디스크 인덱스에서는 B+트리의 페이지 배치가 I/O 횟수를 줄이는 데 도움이 된다.
범위 질의와 정렬을 보장하는 구조를 채택하면 관련 버그 클래스를 줄일 수 있다. 리사이즈와 리밸런스 전략을 미리 정의해 두면 지연 스파이크도 완화할 수 있다. 자료구조는 해시·트리·힙·그래프 중 하나를 고르는 문제가 아니라, 연산 프로필과 제약, 저장 계층, 동시성 전략을 맞물려 설계하는 문제다.