새 프로젝트를 시작할 때 가장 먼저 마주치는 선택 중 하나가 데이터베이스입니다. SQL(관계형)과 NoSQL(비관계형)은 데이터를 저장하고 조회하는 방식이 근본적으로 다릅니다. "어떤 게 더 좋아?"가 아니라 "이 상황에 무엇이 적합한가"로 접근해야 합니다.
관계형 DB(SQL) — PostgreSQL 예시
데이터를 테이블(행과 열)로 저장하고, 테이블 간 관계를 외래 키로 정의합니다.
-- 테이블 생성
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(500) NOT NULL,
content TEXT,
published BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- JOIN으로 연관 데이터 조회
SELECT
u.name AS author,
p.title,
p.created_at
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.published = TRUE
ORDER BY p.created_at DESC
LIMIT 10;
트랜잭션 — ACID 보장
-- 계좌 이체: 출금과 입금이 동시에 성공/실패해야 함
BEGIN;
UPDATE accounts SET balance = balance - 10000
WHERE id = 1 AND balance >= 10000;
UPDATE accounts SET balance = balance + 10000
WHERE id = 2;
-- 두 업데이트 모두 성공해야 커밋
COMMIT;
-- 하나라도 실패하면 ROLLBACK;
비관계형 DB(NoSQL) — MongoDB 예시
데이터를 문서(Document) 형태로 저장합니다. 스키마가 유연하며 중첩 구조를 그대로 저장할 수 있습니다.
// MongoDB — 문서(Document) 예시
{
_id: ObjectId("507f1f77bcf86cd799439011"),
email: "dev@devlog.kr",
name: "홍길동",
// 관련 데이터를 중첩해서 저장 (JOIN 불필요)
posts: [
{
title: "Node.js 입문",
tags: ["javascript", "backend"],
published: true,
views: 1234
}
],
preferences: {
theme: "dark",
notifications: { email: true, push: false }
}
}
// 조회
db.users.find({
"posts.published": true,
"preferences.theme": "dark"
}).sort({ createdAt: -1 }).limit(10);
// 집계 파이프라인
db.posts.aggregate([
{ $match: { published: true } },
{ $group: { _id: "$tags", count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 5 }
]);
Redis — 인메모리 캐시 DB
const redis = require('redis');
const client = redis.createClient();
// 세션 저장 (1시간 만료)
await client.setEx(`session:${userId}`, 3600, JSON.stringify(sessionData));
// 캐시 패턴
async function getUser(id) {
const cached = await client.get(`user:${id}`);
if (cached) return JSON.parse(cached);
const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
await client.setEx(`user:${id}`, 300, JSON.stringify(user)); // 5분 캐시
return user;
}
// 카운터 (조회수, 좋아요 수)
await client.incr(`post:${postId}:views`);
// 순위 (리더보드)
await client.zAdd('leaderboard', { score: 1500, value: 'user:123' });
const top10 = await client.zRangeWithScores('leaderboard', 0, 9, { REV: true });
언제 무엇을 선택할까?
SQL (PostgreSQL, MySQL)을 선택할 때:
✅ 데이터 간 복잡한 관계가 많을 때 (ERP, 금융)
✅ 트랜잭션 일관성이 중요할 때 (주문, 결제)
✅ 데이터 구조가 명확하고 잘 변하지 않을 때
✅ 복잡한 쿼리, 집계, 리포팅이 필요할 때
MongoDB를 선택할 때:
✅ 데이터 구조가 유연하거나 자주 바뀔 때
✅ 문서 형태 데이터 (블로그 글, 상품 카탈로그)
✅ 빠른 프로토타이핑이 필요할 때
✅ 스케일 아웃(수평 확장)이 중요할 때
Redis를 선택할 때:
✅ 세션, 캐시, 임시 데이터 저장
✅ 실시간 순위표, 카운터
✅ 메시지 큐, Pub/Sub
✅ 응답 속도가 ms 단위여야 할 때
인덱스 — 성능의 핵심
-- 자주 검색하는 컬럼에 인덱스 추가
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_created ON posts(created_at DESC);
-- 복합 인덱스 (조건에 자주 함께 등장하는 컬럼)
CREATE INDEX idx_posts_user_published
ON posts(user_id, published)
WHERE published = TRUE; -- 부분 인덱스
-- 인덱스 활용 여부 확인
EXPLAIN ANALYZE
SELECT * FROM posts WHERE user_id = 1 AND published = TRUE;
핵심 정리
SQL은 데이터 일관성과 복잡한 관계, NoSQL은 유연한 스키마와 수평 확장에 강합니다. 많은 실제 서비스는 둘을 함께 사용합니다: PostgreSQL로 핵심 비즈니스 데이터를 관리하고, Redis로 캐시/세션을 처리합니다. 인덱스 설계는 쿼리 성능에 결정적인 영향을 미치므로 반드시 EXPLAIN으로 검증하세요.
SQL은 데이터 일관성과 복잡한 관계, NoSQL은 유연한 스키마와 수평 확장에 강합니다. 많은 실제 서비스는 둘을 함께 사용합니다: PostgreSQL로 핵심 비즈니스 데이터를 관리하고, Redis로 캐시/세션을 처리합니다. 인덱스 설계는 쿼리 성능에 결정적인 영향을 미치므로 반드시 EXPLAIN으로 검증하세요.