[CS 기초 #16] 데이터베이스 이론: RDBMS부터 NoSQL까지 완벽 정리
데이터를 지배하는 자! RDBMS부터 NoSQL까지, 데이터베이스의 모든 것을 배웁니다.
🎯 이 글을 읽고 나면
- RDBMS와 NoSQL의 차이를 명확히 설명할 수 있습니다
- 정규화의 필요성과 1NF~3NF를 이해합니다
- 인덱스가 검색을 빠르게 하는 원리를 안다
- ACID와 CAP 정리를 설명할 수 있습니다
- 상황에 맞는 데이터베이스를 선택할 수 있습니다
📚 사전 지식
💡 핵심 개념 미리보기
데이터베이스는 데이터를 효율적으로 저장/관리하는 시스템입니다. RDBMS(MySQL, PostgreSQL)는 테이블과 SQL로 정형 데이터를 다루고, NoSQL(MongoDB, Redis)은 유연한 스키마로 비정형 데이터를 처리합니다. 인덱스로 검색 속도를 높이고, 트랜잭션으로 데이터 일관성을 보장합니다!
🗄️ 들어가며: 데이터베이스의 중요성
인스타그램은 어떻게 수십억 개의 사진을 관리할까요? 은행은 어떻게 수백만 건의 거래를 안전하게 처리할까요?
오늘은 현대 소프트웨어의 핵심인 데이터베이스를 완벽하게 정리해보겠습니다. RDBMS의 기초부터 NoSQL, 그리고 최신 트렌드까지 모두 다룹니다!
📊 데이터베이스 기초
데이터베이스 개념
graph TB
subgraph "데이터베이스 구조"
DB[Database]
DB --> T1[Table 1]
DB --> T2[Table 2]
DB --> T3[Table 3]
T1 --> R1[Rows/Records]
T1 --> C1[Columns/Fields]
T2 --> FK[Foreign Key]
FK --> T1
end
subgraph "DBMS 계층"
APP[Application]
APP --> DBMS[DBMS]
DBMS --> DB
DBMS --> STORAGE[Storage]
end
데이터베이스 종류
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def database_types():
"""데이터베이스 종류와 특징"""
db_types = {
"관계형 DB (RDBMS)": {
"구조": "테이블, 행, 열",
"특징": "ACID, SQL, 정규화",
"예시": "MySQL, PostgreSQL, Oracle",
"사용처": "정형 데이터, 트랜잭션"
},
"NoSQL - 문서형": {
"구조": "JSON/BSON 문서",
"특징": "스키마 자유, 중첩 데이터",
"예시": "MongoDB, CouchDB",
"사용처": "웹 애플리케이션, CMS"
},
"NoSQL - 키-값": {
"구조": "키-값 쌍",
"특징": "단순, 빠름",
"예시": "Redis, DynamoDB",
"사용처": "캐시, 세션 저장"
},
"NoSQL - 컬럼형": {
"구조": "컬럼 패밀리",
"특징": "분산, 확장성",
"예시": "Cassandra, HBase",
"사용처": "시계열 데이터, 로그"
},
"NoSQL - 그래프": {
"구조": "노드, 엣지",
"특징": "관계 중심",
"예시": "Neo4j, Amazon Neptune",
"사용처": "소셜 네트워크, 추천"
}
}
print("데이터베이스 종류:")
print("=" * 70)
for db_type, details in db_types.items():
print(f"\n{db_type}:")
for key, value in details.items():
print(f" {key}: {value}")
database_types()
🔤 SQL 기초
DDL (Data Definition Language)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- 테이블 생성
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_email (email)
);
-- 테이블 구조 변경
ALTER TABLE users
ADD COLUMN age INT,
ADD COLUMN status ENUM('active', 'inactive') DEFAULT 'active';
-- 테이블 삭제
DROP TABLE IF EXISTS users;
-- 인덱스 생성
CREATE INDEX idx_username ON users(username);
CREATE UNIQUE INDEX idx_email ON users(email);
DML (Data Manipulation Language)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import sqlite3
def sql_operations_demo():
"""SQL 기본 연산 데모"""
# 연결 및 테이블 생성
conn = sqlite3.connect(':memory:')
cursor = conn.cursor()
# 테이블 생성
cursor.execute('''
CREATE TABLE employees (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
department TEXT,
salary INTEGER,
hire_date DATE
)
''')
# INSERT - 데이터 삽입
employees = [
('김철수', 'Engineering', 70000, '2020-01-15'),
('이영희', 'Marketing', 65000, '2019-03-20'),
('박민수', 'Engineering', 80000, '2018-07-10'),
('정수진', 'HR', 60000, '2021-02-01'),
('최동훈', 'Engineering', 75000, '2019-11-30')
]
cursor.executemany(
'INSERT INTO employees (name, department, salary, hire_date) VALUES (?, ?, ?, ?)',
employees
)
# SELECT - 데이터 조회
print("모든 직원:")
cursor.execute('SELECT * FROM employees')
for row in cursor.fetchall():
print(f" {row}")
# WHERE 절
print("\nEngineering 부서 직원:")
cursor.execute('SELECT name, salary FROM employees WHERE department = ?', ('Engineering',))
for row in cursor.fetchall():
print(f" {row[0]}: ${row[1]:,}")
# UPDATE - 데이터 수정
cursor.execute('UPDATE employees SET salary = salary * 1.1 WHERE department = ?', ('Engineering',))
print(f"\nEngineering 부서 급여 10% 인상")
# DELETE - 데이터 삭제
cursor.execute('DELETE FROM employees WHERE salary < ?', (65000,))
print(f"급여 65000 미만 직원 삭제")
# 집계 함수
cursor.execute('''
SELECT
department,
COUNT(*) as count,
AVG(salary) as avg_salary,
MAX(salary) as max_salary,
MIN(salary) as min_salary
FROM employees
GROUP BY department
''')
print("\n부서별 통계:")
for row in cursor.fetchall():
print(f" {row[0]}: 인원={row[1]}, 평균=${row[2]:,.0f}, 최대=${row[3]:,}, 최소=${row[4]:,}")
conn.close()
sql_operations_demo()
JOIN 연산
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
def join_operations_demo():
"""JOIN 연산 데모"""
conn = sqlite3.connect(':memory:')
cursor = conn.cursor()
# 테이블 생성
cursor.execute('''
CREATE TABLE departments (
id INTEGER PRIMARY KEY,
name TEXT UNIQUE,
location TEXT
)
''')
cursor.execute('''
CREATE TABLE employees (
id INTEGER PRIMARY KEY,
name TEXT,
dept_id INTEGER,
salary INTEGER,
FOREIGN KEY (dept_id) REFERENCES departments(id)
)
''')
cursor.execute('''
CREATE TABLE projects (
id INTEGER PRIMARY KEY,
name TEXT,
emp_id INTEGER,
FOREIGN KEY (emp_id) REFERENCES employees(id)
)
''')
# 데이터 삽입
departments = [
(1, 'Engineering', 'Seoul'),
(2, 'Marketing', 'Busan'),
(3, 'HR', 'Seoul'),
(4, 'Finance', 'Jeju')
]
employees = [
(1, '김철수', 1, 70000),
(2, '이영희', 2, 65000),
(3, '박민수', 1, 80000),
(4, '정수진', 3, 60000),
(5, '최동훈', None, 75000) # 부서 미배정
]
projects = [
(1, 'Project A', 1),
(2, 'Project B', 1),
(3, 'Project C', 3)
]
cursor.executemany('INSERT INTO departments VALUES (?, ?, ?)', departments)
cursor.executemany('INSERT INTO employees VALUES (?, ?, ?, ?)', employees)
cursor.executemany('INSERT INTO projects VALUES (?, ?, ?)', projects)
# INNER JOIN
print("INNER JOIN - 부서가 있는 직원만:")
cursor.execute('''
SELECT e.name, d.name, d.location
FROM employees e
INNER JOIN departments d ON e.dept_id = d.id
''')
for row in cursor.fetchall():
print(f" {row[0]} - {row[1]} ({row[2]})")
# LEFT JOIN
print("\nLEFT JOIN - 모든 직원 (부서 없어도):")
cursor.execute('''
SELECT e.name, COALESCE(d.name, '미배정') as dept
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.id
''')
for row in cursor.fetchall():
print(f" {row[0]} - {row[1]}")
# RIGHT JOIN (SQLite는 지원 안함, LEFT JOIN으로 대체)
print("\nRIGHT JOIN 효과 - 모든 부서:")
cursor.execute('''
SELECT d.name, COUNT(e.id) as emp_count
FROM departments d
LEFT JOIN employees e ON d.id = e.dept_id
GROUP BY d.id
''')
for row in cursor.fetchall():
print(f" {row[0]}: {row[1]}명")
# 다중 JOIN
print("\n다중 JOIN - 직원-부서-프로젝트:")
cursor.execute('''
SELECT e.name, d.name, p.name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.id
LEFT JOIN projects p ON e.id = p.emp_id
WHERE p.name IS NOT NULL
''')
for row in cursor.fetchall():
print(f" {row[0]} ({row[1]}) - {row[2]}")
conn.close()
join_operations_demo()
📐 정규화 (Normalization)
정규화 단계
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def normalization_examples():
"""정규화 예시"""
print("정규화 단계별 예시")
print("=" * 70)
# 비정규형
print("\n비정규형 (Unnormalized):")
print("Orders 테이블:")
print("| OrderID | Customer | Products | Prices |")
print("|---------|----------|----------------------|-------------|")
print("| 1 | 김철수 | 노트북, 마우스 | 1000, 30 |")
print("| 2 | 이영희 | 키보드 | 50 |")
print("문제: 반복 그룹 존재")
# 1NF
print("\n\n제1정규형 (1NF) - 원자값:")
print("Orders 테이블:")
print("| OrderID | Customer | Product | Price |")
print("|---------|----------|---------|-------|")
print("| 1 | 김철수 | 노트북 | 1000 |")
print("| 1 | 김철수 | 마우스 | 30 |")
print("| 2 | 이영희 | 키보드 | 50 |")
print("✓ 각 필드가 원자값")
# 2NF
print("\n\n제2정규형 (2NF) - 부분 종속 제거:")
print("Orders 테이블:")
print("| OrderID | Customer |")
print("|---------|----------|")
print("| 1 | 김철수 |")
print("| 2 | 이영희 |")
print("\nOrderItems 테이블:")
print("| OrderID | ProductID | Quantity |")
print("|---------|-----------|----------|")
print("| 1 | 101 | 1 |")
print("| 1 | 102 | 1 |")
print("\nProducts 테이블:")
print("| ProductID | Product | Price |")
print("|-----------|---------|-------|")
print("| 101 | 노트북 | 1000 |")
print("| 102 | 마우스 | 30 |")
print("✓ 기본키의 일부에만 종속된 속성 분리")
# 3NF
print("\n\n제3정규형 (3NF) - 이행 종속 제거:")
print("Customers 테이블:")
print("| CustomerID | Name | CityID |")
print("|------------|--------|--------|")
print("| 1 | 김철수 | 1 |")
print("\nCities 테이블:")
print("| CityID | City | Country |")
print("|--------|------|---------|")
print("| 1 | 서울 | 한국 |")
print("✓ 비키 속성 간 종속 제거")
# BCNF
print("\n\nBCNF - 모든 결정자가 후보키:")
print("✓ 모든 함수 종속에서 결정자가 후보키")
normalization_examples()
정규화 장단점
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def normalization_tradeoffs():
"""정규화 트레이드오프"""
tradeoffs = {
"정규화 장점": [
"데이터 중복 최소화",
"데이터 무결성 향상",
"이상 현상(Anomaly) 방지",
"저장 공간 절약",
"데이터 일관성 유지"
],
"정규화 단점": [
"JOIN 연산 증가",
"쿼리 복잡도 증가",
"성능 저하 가능성",
"개발 복잡도 증가"
],
"역정규화 시기": [
"읽기 성능이 중요한 경우",
"JOIN이 너무 많은 경우",
"실시간 집계가 필요한 경우",
"히스토리 데이터 저장"
]
}
print("정규화 트레이드오프:")
print("=" * 50)
for category, items in tradeoffs.items():
print(f"\n{category}:")
for item in items:
print(f" • {item}")
normalization_tradeoffs()
🔍 인덱스 (Index)
인덱스 종류와 구조
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class BTreeIndex:
"""B-Tree 인덱스 시뮬레이션"""
def __init__(self, order=3):
self.order = order
self.root = None
class Node:
def __init__(self, leaf=True):
self.keys = []
self.children = []
self.leaf = leaf
def search(self, key):
"""인덱스 검색 - O(log n)"""
return self._search_helper(self.root, key)
def _search_helper(self, node, key):
if node is None:
return False
i = 0
while i < len(node.keys) and key > node.keys[i]:
i += 1
if i < len(node.keys) and key == node.keys[i]:
return True
if node.leaf:
return False
return self._search_helper(node.children[i], key)
def index_types():
"""인덱스 종류"""
index_info = {
"B-Tree 인덱스": {
"구조": "균형 트리",
"특징": "범위 검색 효율적",
"사용": "대부분의 RDBMS 기본",
"시간복잡도": "O(log n)"
},
"Hash 인덱스": {
"구조": "해시 테이블",
"특징": "동등 비교만 가능",
"사용": "Memory 테이블",
"시간복잡도": "O(1)"
},
"Bitmap 인덱스": {
"구조": "비트맵",
"특징": "카디널리티 낮은 컬럼",
"사용": "데이터 웨어하우스",
"시간복잡도": "O(1)"
},
"Full-text 인덱스": {
"구조": "역 인덱스",
"특징": "텍스트 검색",
"사용": "검색 엔진",
"시간복잡도": "O(n)"
}
}
print("인덱스 종류:")
print("=" * 60)
for idx_type, details in index_info.items():
print(f"\n{idx_type}:")
for key, value in details.items():
print(f" {key}: {value}")
index_types()
인덱스 최적화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def index_optimization_demo():
"""인덱스 최적화 예시"""
conn = sqlite3.connect(':memory:')
cursor = conn.cursor()
# 대량 데이터 테이블 생성
cursor.execute('''
CREATE TABLE users (
id INTEGER PRIMARY KEY,
username TEXT,
email TEXT,
age INTEGER,
created_at DATE
)
''')
# 데이터 삽입 (실제로는 더 많은 데이터)
import random
from datetime import datetime, timedelta
users = []
for i in range(10000):
username = f"user{i}"
email = f"user{i}@example.com"
age = random.randint(18, 80)
created = datetime.now() - timedelta(days=random.randint(0, 365))
users.append((username, email, age, created.isoformat()))
cursor.executemany(
'INSERT INTO users (username, email, age, created_at) VALUES (?, ?, ?, ?)',
users
)
# 인덱스 없이 쿼리
print("인덱스 최적화 예시:")
print("-" * 50)
# EXPLAIN QUERY PLAN 시뮬레이션
print("\n1. 인덱스 없는 쿼리:")
print(" SELECT * FROM users WHERE email = '[email protected]'")
print(" → Full Table Scan: 10,000개 행 검색")
# 인덱스 생성
cursor.execute('CREATE INDEX idx_email ON users(email)')
print("\n2. 인덱스 생성 후:")
print(" CREATE INDEX idx_email ON users(email)")
print(" → Index Scan: 1개 행 직접 접근")
# 복합 인덱스
cursor.execute('CREATE INDEX idx_age_created ON users(age, created_at)')
print("\n3. 복합 인덱스:")
print(" CREATE INDEX idx_age_created ON users(age, created_at)")
print(" WHERE age = 25 AND created_at > '2024-01-01'")
print(" → 복합 인덱스 활용")
# 커버링 인덱스
print("\n4. 커버링 인덱스:")
print(" CREATE INDEX idx_covering ON users(email, username, age)")
print(" SELECT username, age FROM users WHERE email = '[email protected]'")
print(" → 인덱스만으로 쿼리 완료 (테이블 접근 불필요)")
conn.close()
index_optimization_demo()
🔐 트랜잭션과 ACID
ACID 속성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def acid_properties():
"""ACID 속성 설명과 예시"""
print("ACID 속성:")
print("=" * 70)
# Atomicity
print("\n1. Atomicity (원자성):")
print(" 정의: 트랜잭션의 모든 연산이 완전히 수행되거나 전혀 수행되지 않음")
print(" 예시: 계좌 이체")
print(" ```sql")
print(" BEGIN TRANSACTION;")
print(" UPDATE accounts SET balance = balance - 100 WHERE id = 1;")
print(" UPDATE accounts SET balance = balance + 100 WHERE id = 2;")
print(" COMMIT; -- 둘 다 성공하거나 둘 다 실패")
print(" ```")
# Consistency
print("\n2. Consistency (일관성):")
print(" 정의: 트랜잭션 전후로 데이터베이스가 일관된 상태 유지")
print(" 예시: 계좌 총액 불변")
print(" ```sql")
print(" -- 제약 조건: balance >= 0")
print(" UPDATE accounts SET balance = balance - 1000 WHERE id = 1;")
print(" -- 잔액 부족시 트랜잭션 롤백")
print(" ```")
# Isolation
print("\n3. Isolation (격리성):")
print(" 정의: 동시 실행 트랜잭션들이 서로 영향을 주지 않음")
print(" 격리 수준:")
print(" • Read Uncommitted: Dirty Read 가능")
print(" • Read Committed: Dirty Read 방지")
print(" • Repeatable Read: Non-repeatable Read 방지")
print(" • Serializable: Phantom Read 방지")
# Durability
print("\n4. Durability (지속성):")
print(" 정의: 커밋된 트랜잭션의 결과는 영구적으로 보존")
print(" 예시: 시스템 장애 후에도 데이터 유지")
print(" 구현: Write-Ahead Logging (WAL)")
acid_properties()
트랜잭션 격리 수준
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
def isolation_levels_demo():
"""트랜잭션 격리 수준 데모"""
problems = {
"Dirty Read": {
"설명": "커밋되지 않은 데이터 읽기",
"예시": "T1이 수정 중인 데이터를 T2가 읽음",
"방지": "Read Committed 이상"
},
"Non-repeatable Read": {
"설명": "같은 쿼리가 다른 결과 반환",
"예시": "T1이 두 번 읽는 사이 T2가 수정",
"방지": "Repeatable Read 이상"
},
"Phantom Read": {
"설명": "없던 행이 생김",
"예시": "T1이 범위 조회 중 T2가 삽입",
"방지": "Serializable"
}
}
levels = {
"Read Uncommitted": {
"Dirty Read": "O",
"Non-repeatable": "O",
"Phantom": "O",
"성능": "최고"
},
"Read Committed": {
"Dirty Read": "X",
"Non-repeatable": "O",
"Phantom": "O",
"성능": "좋음"
},
"Repeatable Read": {
"Dirty Read": "X",
"Non-repeatable": "X",
"Phantom": "O",
"성능": "보통"
},
"Serializable": {
"Dirty Read": "X",
"Non-repeatable": "X",
"Phantom": "X",
"성능": "최저"
}
}
print("트랜잭션 격리 수준:")
print("=" * 70)
print("\n문제 현상:")
for problem, details in problems.items():
print(f"\n{problem}:")
for key, value in details.items():
print(f" {key}: {value}")
print("\n\n격리 수준별 특징:")
print("-" * 60)
print(f"{'격리 수준':<20} {'Dirty':<10} {'Non-rep':<10} {'Phantom':<10} {'성능':<10}")
print("-" * 60)
for level, features in levels.items():
print(f"{level:<20} {features['Dirty Read']:<10} {features['Non-repeatable']:<10} "
f"{features['Phantom']:<10} {features['성능']:<10}")
isolation_levels_demo()
🌐 NoSQL 데이터베이스
CAP 정리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def cap_theorem():
"""CAP 정리 설명"""
print("CAP 정리 (Brewer's Theorem):")
print("=" * 70)
print("분산 시스템에서 다음 3가지 중 최대 2개만 보장 가능:\n")
properties = {
"Consistency (일관성)": {
"정의": "모든 노드가 같은 데이터를 가짐",
"예시": "강한 일관성 vs 최종 일관성"
},
"Availability (가용성)": {
"정의": "모든 요청이 응답을 받음",
"예시": "장애 시에도 서비스 지속"
},
"Partition Tolerance (분할 내성)": {
"정의": "네트워크 분할 시에도 동작",
"예시": "노드 간 통신 장애 처리"
}
}
for prop, details in properties.items():
print(f"\n{prop}:")
for key, value in details.items():
print(f" {key}: {value}")
print("\n\n시스템별 선택:")
systems = {
"CA (일관성 + 가용성)": {
"특징": "단일 노드 시스템",
"예시": "전통적 RDBMS (MySQL, PostgreSQL)",
"단점": "분산 불가"
},
"CP (일관성 + 분할내성)": {
"특징": "강한 일관성 분산 시스템",
"예시": "MongoDB, HBase, Redis",
"단점": "가용성 희생"
},
"AP (가용성 + 분할내성)": {
"특징": "최종 일관성 분산 시스템",
"예시": "Cassandra, DynamoDB, CouchDB",
"단점": "일관성 희생"
}
}
for system, details in systems.items():
print(f"\n{system}:")
for key, value in details.items():
print(f" {key}: {value}")
cap_theorem()
NoSQL 데이터 모델링
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# MongoDB 스타일 문서형 DB
def document_db_example():
"""문서형 DB 예시"""
# RDBMS에서는 여러 테이블로 분리될 데이터
user_document = {
"_id": "user123",
"username": "johndoe",
"email": "[email protected]",
"profile": {
"firstName": "John",
"lastName": "Doe",
"age": 30,
"address": {
"street": "123 Main St",
"city": "Seoul",
"country": "Korea"
}
},
"orders": [
{
"orderId": "order1",
"date": "2024-01-15",
"total": 150.00,
"items": [
{"productId": "prod1", "quantity": 2, "price": 50.00},
{"productId": "prod2", "quantity": 1, "price": 50.00}
]
}
],
"preferences": {
"newsletter": True,
"notifications": {
"email": True,
"sms": False
}
},
"tags": ["premium", "active", "verified"]
}
print("문서형 DB 예시 (MongoDB):")
print("-" * 50)
import json
print(json.dumps(user_document, indent=2, ensure_ascii=False))
print("\n\n장점:")
print("• 스키마 유연성")
print("• 중첩 데이터 저장")
print("• JOIN 불필요")
print("• 수평 확장 용이")
document_db_example()
Redis 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def redis_patterns():
"""Redis 사용 패턴"""
print("Redis 사용 패턴:")
print("=" * 60)
patterns = {
"캐싱": {
"용도": "DB 쿼리 결과 캐시",
"예시": """
# 캐시 확인
cached = redis.get(f"user:{user_id}")
if cached:
return json.loads(cached)
# DB 조회 후 캐시 저장
user = db.query(f"SELECT * FROM users WHERE id = {user_id}")
redis.setex(f"user:{user_id}", 3600, json.dumps(user))
""",
"TTL": "1시간"
},
"세션 저장": {
"용도": "사용자 세션 관리",
"예시": """
# 세션 저장
redis.setex(f"session:{session_id}", 1800, user_data)
# 세션 조회
session = redis.get(f"session:{session_id}")
""",
"TTL": "30분"
},
"실시간 랭킹": {
"용도": "리더보드, 순위",
"예시": """
# 점수 추가/업데이트
redis.zadd("leaderboard", {user_id: score})
# 상위 10명 조회
top10 = redis.zrevrange("leaderboard", 0, 9, withscores=True)
""",
"자료구조": "Sorted Set"
},
"발행/구독": {
"용도": "실시간 메시징",
"예시": """
# 발행
redis.publish("channel:news", message)
# 구독
pubsub = redis.pubsub()
pubsub.subscribe("channel:news")
""",
"패턴": "Pub/Sub"
},
"분산 락": {
"용도": "동시성 제어",
"예시": """
# 락 획득
if redis.set("lock:resource", "1", nx=True, ex=10):
# 임계 영역
process_resource()
redis.delete("lock:resource")
""",
"TTL": "10초"
}
}
for pattern, details in patterns.items():
print(f"\n{pattern}:")
for key, value in details.items():
if key == "예시":
print(f" {key}:{value}")
else:
print(f" {key}: {value}")
redis_patterns()
🔄 데이터베이스 최적화
쿼리 최적화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def query_optimization_tips():
"""쿼리 최적화 팁"""
tips = {
"SELECT 최적화": [
"SELECT * 대신 필요한 컬럼만 선택",
"LIMIT 사용으로 결과 제한",
"EXISTS 대신 IN 사용 고려",
"서브쿼리보다 JOIN 선호"
],
"JOIN 최적화": [
"작은 테이블을 먼저 JOIN",
"ON 절에 인덱스 컬럼 사용",
"불필요한 JOIN 제거",
"적절한 JOIN 타입 선택"
],
"WHERE 최적화": [
"인덱스 컬럼 우선 사용",
"함수 사용 자제 (WHERE YEAR(date) 대신 범위 사용)",
"OR 대신 IN 사용",
"NULL 비교 최소화"
],
"인덱스 활용": [
"카디널리티 높은 컬럼에 인덱스",
"복합 인덱스 순서 고려",
"커버링 인덱스 활용",
"불필요한 인덱스 제거"
],
"기타 최적화": [
"배치 INSERT 사용",
"EXPLAIN으로 실행 계획 확인",
"쿼리 캐시 활용",
"파티셔닝 고려"
]
}
print("쿼리 최적화 팁:")
print("=" * 60)
for category, items in tips.items():
print(f"\n{category}:")
for item in items:
print(f" • {item}")
query_optimization_tips()
샤딩과 복제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def sharding_replication():
"""샤딩과 복제 전략"""
print("데이터베이스 확장 전략:")
print("=" * 70)
# 수직 확장 vs 수평 확장
print("\n1. 확장 방식:")
print(" 수직 확장 (Scale-up): 서버 성능 향상")
print(" 수평 확장 (Scale-out): 서버 수 증가")
# 복제
print("\n2. 복제 (Replication):")
print(" Master-Slave:")
print(" • Master: 쓰기 전담")
print(" • Slave: 읽기 전담")
print(" • 읽기 성능 향상")
print(" Master-Master:")
print(" • 양방향 복제")
print(" • 충돌 해결 필요")
# 샤딩
print("\n3. 샤딩 (Sharding):")
print(" 수평 샤딩:")
print(" • 행 단위로 분할")
print(" • 예: user_id % 4로 샤드 결정")
print(" 수직 샤딩:")
print(" • 테이블/컬럼 단위로 분할")
print(" • 예: 사용자 정보와 주문 정보 분리")
# 샤딩 전략
print("\n4. 샤딩 키 전략:")
strategies = {
"Range": "날짜, ID 범위로 분할",
"Hash": "해시 함수로 균등 분산",
"List": "지역, 카테고리별 분할",
"Composite": "여러 키 조합"
}
for strategy, description in strategies.items():
print(f" {strategy}: {description}")
sharding_replication()
🎯 핵심 정리
꼭 기억해야 할 5가지
- ACID: 트랜잭션의 4가지 속성
- 정규화: 중복 제거와 무결성 vs 성능
- 인덱스: B-Tree 구조와 최적화
- CAP 정리: 분산 시스템의 트레이드오프
- SQL vs NoSQL: 각각의 장단점과 사용 시기
데이터베이스 선택 가이드
- RDBMS: 정형 데이터, ACID 필요, 복잡한 쿼리
- 문서형 DB: 유연한 스키마, 중첩 데이터
- 키-값 DB: 단순 조회, 캐싱, 세션
- 그래프 DB: 관계 중심 데이터, 추천 시스템
💬 면접 질문으로 점검하기
1. 정규화는 왜 하고, 언제 반정규화를 고려하나요?
정규화는 데이터 중복을 줄이고 삽입, 수정, 삭제 이상을 방지하기 위해 사용합니다. 주문, 회원, 상품처럼 관계와 무결성이 중요한 데이터에서는 정규화된 모델이 유지보수에 유리합니다.
반정규화는 조회 성능이나 분석 편의가 더 중요할 때 고려합니다. 예를 들어 주문 목록 화면에서 매번 여러 테이블을 JOIN하는 비용이 크다면 자주 쓰는 값을 일부 중복 저장할 수 있습니다. 단, 중복 데이터의 갱신 책임이 생기므로 동기화 전략이 함께 필요합니다.
2. 인덱스를 많이 만들면 항상 좋은가요?
아닙니다. 인덱스는 조회 성능을 높이지만 저장 공간을 사용하고, INSERT/UPDATE/DELETE 때 인덱스도 함께 갱신해야 하므로 쓰기 성능이 떨어질 수 있습니다. 선택도가 낮은 컬럼이나 자주 바뀌는 컬럼에 무분별하게 인덱스를 만들면 오히려 손해입니다.
좋은 답변은 WHERE, JOIN, ORDER BY에 자주 쓰이는 컬럼, 카디널리티, 복합 인덱스의 컬럼 순서, 실행 계획 확인까지 함께 언급합니다.
3. 트랜잭션 격리 수준은 왜 필요한가요?
동시에 여러 트랜잭션이 같은 데이터를 읽고 쓸 때 Dirty Read, Non-repeatable Read, Phantom Read 같은 문제가 생길 수 있습니다. 격리 수준은 일관성과 동시성 사이의 균형을 조절하는 장치입니다.
격리 수준을 높이면 데이터 일관성은 강해지지만 락 경합과 대기 시간이 늘어날 수 있습니다. 따라서 결제, 재고 차감처럼 정확성이 중요한 흐름과 조회 중심 화면의 요구사항을 분리해서 선택해야 합니다.
4. RDBMS와 NoSQL은 어떻게 선택하나요?
스키마가 명확하고 트랜잭션과 JOIN이 중요하면 RDBMS가 기본 선택입니다. 반대로 데이터 구조가 자주 바뀌거나, 키 기반 고속 조회, 대규모 분산 저장, 특정 데이터 모델이 중요하면 NoSQL을 검토할 수 있습니다.
중요한 것은 “NoSQL이 더 빠르다”처럼 단정하지 않는 것입니다. 쿼리 패턴, 일관성 요구사항, 운영 복잡도, 팀의 경험까지 포함해 선택해야 합니다.
🧪 데이터베이스 설계 점검표
새 테이블이나 컬렉션을 설계할 때는 아래 질문을 먼저 확인하세요.
| 점검 항목 | 확인 질문 |
|---|---|
| 데이터 모델 | 핵심 엔티티와 관계가 명확한가? |
| 무결성 | 반드시 지켜야 하는 제약 조건은 무엇인가? |
| 조회 패턴 | 가장 자주 실행되는 쿼리는 무엇인가? |
| 인덱스 | 조회 조건과 정렬 조건에 맞는 인덱스가 있는가? |
| 트랜잭션 | 여러 변경을 하나의 원자적 작업으로 묶어야 하는가? |
| 성장 | 데이터가 10배 늘어도 현재 구조가 버틸 수 있는가? |
| 운영 | 백업, 복구, 마이그레이션, 모니터링 계획이 있는가? |
📚 추가 학습 자료
추천 자료
- “Database System Concepts” - Silberschatz
- “Designing Data-Intensive Applications” - Martin Kleppmann
- SQL 연습: HackerRank, LeetCode Database
🚀 다음 시간 예고
다음 포스트에서는 소프트웨어 공학을 다룹니다. 개발 방법론, 디자인 패턴, 테스팅, CI/CD 등을 알아보겠습니다!
