0Pricing
Coding Interview Prep · 강의

확장 가능한 데이터 저장소: SQL과 NoSQL

접근 패턴, 일관성, 확장 요구 사항을 바탕으로 관계형 데이터베이스, 키-값 저장소, 문서 데이터베이스, 와이드 컬럼 저장소 중에서 선택합니다.

확장 가능한 데이터 저장소: SQL과 NoSQL은(는) CoddyKit의 무료 Coding Interview Prep 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Coding Interview Prep 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Coding Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

저장소 선택의 상충 관계

데이터 저장소를 선택하는 일은 시스템 설계에서 가장 중요한 결정 중 하나입니다. 모든 상황에 최적인 데이터베이스는 없습니다. 각 유형은 서로 다른 접근 패턴, 일관성 보장 수준, 확장 특성에 맞게 최적화되어 있습니다. 운영 환경에서 이를 잘못 선택하면 몇 달에 걸친 고통스러운 데이터 이전 작업이 발생합니다.

면접에서는 지원자가 근본적인 차이점을 이해하고 저장 엔진을 문제의 요구 사항에 맞출 수 있는지 확인합니다. 질문은 결코 '어느 쪽이 더 나은가?'가 아니라 '이 특정 작업 부하에 어느 쪽이 더 나은가?'입니다. 항상 구체적인 요구 사항을 근거로 선택을 정당화하셔야 합니다.

# Storage decision matrix summary
factors = [
    'Data structure (tabular, documents, key-value, graph, time-series)',
    'Read vs write ratio (read-heavy, write-heavy, balanced)',
    'Query patterns (point lookups, range scans, aggregations, joins)',
    'Consistency requirements (ACID vs eventual consistency)',
    'Scale requirements (single node, sharding, global distribution)',
    'Latency requirements (milliseconds vs microseconds)',
    'Team familiarity and operational complexity',
]
print('Key factors for storage selection:')
for f in factors:
    print(f'  - {f}')

관계형 데이터베이스(SQL)의 장점

관계형 데이터베이스(PostgreSQL, MySQL, 에스큐라이트)는 고정된 스키마를 사용하는 테이블에 데이터를 저장하고 ACID 트랜잭션인 원자성, 일관성, 격리성, 지속성을 지원합니다. 관계형 데이터베이스는 조인, 집계, 필터가 포함된 복잡한 질의에 뛰어나므로 관계가 명확하게 정의된 구조화된 데이터에 적합합니다.

주요 장점은 SQL을 통한 복잡한 다중 테이블 질의, 데이터 무결성을 위한 외래 키 제약 조건, 강력한 인덱싱(B-트리, 해시, 전문 검색), 복제와 백업을 지원하는 성숙한 생태계입니다. 데이터 간 관계가 긴밀하고 일관성이 중요하며 질의가 복잡하고 다양할 때 SQL을 사용하시면 됩니다.

# SQL excels at: complex queries, joins, transactions

# Example: find top 5 products by revenue this month
sql_query = '''
SELECT p.name, SUM(oi.quantity * oi.price) AS revenue
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.created_at >= DATE_TRUNC('month', NOW())
GROUP BY p.id, p.name
ORDER BY revenue DESC
LIMIT 5;
'''
print('SQL shines for relational queries with JOINs:')
print(sql_query)

print('ACID guarantees example (transfer $100 between accounts):')
transfer_sql = '''
BEGIN;
  UPDATE accounts SET balance = balance - 100 WHERE id = 1;
  UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;  -- either both succeed or neither does
'''  
print(transfer_sql)

SQL 확장의 과제

SQL 데이터베이스는 자연스럽게 수직 확장됩니다(더 큰 서버와 더 많은 CPU/RAM 사용). 하지만 수평 확장은 복잡합니다. 읽기 복제본은 읽기 중심 작업 부하를 처리하기 위해 읽기 요청을 복제본 노드로, 쓰기 요청을 주 노드로 전달합니다. 그러나 샤딩을 추가하지 않는 한 쓰기 처리량은 단일 주 노드의 한도에 묶입니다.

샤딩은 샤드 키(예: 사용자 식별자를 N으로 나눈 나머지)를 기준으로 여러 데이터베이스 인스턴스에 데이터를 분할합니다. 이를 통해 쓰기를 확장할 수 있지만 SQL의 가장 강력한 기능 중 두 가지인 샤드 간 조인과 트랜잭션을 사용할 수 없게 됩니다. 대부분의 웹 애플리케이션은 데이터가 약 5~10 TB에 이르거나 초당 쓰기가 약 10만 건에 이르면 단일 노드 SQL의 한계를 넘어섭니다.

# SQL scaling strategies
strategies = {
    'Read replicas': {
        'how': 'One primary (writes), multiple replicas (reads)',
        'scales': 'Read throughput (10x+)',
        'limit': 'Write throughput still bounded by single primary',
    },
    'Connection pooling (PgBouncer)': {
        'how': 'Pool of persistent DB connections shared among app servers',
        'scales': 'Connection count (PostgreSQL max ~500 connections)',
        'limit': 'Does not increase query throughput',
    },
    'Horizontal sharding': {
        'how': 'Partition rows by shard key across N database instances',
        'scales': 'Both reads and writes (N×)',
        'limit': 'Cross-shard joins and transactions broken; complex routing',
    },
    'CQRS': {
        'how': 'Separate write model (SQL) from read model (denormalised/NoSQL)',
        'scales': 'Optimise each path independently',
        'limit': 'Eventual consistency between write and read models',
    },
}
for strategy, info in strategies.items():
    print(f'{strategy}:\n  How: {info["how"]}\n  Scales: {info["scales"]}\n  Limit: {info["limit"]}\n')

키-값 저장소: 레디스와 DynamoDB

키-값 저장소는 키 → 값 쌍으로 데이터를 저장하며 키를 사용한 읽기와 쓰기를 O(1)에 수행합니다. 키-값 저장소는 매우 높은 성능과 수평 확장성을 얻는 대신 질의 유연성을 포기합니다. 레디스(메모리 기반)는 마이크로초 단위의 지연 시간을 달성하고, DynamoDB(관리형)는 자동으로 확장되면서 어떤 처리량에서도 한 자릿수 밀리초의 지연 시간을 달성합니다.

다음과 같은 경우 키-값 저장소를 사용하시면 됩니다: 세션 저장, 캐싱, 기능 플래그, 웹 주소 단축기 매핑, 장바구니, 실시간 순위표. 다음과 같은 경우에는 사용하지 마십시오: 복잡한 질의, 엔터티 간 관계, 임의 필터링이 필요한 경우입니다. 정확한 키로만 조회할 수 있기 때문입니다.

# Key-value store use cases
kv_operations = {
    'GET key':        'O(1) point lookup — the core operation',
    'SET key value':  'O(1) insert or update',
    'DEL key':        'O(1) delete',
    'EXPIRE key ttl': 'Set time-to-live; key auto-deleted after ttl seconds',
    'INCR key':       'Atomic increment — useful for counters and rate limiting',
    'LPUSH/LRANGE':   'List operations — useful for queues and recent-items feeds',
    'ZADD/ZRANGE':    'Sorted set — leaderboards, rate limiting with sliding window',
}
print('Redis operation set:')
for op, desc in kv_operations.items():
    print(f'  {op:25s}: {desc}')

print('\nDynamoDB vs Redis:')
print('  Redis:    microsecond latency, in-memory, needs persistence config')
print('  DynamoDB: single-digit ms, managed, auto-scaling, durable by default')

문서 저장소: 몽고DB

문서 저장소(MongoDB, 카우치베이스)는 자바스크립트 객체 표기법과 유사한 문서 형태로 데이터를 저장하므로 유연한 스키마를 사용할 수 있습니다. 같은 컬렉션에 있는 문서라도 필드가 서로 다를 수 있습니다. 모든 필드에 인덱스를 만들 수 있고 필터, 프로젝션, 집계와 같이 상당히 풍부한 질의를 지원하지만, 여러 문서에 걸친 트랜잭션은 SQL보다 제한적입니다.

문서 저장소는 엔터티마다 데이터 형태가 다르거나(서로 다른 속성을 가진 사용자 프로필), 스키마가 빠르게 변화하거나(데이터 모델을 자주 변경하는 신생 기업), 테이블을 가로질러 조인하기보다 전체 엔터티를 가져오는 읽기 패턴이 주를 이루는 애플리케이션에 적합합니다.

# MongoDB document example
user_doc = {
    '_id': 'user123',
    'name': 'Alice',
    'email': 'alice@example.com',
    'preferences': {
        'theme': 'dark',
        'language': 'en',
        'notifications': ['email', 'push']
    },
    'addresses': [
        {'type': 'home', 'city': 'Berlin', 'country': 'DE'},
        {'type': 'work', 'city': 'Munich', 'country': 'DE'}
    ],
    'subscription_tier': 'pro',
    # Note: not all users have all fields -- flexible schema!
}

import json
print('Document structure (flexible schema):')
print(json.dumps(user_doc, indent=2))

print('\nDocument store strengths:')
print('  - Nested/array fields without joins')
print('  - Flexible schema (different fields per document)')
print('  - Scales horizontally by sharding on _id')

광열 저장소: 카산드라

광열 저장소(아파치 카산드라, 에이치베이스)는 행과 열에 데이터를 저장하면서도 각 행에 서로 다른 열 집합을 허용합니다. 광열 저장소는 여러 노드에 분산된 대규모 쓰기 처리량을 위해 설계되며, 기본적으로 최종 일관성을 사용합니다. 카산드라는 선형 쓰기 확장을 달성하므로 노드를 두 배로 늘리면 쓰기 처리량도 두 배가 됩니다.

상충 관계는 질의를 파티션 키를 중심으로 설계해야 한다는 점입니다. 임의의 열을 효율적으로 필터링하거나 정렬할 수 없으므로 먼저 질의 패턴을 정의한 다음 그에 맞게 테이블을 설계해야 합니다. 이는 '데이터를 설계한 다음 어떤 질의든 작성하는' SQL의 접근 방식과 정반대입니다.

# Cassandra table design for time-series events
# Design around the query: 'give me all events for user X, most recent first'

cassandra_table = '''
CREATE TABLE user_events (
    user_id    UUID,
    event_time TIMESTAMP,
    event_type TEXT,
    metadata   MAP<TEXT, TEXT>,
    PRIMARY KEY (user_id, event_time)
) WITH CLUSTERING ORDER BY (event_time DESC);

-- Query (matches partition key exactly):
SELECT * FROM user_events WHERE user_id = ? LIMIT 100;
'''
print('Cassandra wide-column design:')
print(cassandra_table)
print('Properties:')
print('  - user_id = partition key (all rows for one user on same node)')
print('  - event_time = clustering key (sorted within partition)')
print('  - Very fast writes: append-only, no locking')
print('  - Cannot query by event_type alone (no partition key)')

CAP 이론: 일관성, 가용성, 분할 내성

CAP 이론에 따르면 분산 시스템은 다음 세 가지 중 최대 두 가지만 보장할 수 있습니다. C일관성(모든 읽기가 최신 쓰기 결과를 반환함), A가용성(모든 요청이 오류가 아닌 응답을 받음), P분할 내성(네트워크 분할이 발생해도 시스템이 계속 작동함)입니다.

분산 시스템에서는 네트워크 분할이 항상 발생하므로 실제 선택지는 CP(일관성을 우선하며 분할 중에는 요청을 거부할 수 있음)와 AP(가용성을 우선하며 오래된 데이터를 반환할 수 있음) 사이에 있습니다. SQL 데이터베이스는 일반적으로 CP이고, 카산드라와 DynamoDB는 AP입니다(기본적으로 최종 일관성). 레디스 클러스터는 CP입니다.

# CAP theorem applied to common databases
databases = {
    'PostgreSQL (single node)': {'C': True,  'A': True,  'P': False, 'note': 'Not distributed; CA'},
    'PostgreSQL (multi-AZ)':    {'C': True,  'A': False, 'P': True,  'note': 'CP: primary fails over, brief downtime'},
    'MySQL Cluster':            {'C': True,  'A': False, 'P': True,  'note': 'CP'},
    'Cassandra':                {'C': False, 'A': True,  'P': True,  'note': 'AP: eventual consistency default'},
    'DynamoDB (default)':       {'C': False, 'A': True,  'P': True,  'note': 'AP: eventual consistency'},
    'DynamoDB (strong read)':   {'C': True,  'A': False, 'P': True,  'note': 'CP: strongly consistent reads'},
    'Redis Cluster':            {'C': True,  'A': False, 'P': True,  'note': 'CP'},
    'MongoDB (default)':        {'C': True,  'A': False, 'P': True,  'note': 'CP: reads from primary'},
}
for db, caps in databases.items():
    c_str = 'C' if caps['C'] else '-'
    a_str = 'A' if caps['A'] else '-'
    p_str = 'P' if caps['P'] else '-'
    print(f'{db:35s} [{c_str}{a_str}{p_str}] {caps["note"]}')

저장소 선택: 의사 결정 프레임워크

면접에서 저장소를 선택할 때 사용할 수 있는 실용적인 의사 결정 프레임워크입니다:

  • ACID 트랜잭션이 필요하신가요? → 관계형 데이터베이스(PostgreSQL, MySQL)
  • 밀리초 미만의 조회나 캐싱이 필요하신가요? → 키-값 저장소(레디스)
  • 유연한 스키마나 문서 중심 데이터가 필요하신가요? → 문서 저장소(MongoDB)
  • 시계열 또는 이벤트 데이터에 대규모 쓰기 처리량(>초당 10만 건)이 필요하신가요? → 광열 저장소(카산드라)
  • 관리형 확장을 통한 전 세계 분산이 필요하신가요? → DynamoDB 또는 코스모스 데이터베이스
  • 그래프 탐색이 필요하신가요? → 그래프 데이터베이스(네오포제이)
# Decision tree in code form
def choose_storage(needs_acid, high_write_throughput, flexible_schema,
                   sub_ms_latency, graph_queries, global_scale):
    if sub_ms_latency:
        return 'Redis (in-memory key-value)'
    if graph_queries:
        return 'Neo4j (graph database)'
    if needs_acid:
        return 'PostgreSQL / MySQL (relational)'
    if high_write_throughput and not flexible_schema:
        return 'Cassandra (wide-column, write-optimised)'
    if flexible_schema:
        return 'MongoDB (document store)'
    if global_scale:
        return 'DynamoDB or Cosmos DB (managed global KV/document)'
    return 'PostgreSQL (safe default for most web apps)'

# Example scenarios
scenarios = [
    {'needs_acid': True,  'high_write_throughput': False, 'flexible_schema': False,
     'sub_ms_latency': False, 'graph_queries': False, 'global_scale': False},
    {'needs_acid': False, 'high_write_throughput': True,  'flexible_schema': False,
     'sub_ms_latency': False, 'graph_queries': False, 'global_scale': False},
    {'needs_acid': False, 'high_write_throughput': False, 'flexible_schema': False,
     'sub_ms_latency': True,  'graph_queries': False, 'global_scale': False},
]
for s in scenarios:
    print(f'{choose_storage(**s)}')

다중 저장소 지속성: 여러 저장소 사용

운영 시스템에서 모든 용도에 하나의 데이터베이스만 사용하는 경우는 드뭅니다. 다중 저장소 지속성은 시스템의 각 부분에 적합한 저장 기술을 사용하는 것을 의미합니다. 일반적인 웹 애플리케이션은 사용자 및 주문의 기준 데이터를 PostgreSQL에 저장하고, 세션 저장과 캐싱에는 레디스를 사용하며, 전문 검색에는 엘라스틱서치를 사용하고, 파일 저장에는 에스쓰리를 사용하며, 이벤트 로그와 분석에는 카산드라를 사용할 수 있습니다.

상충 관계는 데이터베이스 유형이 늘어날수록 운영 복잡성이 증가한다는 점입니다. 팀은 여러 시스템을 유지 관리하고, 모니터링하고, 백업해야 합니다. 설계의 근거는 규모가 커질수록 각 작업 부하에 적합한 저장소를 선택해서 얻는 성능 및 확장성 향상이 운영 비용을 능가한다는 것입니다.

# Polyglot persistence in an e-commerce system
components = {
    'User accounts, orders, payments': {
        'storage': 'PostgreSQL',
        'reason': 'ACID transactions (payment integrity), complex queries',
    },
    'Product catalogue': {
        'storage': 'MongoDB or PostgreSQL with JSONB',
        'reason': 'Flexible product attributes vary by category',
    },
    'Session tokens': {
        'storage': 'Redis (with TTL)',
        'reason': 'O(1) lookup, automatic expiry, high throughput',
    },
    'Product search': {
        'storage': 'Elasticsearch',
        'reason': 'Full-text search, faceted filtering, relevance scoring',
    },
    'Activity/event log': {
        'storage': 'Cassandra or Kafka + S3',
        'reason': 'High write throughput, append-only, time-series queries',
    },
    'Product images / videos': {
        'storage': 'S3 + CloudFront CDN',
        'reason': 'Cheap object storage, global distribution via CDN',
    },
}
for component, info in components.items():
    print(f'{component}:\n  {info["storage"]}: {info["reason"]}\n')

저장소 유형별 인덱싱 전략

모든 저장 시스템은 쓰기 속도와 추가 저장 공간을 희생하여 읽기 속도를 높이는 인덱스를 사용합니다. 시스템 설계에서는 저장소 유형별 인덱싱을 이해하는 것이 중요합니다:

  • SQL: 모든 열에 B-트리 인덱스 사용 가능, 다중 열 질의를 위한 복합 인덱스, 테이블 조회를 피하기 위한 포함 인덱스
  • MongoDB: 모든 필드에 인덱스 사용 가능, 복합 인덱스, 자동 만료를 위한 TTL 인덱스
  • 카산드라: 파티션 키와 클러스터링 열만 기본적으로 인덱싱되며, 보조 인덱스는 비용이 많이 듭니다
  • 레디스: 범위 질의 인덱스로 정렬 집합 사용, 키-값 저장소에는 전통적인 인덱싱이 필요하지 않음
# Indexing examples across storage types

# PostgreSQL: B-tree composite index
postgres_index = '''
CREATE INDEX idx_orders_user_created
ON orders(user_id, created_at DESC);
-- Optimises: SELECT * FROM orders WHERE user_id=? ORDER BY created_at DESC
'''

# MongoDB: compound index
mongo_index = '''
db.products.createIndex({ category: 1, price: -1 })
// Optimises: db.products.find({category:'Electronics'}).sort({price:-1})
'''

# Cassandra: cluster key ordering (built into table design)
cassandra_index = '''
-- No separate index needed; clustering key IS the index:
PRIMARY KEY (user_id, event_time) WITH CLUSTERING ORDER BY (event_time DESC)
'''

print('PostgreSQL:', postgres_index)
print('MongoDB:', mongo_index)
print('Cassandra:', cassandra_index)

면접에서 SQL과 NoSQL 비교하기: 답변 방법

시스템 설계 면접에서 'SQL인가 NoSQL인가?'라는 질문을 받았을 때 한 단어로 답하지 마십시오. 대신 다음 구조를 따르시면 됩니다:

  1. 작업 부하를 밝힙니다: '이 시스템은 읽기 중심이고 복잡한 필터링이 필요하므로…'
  2. 요구 사항을 밝힙니다: '금융 트랜잭션에는 강한 일관성이 필요하므로…'
  3. 선택을 밝힙니다: '읽기 복제본을 사용하는 PostgreSQL을 선택하겠습니다'
  4. 상충 관계를 밝힙니다: '수평 쓰기 확장에는 샤딩이 필요해 복잡성이 커진다는 점이 상충 관계입니다'
  5. 대안을 언급합니다: '쓰기량이 더 많다면 DynamoDB를 고려할 수도 있습니다'
# Sample answer structure for 'SQL or NoSQL?'
def answer_storage_question(workload, consistency_need, scale):
    print(f'Workload: {workload}')
    print(f'Consistency: {consistency_need}')
    print(f'Scale: {scale}')
    print()
    if 'financial' in workload.lower() or consistency_need == 'strong':
        choice = 'PostgreSQL (ACID, strong consistency)'
        tradeoff = 'Horizontal write scaling requires sharding'
        alternative = 'Google Spanner for global transactions'
    elif 'event' in workload.lower() or 'log' in workload.lower():
        choice = 'Cassandra (high write throughput, time-series)'
        tradeoff = 'Eventual consistency; queries limited to partition key'
        alternative = 'Kafka + S3 for long-term event archival'
    else:
        choice = 'DynamoDB (managed, auto-scale, low latency)'
        tradeoff = 'Limited query flexibility; cross-item transactions limited'
        alternative = 'PostgreSQL if complex queries emerge'
    print(f'Choice: {choice}\nTrade-off: {tradeoff}\nAlternative: {alternative}')

answer_storage_question('Social media feed', 'eventual', '10M users')

빠른 확인

이 단원에서 다룬 자료 구조 및 알고리즘 — 코딩 면접 준비 개념에 대한 이해도를 확인해 보세요.

단원 요약

이 단원에서는 다음을 배웠습니다: SQL 데이터베이스는 ACID 트랜잭션과 복잡한 질의를 제공하지만 쓰기를 확장하기 어렵고, NoSQL 데이터베이스는 일관성 또는 질의 유연성을 희생하여 매우 큰 규모와 높은 가용성을 얻습니다. 또한 CAP 이론에 따라 분산 시스템은 네트워크 분할 중에 일관성과 가용성 중 하나를 선택해야 합니다. 그리고 운영 시스템은 일반적으로 다중 저장소 지속성을 사용하여 각 작업 부하를 적합한 저장 엔진에 맞춥니다. 다음으로는 캐싱 계층, CDN, 부하 분산을 살펴보고 읽기 중심 시스템을 더 확장하는 방법을 알아보겠습니다.

자주 묻는 질문

“확장 가능한 데이터 저장소: SQL과 NoSQL” 강의는 무료인가요?

네 — “확장 가능한 데이터 저장소: SQL과 NoSQL” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Coding Interview Prep 강의 전체를 잠금 해제할 수 있습니다. Coding Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

“확장 가능한 데이터 저장소: SQL과 NoSQL”에서 뭘 배우나요?

접근 패턴, 일관성, 확장 요구 사항을 바탕으로 관계형 데이터베이스, 키-값 저장소, 문서 데이터베이스, 와이드 컬럼 저장소 중에서 선택합니다. 브라우저에서 직접 실행하는 실습 코드로 Coding Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Coding Interview Prep을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Coding Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“확장 가능한 데이터 저장소: SQL과 NoSQL” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Coding Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Coding Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 시스템 설계 면접 프레임워크
  2. 확장 가능한 데이터 저장소: SQL과 NoSQL
  3. 캐싱, CDN과 부하 분산
  4. 속도 제한기 설계와 트위터 피드 설계
← Coding Interview Prep(으)로 돌아가기