📌 데이터베이스 깊이 파기 시리즈 — 2편
들어가며
1편에서 RDB, 문서 DB, 그래프 DB의 데이터 모델을 비교했습니다. 데이터를 어떤 구조로 저장하느냐의 문제였습니다. 이번 편에서는 그 반대편 질문을 다룹니다 — 저장된 데이터를 어떻게 꺼내느냐, 즉 질의 언어의 문제입니다.
질의 언어는 크게 선언형(Declarative)과 명령형(Imperative) 접근으로 나뉩니다. SQL은 선언형의 대표주자이고, MapReduce는 그 중간 어딘가에 위치합니다. 그래프 질의 언어들은 각각 선언형(Cypher, SPARQL)과 명령형(Gremlin)으로 갈라집니다. 이 글에서는 각 질의 언어가 어떤 철학 위에 서 있는지, 그리고 데이터 모델에 따라 왜 서로 다른 질의 언어가 필요한지를 살펴보겠습니다.
1. 선언형 vs 명령형 — 질의 언어의 두 가지 철학
질의 언어의 차이를 이해하려면, 먼저 선언형과 명령형의 근본적인 차이를 짚어야 합니다.
명령형: "어떻게" 할지를 지시한다
명령형 접근에서는 프로그래머가 데이터를 처리하는 절차를 단계별로 기술합니다. 어떤 순서로 반복하고, 어떤 조건으로 필터링하고, 어떤 변수에 결과를 누적할지를 모두 직접 지정합니다.
// 명령형: 상어 종류별 관측 수를 직접 구한다
function countSharks(observations) {
const counts = {};
for (const obs of observations) {
if (obs.family === "Sharks") {
if (!counts[obs.species]) {
counts[obs.species] = 0;
}
counts[obs.species]++;
}
}
return counts;
}
이 코드는 정확히 무엇을 하는지 한 줄씩 따라가야 이해할 수 있습니다. 반복문의 순서, 조건 분기, 변수 갱신까지 모든 것이 프로그래머의 책임입니다.
선언형: "무엇을" 원하는지만 말한다
선언형 접근에서는 원하는 결과의 조건만 기술합니다. 그 결과를 얻기 위한 구체적인 실행 절차는 시스템(데이터베이스 엔진, 질의 최적화기 등)이 알아서 결정합니다.
-- 선언형: 같은 결과를 한 문장으로 표현한다
SELECT species, COUNT(*)
FROM observations
WHERE family = 'Sharks'
GROUP BY species;
이 SQL 문은 "상어과에 속하는 관측 데이터를 종별로 묶어서 개수를 세라"는 의도만 전달합니다. 내부적으로 어떤 인덱스를 쓸지, 병렬로 처리할지, 어떤 순서로 스캔할지는 데이터베이스 엔진의 질의 최적화기(Query Optimizer)가 판단합니다.
선언형이 갖는 구조적 이점
선언형 질의가 단순히 "코드가 짧다"는 것 이상의 의미를 갖는 이유는 다음과 같습니다.
최적화의 자유도 — 선언형 질의는 실행 방법을 지정하지 않기 때문에, 데이터베이스 엔진이 내부적으로 실행 전략을 자유롭게 바꿀 수 있습니다. 새로운 인덱스가 추가되거나, 옵티마이저가 개선되면 같은 질의도 더 빠르게 실행될 수 있습니다. 질의를 수정할 필요가 없습니다.
병렬 처리 친화성 — 명령형 코드는 특정 순서로 실행되도록 작성되는 경우가 많아, 병렬화하려면 코드를 다시 작성해야 합니다. 선언형 질의는 실행 순서를 지정하지 않으므로 엔진이 자동으로 병렬 실행 계획을 수립할 수 있습니다.
추상화 수준 — 스토리지 엔진이 B-Tree에서 LSM-Tree로 바뀌어도(2편 참고), 선언형 질의는 영향을 받지 않습니다. 저장 계층과 질의 계층이 깔끔하게 분리됩니다.

2. SQL — 선언형 질의의 대표
SQL(Structured Query Language)은 1970년대 관계형 모델과 함께 등장한 이후, 반세기가 넘도록 데이터 질의의 표준 자리를 지키고 있습니다. SQL이 오랫동안 살아남은 핵심 이유는, 관계 대수(Relational Algebra)라는 수학적 기반 위에 선언형 인터페이스를 얹었기 때문입니다.
관계 대수와 SQL의 관계
관계 대수는 릴레이션(테이블)에 대한 연산을 수학적으로 정의합니다. SQL의 각 절(clause)은 관계 대수의 연산에 대응됩니다.
| 관계 대수 연산 | SQL 대응 | 의미 |
| σ (Selection) | WHERE |
조건에 맞는 행 필터링 |
| π (Projection) | SELECT 컬럼 |
특정 열만 추출 |
| ⋈ (Join) | JOIN |
두 테이블 결합 |
| γ (Aggregation) | GROUP BY |
그룹별 집계 |
이 수학적 기반 덕분에, 옵티마이저는 질의를 동치인 다른 형태로 변환하면서 최적의 실행 계획을 탐색할 수 있습니다. 예를 들어, WHERE 조건을 JOIN 이전에 적용하면(Predicate Pushdown) 조인할 데이터 양이 줄어들어 성능이 향상됩니다. 이런 최적화가 가능한 이유는 SQL이 실행 순서를 지정하지 않는 선언형이기 때문입니다.
SQL의 실행 흐름
SQL 질의가 실행되는 과정은 파서 → 옵티마이저 → 실행 엔진 순서로 진행됩니다. 파서가 구문을 분석하여 AST(Abstract Syntax Tree)를 생성하면, 옵티마이저가 이를 바탕으로 실행 계획 후보들을 만들어냅니다. 같은 질의에 대해 수십~수백 개의 실행 계획 후보가 존재할 수 있고, 옵티마이저는 테이블 크기, 인덱스 유무, 데이터 분포 통계 등을 종합하여 가장 비용이 낮은 계획을 선택합니다. 이 전체 과정에 프로그래머가 개입할 필요가 없다는 것이 선언형의 핵심입니다.
웹에서의 선언형 유사 사례: CSS
선언형 접근은 데이터베이스에만 있는 것이 아닙니다. 웹 개발에서 CSS도 본질적으로 선언형입니다.
/* 선언형: 선택된 항목의 스타일을 "선언"한다 */
li.selected > p {
background-color: blue;
}
이 CSS는 "selected 클래스를 가진 li의 자식 p 요소에 파란 배경을 적용하라"는 의도만 기술합니다. 같은 일을 JavaScript로 명령형으로 작성하면, DOM을 직접 순회하면서 조건을 검사하고 스타일을 하나씩 변경하는 코드를 작성해야 합니다.
DOM 구조가 바뀌면 코드도 깨질 수 있고, 브라우저의 렌더링 최적화를 활용하기도 어렵습니다. SQL과 동일한 패턴입니다 — 선언형은 유지보수성과 최적화 가능성 면에서 구조적 우위를 갖습니다.
3. MapReduce — 선언형도 명령형도 아닌 중간 지대
MapReduce는 Google이 2004년 논문으로 발표한 대규모 데이터 처리 프로그래밍 모델입니다. 완전한 선언형도, 완전한 명령형도 아닌 독특한 위치에 있습니다.
MapReduce의 기본 구조
MapReduce는 이름 그대로 map과 reduce 두 단계로 구성됩니다.
map 함수 — 입력 데이터의 각 레코드를 받아서, 키-값 쌍(key-value pair)을 출력합니다. 필터링과 변환을 담당합니다.
reduce 함수 — 같은 키를 가진 값들을 모아서 하나의 결과로 합산합니다. 집계를 담당합니다.
앞서 SQL로 작성한 "상어 종별 관측 수" 질의를 MapReduce로 표현하면 이렇습니다.
// map: 각 관측 데이터에서 상어만 필터링하여 (종, 1) 쌍을 내보냄
function map(doc) {
if (doc.family === "Sharks") {
emit(doc.species, 1); // key: 종 이름, value: 1
}
}
// reduce: 같은 종끼리 모인 값들을 합산
function reduce(key, values) {
return values.reduce((sum, val) => sum + val, 0);
}
map 함수는 각 문서를 독립적으로 처리하고, reduce 함수는 같은 키로 그룹핑된 결과를 집계합니다. 같은 결과를 SQL 한 줄로 표현할 수 있는 것에 비하면 코드량이 늘어납니다.
왜 "중간 지대"인가
MapReduce가 선언형과 명령형의 중간에 위치하는 이유를 정리하겠습니다.
명령형에 가까운 부분 — map과 reduce 함수의 내부 로직은 프로그래머가 직접 범용 프로그래밍 언어로 작성합니다. 필터링 조건, 데이터 변환 방식, 집계 로직을 코드로 구현해야 합니다. SQL처럼 의도만 선언하는 것이 아닙니다.
선언형에 가까운 부분 — map과 reduce 함수를 언제, 어디서, 몇 개의 노드에서 실행할지는 프레임워크가 결정합니다. 데이터 분배, 셔플링(같은 키끼리 모으기), 장애 복구 등의 분산 처리 로직을 프로그래머가 작성할 필요가 없습니다.
정리하면, MapReduce에서 함수 내부 로직은 프로그래머가 직접 작성하지만(명령형), 함수의 실행 스케줄링과 분산 처리는 프레임워크가 제어합니다(선언형). 이 이중 구조가 MapReduce를 "중간 지대"에 위치시킵니다.
MapReduce의 제약: 순수 함수
MapReduce의 map과 reduce 함수에는 중요한 제약이 있습니다. 이 함수들은 반드시 순수 함수(Pure Function)여야 합니다. 즉, 입력 데이터만 사용해야 하고, 외부 상태를 변경하거나 부수 효과(Side Effect)를 일으켜서는 안 됩니다.
이 제약이 있기 때문에 프레임워크가 함수를 어떤 순서로든, 어떤 노드에서든, 실패 시 재실행까지 자유롭게 다룰 수 있습니다. 순수 함수가 아니라면 재실행했을 때 결과가 달라질 수 있어 분산 환경에서 안정적으로 동작하지 않습니다.
SQL vs MapReduce: 동일 질의 비교
동일한 질의를 나란히 놓고 비교하면 차이가 명확해집니다.

| 비교 항목 | SQL | MapReduce |
| 추상화 수준 | 결과만 선언 | 처리 로직을 함수로 구현 |
| 최적화 | 옵티마이저가 자동 수행 | 프로그래머가 작성한 대로 실행 |
| 표현력 | SQL 문법 범위 내 | 범용 언어의 모든 기능 사용 가능 |
| 학습 곡선 | SQL 문법 학습 | 프로그래밍 + 분산 처리 개념 이해 |
| 사용성 | 한 문장으로 질의 가능 | 두 개 이상의 함수 작성 필요 |
MapReduce의 현실적 위치
MapReduce는 Hadoop 생태계에서만 쓰이는 것이 아닙니다. 1편에서 다룬 문서 DB인 MongoDB도 mapReduce() 메서드를 통해 MapReduce를 지원했습니다. 문서 모델 위에서 집계 질의를 수행할 때, SQL의 GROUP BY에 해당하는 기능을 MapReduce로 구현할 수 있었던 것입니다. 다만 MongoDB 역시 사용성 문제를 인식하고, 이후 Aggregation Pipeline이라는 선언형에 가까운 질의 방식을 도입하여 MapReduce를 대체하는 방향으로 발전했습니다.
비슷한 흐름이 Hadoop 생태계에서도 반복되었습니다. MapReduce가 SQL보다 작성이 번거롭다는 점은 실무에서 큰 부담이었고, 이를 해결하기 위해 Hive, Pig, Spark SQL 같은 도구들이 등장했습니다. 이들은 공통적으로 SQL 또는 SQL과 유사한 선언형 인터페이스를 제공하고, 내부적으로 MapReduce(또는 유사한 분산 처리 엔진)로 변환하여 실행합니다.
결국 MapReduce 위에 다시 선언형 계층을 얹는 방향으로 수렴한 것입니다. MongoDB든 Hadoop이든, 대규모 데이터 처리에서 선언형 접근의 생산성 이점은 일관되게 확인됩니다.
4. 그래프 질의 언어 — 관계 탐색에 특화된 질의
1편에서 그래프 데이터 모델이 다대다 관계와 복잡한 연결 구조에 적합하다는 점을 살펴봤습니다. 그래프 모델에 대한 질의는 일반적인 관계형 질의와는 본질적으로 다른 문제를 풀어야 합니다 — 바로 그래프 탐색(Graph Traversal)입니다.
SQL로 그래프를 탐색하면 생기는 문제
먼저, 왜 SQL만으로는 부족한지 살펴보겠습니다. "Alice가 아는 사람 중에서, 미국에 사는 사람을 모두 찾아라"는 단순한 질의를 SQL로 작성하면 이렇습니다.
-- 1단계 깊이: Alice의 직접적인 지인
SELECT p2.name
FROM person p1
JOIN knows ON knows.person_id = p1.id
JOIN person p2 ON p2.id = knows.friend_id
JOIN lives_in ON lives_in.person_id = p2.id
JOIN location ON location.id = lives_in.location_id
WHERE p1.name = 'Alice'
AND location.country = 'USA';
여기까지는 괜찮습니다. 하지만 "친구의 친구" 까지로 범위를 넓히면 JOIN이 한 단계 더 추가됩니다. "친구의 친구의 친구"까지라면 또 한 단계. 탐색 깊이가 가변적이라면 — 예를 들어 "Alice에서 출발하여 몇 단계든 연결된 사람 중 미국에 사는 사람" — SQL로는 질의를 작성하는 것 자체가 매우 어려워집니다.
SQL:1999부터 도입된 WITH RECURSIVE를 사용하면 가능하긴 합니다.
WITH RECURSIVE connected AS (
-- 시작점: Alice
SELECT friend_id AS person_id, 1 AS depth
FROM knows
WHERE person_id = (SELECT id FROM person WHERE name = 'Alice')
UNION ALL
-- 재귀: 연결된 사람의 연결된 사람
SELECT k.friend_id, c.depth + 1
FROM connected c
JOIN knows k ON k.person_id = c.person_id
WHERE c.depth < 5 -- 깊이 제한
)
SELECT DISTINCT p.name
FROM connected c
JOIN person p ON p.id = c.person_id
JOIN lives_in ON lives_in.person_id = p.id
JOIN location ON location.id = lives_in.location_id
WHERE location.country = 'USA';
동작은 하지만, 코드가 복잡해지고 가독성이 떨어집니다. 관계형 모델 위에서 그래프 탐색을 억지로 표현하고 있기 때문입니다.
Cypher — 패턴 매칭 기반의 선언형 그래프 질의
Cypher는 Neo4j에서 만든 그래프 질의 언어로, 현재 가장 널리 사용되고 있습니다. 핵심 아이디어는 ASCII 아트 형태의 패턴 매칭입니다.
같은 질의를 Cypher로 작성하면 이렇습니다.
// Alice에서 출발하여 KNOWS 관계로 연결된 사람 중 미국 거주자
MATCH (alice:Person {name: 'Alice'})-[:KNOWS*1..5]->(friend:Person)
-[:LIVES_IN]->(loc:Location {country: 'USA'})
RETURN DISTINCT friend.name;
-[:KNOWS*1..5]-> 부분이 핵심입니다. *1..5는 KNOWS 관계를 1~5단계까지 가변적으로 따라가라는 의미입니다. SQL에서 WITH RECURSIVE로 복잡하게 표현했던 가변 깊이 탐색을 한 줄의 패턴으로 표현합니다.
Cypher의 패턴 문법을 분해하면 이렇습니다.
(노드:레이블 {속성}) — 둥근 괄호는 노드
-[:관계유형]-> — 화살표는 방향이 있는 간선
-[:관계유형*min..max]-> — 가변 깊이 탐색
이 문법이 직관적인 이유는, 그래프의 시각적 구조를 텍스트로 그대로 표현하기 때문입니다. 다이어그램에서 노드를 동그라미로, 간선을 화살표로 그리는 것과 같은 방식입니다.
SPARQL — 트리플 패턴의 세계
SPARQL은 RDF(Resource Description Framework) 데이터를 위한 W3C 표준 질의 언어입니다. RDF는 모든 데이터를 (주어, 서술어, 목적어) 형태의 트리플(triple)로 표현합니다.
예를 들어 "Alice는 서울에 산다"는 사실은 이렇게 표현됩니다.
(Alice, livesIn, Seoul)
SPARQL은 이런 트리플 패턴을 매칭하는 방식으로 질의합니다.
PREFIX : <http://example.org/>
SELECT ?friendName WHERE {
:Alice :knows+ ?friend . # Alice에서 knows 관계를 1회 이상 따라감
?friend :livesIn ?loc . # 그 친구가 사는 곳
?loc :country "USA" . # 그 곳의 나라가 USA
}
SPARQL에서 :knows+의 +는 "1회 이상 반복"을 의미하며, Cypher의 *1..과 같은 역할을 합니다.
SPARQL과 Cypher의 가장 큰 차이는 데이터 모델에 있습니다. Cypher는 속성 그래프(Property Graph) 모델을 사용하여 노드와 간선 모두에 속성(key-value)을 가질 수 있습니다. 반면 SPARQL의 RDF는 순수하게 트리플만으로 모든 것을 표현합니다. 노드의 속성도 별도의 트리플로 분리됩니다.
| 비교 항목 | Cypher (속성 그래프) | SPARQL (RDF) |
| 데이터 단위 | 노드와 간선에 속성 부여 | (주어, 서술어, 목적어) 트리플 |
| 질의 방식 | 시각적 패턴 매칭 | 트리플 패턴 매칭 |
| 표준화 | openCypher / GQL(ISO 표준 진행중) | W3C 표준 |
| 주요 사용처 | Neo4j, Amazon Neptune | Wikidata, DBpedia, 시맨틱 웹 |
Gremlin — 그래프 위를 걸어가는 명령형 순회
Gremlin은 Apache TinkerPop 프레임워크의 그래프 순회(traversal) 언어입니다. Cypher와 SPARQL이 선언형인 것과 달리, Gremlin은 명령형에 가깝습니다. 그래프 위를 한 발짝씩 걸어가는 방식으로 질의를 구성합니다.
// Gremlin: 단계별로 그래프를 순회한다
g.V().has('name', 'Alice') // 1. Alice 노드에서 시작
.repeat(out('KNOWS')) // 2. KNOWS 간선을 따라 반복 이동
.times(5) // 3. 최대 5단계까지
.out('LIVES_IN') // 4. LIVES_IN 간선을 따라 이동
.has('country', 'USA') // 5. country가 USA인 노드 필터링
.path() // 6. 경로 전체를 반환
Gremlin의 각 메서드 호출은 현재 위치에서 다음 위치로 한 단계 이동하는 것에 해당합니다. 마지막의 .path()는 단순히 최종 노드만 반환하는 것이 아니라, Alice에서 출발하여 도착지까지 거쳐온 경로 전체를 반환합니다. "누구를 통해 연결되었는가"까지 알 수 있으므로, 소셜 네트워크 분석이나 추천 시스템에서 유용합니다. 이런 단계별 순회 방식은 복잡한 경로 조건이나 알고리즘적 탐색(최단 경로, 가중치 기반 탐색 등)을 표현할 때 특히 유리합니다.

5. 전체 비교: 같은 질문, 다른 언어
지금까지 살펴본 질의 언어들을 하나의 표로 정리하겠습니다.
| 비교 항목 | SQL | MapReduce | Cypher | SPARQL | Gremlin |
| 접근 방식 | 선언형 | 선언형 + 명령형 혼합 | 선언형 | 선언형 | 명령형 |
| 데이터 모델 | 관계형 (테이블) | 비정형 (키-값) | 속성 그래프 | RDF (트리플) | 속성 그래프 |
| 핵심 메커니즘 | 관계 대수 연산 | map/reduce 함수 조합 | 패턴 매칭 | 트리플 패턴 매칭 | 그래프 순회 |
| 가변 깊이 탐색 | WITH RECURSIVE (복잡) | 다단계 job 체이닝 (매우 복잡) | *min..max (간결) |
+, * (간결) |
.repeat().times() (직관적) |
| 자동 최적화 | 옵티마이저가 수행 | 없음 (코드 그대로 실행) | 옵티마이저가 수행 | 옵티마이저가 수행 | 제한적 |
| 병렬 처리 | 엔진이 자동 판단 | 프레임워크가 자동 분배 | 엔진 의존 | 엔진 의존 | 엔진 의존 |
| 대표 시스템 | PostgreSQL, MySQL | Hadoop, MongoDB | Neo4j | Wikidata, Jena | JanusGraph, Neptune |
데이터 모델과 질의 언어의 대응 관계
1편에서 다룬 데이터 모델과 이번 편의 질의 언어를 연결하면 다음과 같은 그림이 됩니다.

관계형 모델에는 SQL이, 문서 모델에는 MongoDB의 질의 API나 MapReduce가, 그래프 모델에는 Cypher·SPARQL·Gremlin이 자연스럽게 대응됩니다. 각 질의 언어는 해당 데이터 모델의 강점을 최대한 활용할 수 있도록 설계되어 있습니다. 관계형 모델에서 JOIN이 자연스러운 것처럼, 그래프 모델에서는 가변 깊이 탐색이 자연스럽습니다.
마무리
질의 언어의 선택은 단순한 문법 선호의 문제가 아닙니다. 데이터 모델과 질의 언어는 함께 움직이며, 둘의 조합이 어떤 종류의 질문에 자연스럽게 답할 수 있는지를 결정합니다.
SQL은 선언형의 이점을 가장 성숙하게 보여주는 언어입니다. 반세기 넘게 살아남은 이유는, 옵티마이저를 통한 자동 최적화 덕분에 프로그래머가 "무엇을" 원하는지에만 집중할 수 있기 때문입니다.
MapReduce는 분산 처리를 위한 과도기적 모델이었습니다. 범용 코드의 유연성을 제공했지만, 결국 그 위에 Hive, Spark SQL, MongoDB Aggregation Pipeline 같은 선언형 계층이 다시 올라가는 방향으로 수렴했습니다.
그래프 질의 언어들은 관계 탐색이라는 특화된 영역에서 SQL의 구조적 한계를 해결합니다. 가변 깊이 탐색을 한 줄로 표현할 수 있다는 것은, 단순히 편의성의 차이가 아니라 데이터 모델에 맞는 질의 언어를 사용하는 것의 본질적 이점입니다.
다음 편에서는 스토리지 엔진의 내부에 대해서 다뤄보겠습니다.
← 이전 편: 1편: RDB vs 문서 DB vs 그래프 DB — 스키마 변동성과 데이터 지역성 관점에서
→ 다음 편: 3편: B-Tree vs LSM-Tree — RDB와 NoSQL은 왜 다른 스토리지 엔진을 선택했을까
[Data] RDB vs 문서 DB vs 그래프 DB — 스키마 변동성과 데이터 지역성 관점에서
들어가며데이터베이스를 선택할 때 "그냥 RDB 쓰면 되지 않나?"라고 생각하기 쉽습니다. 실제로 대부분의 경우 RDB는 훌륭한 선택입니다. 하지만 요구사항에 따라 문서 DB나 그래프 DB가 확실한 이
dmoritle.tistory.com
'Data' 카테고리의 다른 글
| [Data] 보조 색인에서 인메모리 DB까지 — 데이터를 찾는 다양한 전략 (0) | 2026.03.31 |
|---|---|
| [Data] Elasticsearch와 Lucene — 세그먼트, 역인덱스, 그리고 벡터 검색 (0) | 2026.03.28 |
| [Data] B-Tree vs LSM-Tree — RDB와 NoSQL은 왜 다른 스토리지 엔진을 선택했을까 (0) | 2026.03.28 |
| [Data] RDB vs 문서 DB vs 그래프 DB — 스키마 변동성과 데이터 지역성 관점에서 (1) | 2026.03.28 |