SQL vs NoSQL: 데이터 모델·일관성·확장성 차이와 선택 기준
이 글의 핵심
NoSQL은 스키마가 유연하다는 이유로 골랐다가 조인과 트랜잭션이 필요해져 곤란해지는 일이 흔합니다. 이 글은 도큐먼트 DB와 키-값 저장소가 각각 어떤 접근 패턴에 강한지 설명하고, 성능과 확장성 비교표를 거쳐 프로젝트 성격에 따라 어떤 조합을 고를지, NoSQL에서도 스키마 설계가 필요한 이유를 정리합니다.
들어가며: 데이터베이스 선택의 중요성
데이터베이스는 한 번 고르면 바꾸기 가장 어려운 구성 요소입니다. 애플리케이션 코드는 리팩터링으로 조금씩 고칠 수 있지만, 수억 건이 쌓인 데이터를 다른 모델로 옮기려면 이중 쓰기, 백필, 검증, 전환 순서를 모두 설계해야 합니다. 그래서 “요즘 무엇이 유행하는가”보다 “우리 데이터가 어떤 모양이고, 어떤 방식으로 읽히고 쓰이는가”를 먼저 따져야 합니다.
SQL과 NoSQL의 경계도 예전만큼 선명하지 않습니다. PostgreSQL은 JSONB로 도큐먼트 저장을 잘 지원하고, MongoDB는 4.0부터 다중 문서 트랜잭션을 지원합니다. 이 글은 MySQL, PostgreSQL, MongoDB, Redis를 예로 들어 각 모델이 어떤 접근 패턴에 강한지, 어떤 가정이 깨질 때 문제가 되는지를 중심으로 비교합니다.
SQL 데이터베이스
SQL이란?
SQL(Structured Query Language) 데이터베이스는 정형화된 스키마와 관계형 모델을 사용합니다. SQL 데이터베이스 구조:
graph TB
A[데이터베이스] --> B[테이블: users]
A --> C[테이블: posts]
A --> D[테이블: comments]
B --> B1[id PK]
B --> B2[name]
B --> B3[email]
C --> C1[id PK]
C --> C2[user_id FK]
C --> C3[title]
C --> C4[content]
D --> D1[id PK]
D --> D2[post_id FK]
D --> D3[user_id FK]
D --> D4[content]
C2 -.참조.-> B1
D2 -.참조.-> C1
D3 -.참조.-> B1
MySQL
특징:
- 가장 인기 있는 오픈소스 RDBMS
- 읽기 성능 우수
- 웹 애플리케이션에 적합 예제:
-- 테이블 생성
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 데이터 삽입
INSERT INTO users (name, email) VALUES
('Alice', '[email protected]'),
('Bob', '[email protected]');
-- 조회
SELECT * FROM users WHERE name LIKE 'A%';
-- 조인
SELECT users.name, posts.title
FROM users
INNER JOIN posts ON users.id = posts.user_id;
장점:
-
빠른 읽기 성능
-
풍부한 생태계
-
쉬운 설정 단점:
-
복잡한 분석 쿼리에서 옵티마이저 선택지가 PostgreSQL보다 적은 편
-
수평 확장(샤딩)은 기본 기능이 아니라 Vitess 같은 별도 계층이나 애플리케이션 레벨 설계가 필요
MySQL(InnoDB)은 기본 키 순서로 데이터를 물리적으로 정렬해 저장하는 클러스터형 인덱스 구조라, 기본 키로 한 행을 찾거나 범위를 읽는 패턴이 매우 효율적입니다. 웹 서비스의 대부분 쿼리가 “id로 한 건 조회”이기 때문에 오랫동안 웹 애플리케이션의 기본 선택지였습니다. 반대로 UUID처럼 무작위 값을 기본 키로 쓰면 삽입할 때마다 인덱스 중간에 끼워 넣어야 해서 페이지 분할이 잦아지고 쓰기 성능이 떨어지는데, 처음 설계할 때 자주 놓치는 부분입니다.
PostgreSQL
특징:
- 고급 기능 지원 (JSON, 전문 검색, GIS)
- ACID 완벽 지원
- 복잡한 쿼리에 강함 예제:
-- JSON 지원
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
attributes JSONB
);
INSERT INTO products (name, attributes) VALUES
('Laptop', '{"brand": "Apple", "ram": 16, "ssd": 512}');
-- JSON 쿼리
SELECT * FROM products
WHERE attributes->>'brand' = 'Apple'
AND (attributes->>'ram')::int >= 16;
-- 전문 검색
CREATE INDEX idx_name_fts ON products USING gin(to_tsvector('english', name));
SELECT * FROM products
WHERE to_tsvector('english', name) @@ to_tsquery('laptop');
장점:
-
고급 기능 (JSON, 배열, GIS)
-
확장성 (Extension)
-
표준 SQL 준수 단점:
-
연결마다 프로세스를 하나씩 띄우는 구조라, 연결 수가 많은 환경에서는 PgBouncer 같은 커넥션 풀러가 사실상 필수
-
MVCC 방식 때문에 UPDATE·DELETE가 많으면 죽은 튜플이 쌓여 VACUUM 튜닝이 필요
PostgreSQL을 쓰면 “관계형 모델 + 필요한 곳만 JSONB”로 NoSQL의 유연함을 상당 부분 가져올 수 있습니다. 위 예제처럼 속성이 제품마다 다른 데이터는 JSONB 컬럼에 넣고, 자주 검색하는 키에는 GIN 인덱스나 표현식 인덱스를 거는 방식입니다. 다만 attributes->>'ram'은 문자열로 꺼내므로 숫자 비교를 하려면 예제처럼 ::int 캐스팅이 필요하고, 값이 숫자가 아닌 문서가 하나라도 섞여 있으면 쿼리 전체가 invalid input syntax for type integer 에러로 실패합니다. 스키마가 없는 컬럼에서는 이런 검증 책임이 쿼리 쪽으로 옮겨 온다는 점을 기억해야 합니다.
NoSQL 데이터베이스
NoSQL 종류
graph TB
A[NoSQL] --> B[Document MongoDB]
A --> C[Key-Value Redis]
A --> D[Column Cassandra]
A --> E[Graph Neo4j]
B --> B1[JSON 문서]
C --> C1[키-값 쌍]
D --> D1[컬럼 패밀리]
E --> E1[노드-엣지]
MongoDB (Document DB)
특징:
- JSON 형식 문서 저장
- 유연한 스키마
- 수평 확장 (Sharding) 예제:
// 문서 삽입
db.users.insertOne({
name: "Alice",
email: "[email protected]",
age: 30,
interests: ["coding", "music"],
address: {
city: "Seoul",
country: "Korea"
}
});
// 조회
db.users.find({ age: { $gte: 25 } });
// 배열 쿼리
db.users.find({ interests: "coding" });
// 중첩 문서 쿼리
db.users.find({ "address.city": "Seoul" });
// 집계 (Aggregation)
db.users.aggregate([
{ $match: { age: { $gte: 25 } } },
{ $group: { _id: "$address.city", count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]);
장점:
- 유연한 스키마
- 샤딩이 기본 기능으로 내장
- 한 화면에 필요한 데이터를 한 문서로 묶으면 조회 한 번으로 끝남
단점:
$lookup으로 조인할 수 있지만 관계형 DB만큼 효율적이지 않아, 조인이 많은 모델에는 맞지 않음- 다중 문서 트랜잭션은 4.0(레플리카 셋)·4.2(샤드 클러스터)부터 지원되지만, 성능 비용이 있어 모델링으로 단일 문서 원자성을 활용하는 것이 권장됨
- 워킹 셋(자주 쓰는 데이터와 인덱스)이 메모리에 들어가지 않으면 성능이 급격히 떨어짐
도큐먼트 모델의 핵심은 “함께 읽히는 데이터는 함께 저장한다”입니다. 위의 사용자 문서처럼 주소와 관심사를 하나로 묶으면 조인 없이 한 번에 읽힙니다. 문제는 이 설계가 특정 조회 패턴에 맞춘 것이라는 점입니다. 나중에 “서울에 사는 사용자가 쓴 댓글 중 좋아요가 많은 것” 같은, 문서 경계를 가로지르는 새 요구가 생기면 데이터를 중복 저장하거나 집계 파이프라인을 복잡하게 짜야 합니다. MongoDB 도입 후 가장 흔히 겪는 어려움이 이것인데, 스키마가 유연하다는 이유로 설계를 미뤘다가 조회 패턴이 늘어나며 곤란해지는 경우입니다.
Redis (Key-Value Store)
특징:
- 인메모리 저장소
- 초고속 읽기/쓰기
- 캐시, 세션, 큐로 사용 예제:
# 문자열
SET user:1:name "Alice"
GET user:1:name # "Alice"
# 해시
HSET user:1 name "Alice" email "[email protected]"
HGET user:1 name # "Alice"
HGETALL user:1 # name: Alice, email: [email protected]
# 리스트 (큐)
LPUSH queue:tasks "task1"
LPUSH queue:tasks "task2"
RPOP queue:tasks # "task1"
# 셋
SADD tags:1 "python" "coding" "tutorial"
SMEMBERS tags:1 # ["python", "coding", "tutorial"]
# 정렬된 셋 (리더보드)
ZADD leaderboard 100 "Alice"
ZADD leaderboard 200 "Bob"
ZREVRANGE leaderboard 0 9 # 상위 10명
장점:
-
초고속 (마이크로초 단위)
-
다양한 자료구조
-
Pub/Sub 지원 단점:
-
데이터 크기가 메모리에 묶임
-
키 기반 접근만 효율적이고, 값 내부를 조건으로 검색하는 쿼리는 불가
-
영속성 설정(RDB 스냅샷, AOF)에 따라 장애 시 최근 쓰기를 잃을 수 있어 원본 데이터 저장소로 쓰기엔 신중해야 함
Redis가 빠른 이유는 데이터가 메모리에 있고 명령을 단일 스레드 이벤트 루프로 처리해 락 경합이 없기 때문입니다. 그 대가로 KEYS *나 큰 컬렉션의 HGETALL·SMEMBERS처럼 오래 걸리는 명령 하나가 실행되는 동안 다른 모든 요청이 멈춥니다. 운영 중 Redis 지연이 갑자기 튀면 가장 먼저 SLOWLOG GET으로 이런 명령이 있었는지 확인하는 것이 좋고, 키 목록이 필요하면 SCAN을 씁니다. 위 예제의 LPUSH + RPOP 큐도 간단하지만, 꺼낸 작업을 처리하다 워커가 죽으면 작업이 사라집니다. 유실이 안 되는 큐가 필요하면 LMOVE로 처리 중 목록에 옮겨 두거나 Redis Streams의 컨슈머 그룹을 씁니다.
비교 분석
SQL vs NoSQL 비교표
| 특징 | SQL | NoSQL |
|---|---|---|
| 스키마 | 고정 (Rigid) | 유연 (Flexible) |
| 확장 | 수직 (Scale-up) | 수평 (Scale-out) |
| 트랜잭션 | ACID 완벽 | BASE (제한적) |
| 조인 | 우수 | 제한적 |
| 쿼리 | 복잡한 쿼리 가능 | 단순 쿼리 |
| 일관성 | 강한 일관성 | 최종 일관성 |
이 표는 전형적인 경향일 뿐 절대적인 구분은 아닙니다. MongoDB는 기본적으로 프라이머리에서 읽고 쓰기 때문에 단일 노드 기준으로는 강한 일관성을 제공하고, 세컨더리 읽기를 켜거나 write concern을 낮출 때 최종 일관성이 됩니다. 반대로 MySQL·PostgreSQL도 비동기 복제 레플리카에서 읽으면 방금 쓴 데이터가 안 보이는 복제 지연을 겪습니다. “SQL이라서 일관적”이 아니라 “어디서 읽고 어떤 확인을 기다리느냐”가 일관성을 결정합니다.
성능 비교
“어느 DB가 몇 ms 더 빠르다”는 수치는 데이터 크기, 인덱스, 메모리 크기, 네트워크, 드라이버 설정에 따라 크게 달라서 일반화하기 어렵습니다. 대신 구조에서 나오는 경향은 분명합니다.
- 키로 한 건 조회: 인메모리인 Redis가 가장 빠르고, 나머지 DB도 인덱스와 워킹 셋이 메모리에 있으면 대부분 네트워크 왕복 시간이 지배적입니다. 이 영역에서 MySQL·PostgreSQL·MongoDB의 차이는 보통 설계 문제(인덱스 유무)에 비해 작습니다.
- 여러 테이블 조인·집계: 관계형 DB의 옵티마이저가 조인 순서와 알고리즘(해시·머지·중첩 루프)을 골라 주므로 유리합니다. MongoDB에서 같은 결과를
$lookup으로 만들면 대개 느리고, 그래서 애초에 조인이 필요 없도록 문서를 설계합니다. - 대량 쓰기: 인덱스가 많을수록, 동기 복제·디스크 flush 설정이 엄격할수록 느려집니다. DB 종류보다 이 설정이 결과를 더 크게 좌우하는 경우가 많습니다.
결국 성능을 비교하려면 실제 데이터 분포와 쿼리로 직접 측정해야 합니다. 벤치마크 글의 숫자를 근거로 DB를 고르면, 우리 서비스의 병목과 전혀 다른 지점을 최적화하게 됩니다.
확장성 비교
graph LR
A[수직 확장 Vertical] --> B[더 큰 서버]
A --> C[CPU/RAM 증설]
D[수평 확장 Horizontal] --> E[서버 추가]
D --> F[샤딩 Sharding]
G[SQL] -.어려움.-> D
G -.쉬움.-> A
H[NoSQL] -.쉬움.-> D
H -.쉬움.-> A
수평 확장이 SQL에서 어려운 근본 이유는 조인과 트랜잭션입니다. 데이터를 여러 서버에 나누면 서로 다른 서버에 있는 행끼리 조인하거나 한 트랜잭션으로 묶기가 비싸집니다. NoSQL이 수평 확장에 강한 것은 마법이 아니라, 처음부터 “한 번의 요청은 한 파티션 안에서 끝난다”는 제약을 받아들였기 때문입니다. 그래서 MongoDB에서도 샤드 키를 잘못 고르면(예: 계속 증가하는 타임스탬프) 모든 쓰기가 한 샤드로 몰려 확장 효과가 없고, 샤드 키가 쿼리 조건에 없으면 모든 샤드에 질의를 뿌리는 scatter-gather가 발생합니다. 대부분의 서비스는 단일 PostgreSQL·MySQL 서버와 읽기 레플리카로 상당한 규모까지 버틸 수 있으므로, 수평 확장은 실제로 필요해졌을 때 고민해도 늦지 않습니다.
선택 가이드
선택 플로우차트
flowchart TD
A[데이터베이스 선택] --> B{정형 데이터?}
B -->|예| C{복잡한 쿼리?}
B -->|아니오| D[NoSQL]
C -->|예| E[PostgreSQL]
C -->|아니오| F[MySQL]
D --> G{주 용도는?}
G -->|문서 저장| H[MongoDB]
G -->|캐시| I[Redis]
G -->|시계열| J[InfluxDB]
G -->|그래프| K[Neo4j]
시나리오별 권장
1. 전자상거래 (E-commerce)
- PostgreSQL: 트랜잭션, 재고 관리
- Redis: 장바구니, 세션
- 주문·결제·재고는 여러 테이블을 한 트랜잭션으로 바꿔야 하므로 관계형 DB가 자연스럽습니다. “재고 차감과 주문 생성 중 하나만 성공”하는 상황이 가장 비싼 버그이기 때문입니다.
2. 소셜 미디어
- MongoDB: 게시글, 댓글 (유연한 스키마)
- Redis: 피드 캐시, 실시간 알림
- 팔로우 관계처럼 다대다 관계가 핵심인 데이터는 관계형 DB나 그래프 DB가 더 다루기 쉽고, 도큐먼트 DB는 게시글처럼 한 덩어리로 읽히는 콘텐츠에 맞습니다.
3. 분석 플랫폼
- PostgreSQL: 집계 쿼리
- InfluxDB: 시계열 데이터
- 예: 대시보드, 리포트
- 데이터가 커지면 행 단위 저장인 OLTP DB로는 집계가 느려지므로, ClickHouse·BigQuery 같은 컬럼 지향 분석 DB로 분리하는 것이 일반적입니다.
4. 실시간 채팅
- MongoDB: 메시지 저장
- Redis: 온라인 사용자, Pub/Sub
- Redis Pub/Sub은 구독자가 연결돼 있지 않으면 메시지를 버리므로, 메시지 원본은 반드시 영속 저장소에 쓰고 Pub/Sub은 알림 전달에만 씁니다.
5. 콘텐츠 관리 시스템 (CMS)
- PostgreSQL 또는 MySQL: 구조화된 콘텐츠
- MongoDB: 유연한 콘텐츠 타입
- WordPress는 MySQL(MariaDB)을, Strapi는 PostgreSQL·MySQL·SQLite를 지원합니다. 콘텐츠 타입이 자주 바뀌더라도 관계형 DB + JSON 컬럼으로 충분한 경우가 많습니다.
정리
핵심 요약
SQL (MySQL, PostgreSQL):
- 정형화된 스키마
- 복잡한 쿼리, 조인
- ACID 트랜잭션
- 수직 확장 NoSQL (MongoDB, Redis):
- 유연한 스키마
- 단순 쿼리
- 수평 확장
- 최종 일관성
데이터베이스 선택 기준
| 우선순위 | 선택 |
|---|---|
| 트랜잭션 | PostgreSQL |
| 복잡한 쿼리 | PostgreSQL |
| 빠른 개발 | MongoDB |
| 수평 확장 | MongoDB |
| 캐시 | Redis |
| 시계열 | InfluxDB |
하이브리드 접근
규모가 커지면 역할별로 여러 데이터베이스를 조합하게 됩니다. 이것을 폴리글랏 퍼시스턴스라고 부릅니다.
PostgreSQL (주 데이터베이스)
├─ 사용자, 주문, 결제
Redis (캐시)
├─ 세션, 장바구니
MongoDB (로그)
├─ 애플리케이션 로그, 이벤트
하이브리드 구성의 비용은 “데이터가 두 곳에 있다”는 사실에서 나옵니다. Redis 캐시가 PostgreSQL과 어긋나면 사용자는 이미 바뀐 가격이나 삭제된 게시글을 보게 됩니다. 캐시를 쓸 때 가장 흔한 실수는 DB를 먼저 업데이트하고 캐시를 갱신하는 과정에서 동시 요청이 끼어들어 옛 값이 다시 캐시에 들어가는 것인데, 그래서 보통 “DB 업데이트 후 캐시 삭제 + 짧은 TTL”로 불일치 시간을 제한합니다. 저장소가 하나 늘 때마다 백업, 모니터링, 장애 대응 절차도 하나씩 늘어나므로, 팀 규모가 작다면 PostgreSQL 하나로 시작해 병목이 확인된 부분만 떼어 내는 편이 운영 부담이 적습니다.
다음 단계
각 데이터베이스의 자세한 사용법은 아래 글을 참고하세요:
- PostgreSQL C++ 연동
- MongoDB C++ 드라이버
- C++에서 Redis 쓰기: hiredis·redis-plus-plus, 파이프라인, 분산 락, Lua 스크립트
- SQL 쿼리 최적화
자주 묻는 질문 (FAQ)
Q. NoSQL은 스키마가 유연하다는데, 그럼 스키마 설계를 하지 않아도 되나요?
A. 그렇지 않습니다. 데이터베이스가 스키마를 강제하지 않을 뿐, 어떤 필드가 있어야 하는지에 대한 규칙은 애플리케이션 코드로 옮겨 가므로 검증을 따로 두지 않으면 필드 이름이나 타입이 제각각인 문서가 쌓입니다. 특히 MongoDB처럼 조회 패턴에 맞춰 데이터를 묶는 모델은 어떤 쿼리를 자주 할지 먼저 정해야 하며, 나중에 구조를 바꾸면 기존 문서 마이그레이션이 필요합니다.
같이 보면 좋은 글
- PostgreSQL vs MySQL 차이와 선택 가이드 | 스키마·트랜잭션·운영
- Node.js 데이터베이스 연동 | MongoDB, PostgreSQL, MySQL
- MongoDB 입문: SQL과의 차이, CRUD, Mongoose, 집계 파이프라인, 인덱싱과 성능