🗄️ 데이터베이스

NoSQL

Not Only SQL

비관계형 데이터베이스. MongoDB, Redis, Cassandra 등. 스키마리스, 수평 확장성이 특징.

📖 상세 설명

NoSQL이란?

NoSQL은 "Not Only SQL"의 약자로, 전통적인 관계형 데이터베이스(RDBMS)의 한계를 극복하기 위해 등장한 비관계형 데이터베이스의 총칭입니다. 고정된 스키마 없이 유연한 데이터 모델을 제공하며, 수평 확장(Scale-out)에 최적화되어 있습니다.

NoSQL의 4가지 유형

  • Document Store: JSON/BSON 문서 단위 저장 (MongoDB, Couchbase, Firebase)
  • Key-Value Store: 단순 키-값 쌍 저장, 초고속 조회 (Redis, DynamoDB, Riak)
  • Wide-Column Store: 행마다 다른 컬럼 가능, 대용량 분석 (Cassandra, HBase, ScyllaDB)
  • Graph Database: 노드와 관계 기반, 복잡한 연결 데이터 (Neo4j, ArangoDB, Dgraph)

SQL vs NoSQL 비교

특성 SQL (RDBMS) NoSQL
스키마 고정 스키마 (테이블, 컬럼) 유연/스키마리스
확장성 수직 확장 (Scale-up) 수평 확장 (Scale-out)
트랜잭션 ACID 보장 BASE (Eventual Consistency)
쿼리 표준 SQL DB별 고유 API/쿼리
JOIN 복잡한 JOIN 지원 제한적 또는 비권장
적합한 경우 정형 데이터, 복잡한 관계 비정형, 대용량, 빠른 확장

CAP 정리와 NoSQL

분산 시스템에서는 Consistency(일관성), Availability(가용성), Partition Tolerance(분단 내성) 중 두 가지만 보장 가능합니다:

  • CP (일관성 + 분단 내성): MongoDB, HBase - 네트워크 분단 시 일부 요청 거부
  • AP (가용성 + 분단 내성): Cassandra, DynamoDB - 일시적 불일치 허용
  • CA: 단일 노드 RDBMS - 분단 시 시스템 전체 중단

주요 사용 사례

  • 실시간 빅데이터: 로그 수집, IoT 센서 데이터, 클릭스트림
  • 콘텐츠 관리: CMS, 상품 카탈로그, 사용자 프로필
  • 캐싱 레이어: 세션 스토어, API 응답 캐시
  • 소셜 네트워크: 친구 관계, 추천 시스템 (Graph DB)
  • 시계열 데이터: 모니터링, 금융 데이터

💻 코드 예제

MongoDB (Document Store)

Python (pymongo)
from pymongo import MongoClient

client = MongoClient('mongodb://localhost:27017')
db = client['ecommerce']
products = db['products']

# 문서 삽입 (스키마 없이 자유롭게)
products.insert_one({
    'name': 'MacBook Pro',
    'category': 'electronics',
    'price': 2499.00,
    'specs': {
        'cpu': 'M3 Pro',
        'ram': '18GB',
        'storage': '512GB SSD'
    },
    'tags': ['laptop', 'apple', 'premium']
})

# 중첩 필드 쿼리
apple_laptops = products.find({
    'category': 'electronics',
    'specs.cpu': {'$regex': '^M[0-9]'}
})

# Aggregation Pipeline
pipeline = [
    {'$match': {'category': 'electronics'}},
    {'$group': {
        '_id': '$specs.cpu',
        'avgPrice': {'$avg': '$price'},
        'count': {'$sum': 1}
    }},
    {'$sort': {'avgPrice': -1}}
]
results = products.aggregate(pipeline)

Redis (Key-Value Store)

Python
import redis
import json

r = redis.Redis(host='localhost', port=6379, decode_responses=True)

# String
r.set('user:1001:session', 'abc123', ex=3600)

# Hash (객체 저장)
r.hset('user:1001', mapping={
    'name': 'Alice',
    'email': 'alice@example.com',
    'plan': 'premium'
})

# Sorted Set (리더보드)
r.zadd('game:leaderboard', {'alice': 1500, 'bob': 1200})
top_players = r.zrevrange('game:leaderboard', 0, 9, withscores=True)

# List (큐)
r.lpush('task:queue', json.dumps({'job': 'send_email', 'to': 'user@example.com'}))
task = r.rpop('task:queue')

Cassandra (Wide-Column Store)

Python (cassandra-driver)
from cassandra.cluster import Cluster

cluster = Cluster(['127.0.0.1'])
session = cluster.connect('analytics')

# 파티션 키 설계가 중요
session.execute("""
    CREATE TABLE IF NOT EXISTS user_events (
        user_id UUID,
        event_date DATE,
        event_time TIMESTAMP,
        event_type TEXT,
        event_data TEXT,
        PRIMARY KEY ((user_id, event_date), event_time)
    ) WITH CLUSTERING ORDER BY (event_time DESC)
""")

# 쓰기 (파티션 단위로 분산)
session.execute("""
    INSERT INTO user_events (user_id, event_date, event_time, event_type, event_data)
    VALUES (%s, %s, %s, %s, %s)
""", (user_id, today, now, 'page_view', json.dumps(data)))

# 읽기 (파티션 키 필수)
rows = session.execute("""
    SELECT * FROM user_events
    WHERE user_id = %s AND event_date = %s
    ORDER BY event_time DESC
    LIMIT 100
""", (user_id, today))

Neo4j (Graph Database)

Cypher
// 노드 생성
CREATE (alice:User {name: 'Alice', age: 28})
CREATE (bob:User {name: 'Bob', age: 32})
CREATE (post:Post {title: 'NoSQL Guide', views: 1500})

// 관계 생성
MATCH (a:User {name: 'Alice'}), (b:User {name: 'Bob'})
CREATE (a)-[:FOLLOWS]->(b)

MATCH (a:User {name: 'Alice'}), (p:Post {title: 'NoSQL Guide'})
CREATE (a)-[:WROTE]->(p)

// 2단계 친구 찾기 (SQL로는 복잡한 쿼리)
MATCH (user:User {name: 'Alice'})-[:FOLLOWS*2]->(fof:User)
WHERE NOT (user)-[:FOLLOWS]->(fof) AND user <> fof
RETURN DISTINCT fof.name AS recommendedFriend

// 최단 경로 탐색
MATCH path = shortestPath(
    (a:User {name: 'Alice'})-[:FOLLOWS*]-(b:User {name: 'Charlie'})
)
RETURN path

DynamoDB (Key-Value / Document)

Python (boto3)
import boto3
from boto3.dynamodb.conditions import Key

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Orders')

# 아이템 저장
table.put_item(Item={
    'PK': 'USER#1001',          # Partition Key
    'SK': 'ORDER#2024-01-15',   # Sort Key
    'total': 299.99,
    'items': [
        {'product': 'Laptop Stand', 'qty': 1},
        {'product': 'USB Cable', 'qty': 2}
    ],
    'status': 'shipped'
})

# 쿼리 (파티션 키 + 정렬 키 범위)
response = table.query(
    KeyConditionExpression=
        Key('PK').eq('USER#1001') &
        Key('SK').begins_with('ORDER#2024')
)

# Single Table Design - GSI로 다양한 접근 패턴
# GSI1: status로 조회, GSI2: 날짜로 조회

💬 현업 대화 예시

주니어 개발자
"새 프로젝트에 데이터베이스 뭘 쓸까요? NoSQL이 요즘 대세라던데..."
시니어 개발자
"대세보다 요구사항이 중요해. 데이터 구조가 자주 바뀌고, 스키마가 유연해야 하면 MongoDB 같은 Document DB. 단순 조회 성능이 중요하면 Redis. 복잡한 관계와 트랜잭션이 필요하면 여전히 PostgreSQL이 나을 수 있어."
백엔드 개발자
"MongoDB 쓰는데 JOIN이 필요해요. $lookup 쓰면 되나요?"
DBA
"$lookup은 있지만 RDBMS만큼 효율적이지 않아. Document DB는 비정규화해서 한 문서에 필요한 데이터를 다 넣는 게 기본 패턴이야. 읽기 위주면 데이터 중복 감수하고 임베딩하는 게 나아."
CTO
"유저가 급증해서 DB 확장해야 하는데, MySQL 샤딩은 너무 복잡하다고?"
아키텍트
"네, 샤딩은 애플리케이션 로직 변경이 많아요. Cassandra나 DynamoDB 같은 NoSQL은 처음부터 분산 설계라 노드 추가만으로 확장돼요. 다만 쿼리 패턴을 파티션 키 중심으로 다시 설계해야 합니다."

⚠️ 주의사항

⚠️ NoSQL은 만능이 아님
NoSQL이 RDBMS를 대체하는 것이 아닙니다. 복잡한 트랜잭션, 강력한 일관성, 복잡한 JOIN이 필요한 경우 RDBMS가 더 적합합니다. "유행이라서" 선택하지 말고 요구사항에 맞게 선택하세요.
⚠️ 데이터 모델링이 핵심
NoSQL은 "쿼리 패턴 중심" 설계가 필수입니다. RDBMS처럼 정규화 후 아무 쿼리나 날리면 성능이 처참합니다. 어떤 쿼리를 할지 먼저 정하고, 그에 맞게 데이터 구조와 인덱스를 설계하세요.
⚠️ Eventual Consistency 이해
대부분의 NoSQL은 강력한 일관성 대신 최종 일관성(Eventual Consistency)을 제공합니다. 쓰기 직후 읽기 시 최신 데이터가 아닐 수 있습니다. 금융, 재고 등 강력한 일관성이 필요한 곳에서는 주의하세요.
⚠️ 벤더 종속 주의
NoSQL은 표준 쿼리 언어가 없습니다. MongoDB에서 DynamoDB로, Cassandra에서 ScyllaDB로 이전 시 코드 변경이 필요합니다. 추상화 레이어를 두거나, 이전 가능성을 미리 고려하세요.

🔗 관련 용어

📚 더 배우기