Skip to content

✍️ 필사 모드: 데이터베이스 기초 완전 가이드 — SQL, 정규화, 인덱스, 트랜잭션, NoSQL

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

1. 데이터베이스란 무엇인가

데이터베이스(Database)는 구조화된 데이터를 저장하고 관리하는 시스템이다. 현대 소프트웨어에서 데이터 저장과 조회는 핵심 기능이며, 올바른 데이터베이스 선택과 설계는 애플리케이션 성능과 확장성을 좌우한다.

RDBMS vs NoSQL

관계형 데이터베이스(RDBMS)와 NoSQL은 근본적으로 다른 철학을 가진다.

구분RDBMSNoSQL
데이터 모델테이블(행과 열)문서, 키-값, 컬럼, 그래프
스키마고정 스키마유연한 스키마
확장 방식수직 확장(Scale-Up)수평 확장(Scale-Out)
트랜잭션ACID 보장BASE(일부 ACID 지원)
쿼리 언어SQL각 DB 고유 API
적합한 경우복잡한 관계, 정합성 중요대용량, 유연한 스키마 필요

주요 데이터베이스 비교

DB유형특징주요 사용처
PostgreSQLRDBMSJSONB, 확장성, 풍부한 자료형범용
MySQLRDBMS높은 호환성, InnoDB 엔진웹 서비스
MongoDBDocument유연한 스키마, 집계 파이프라인콘텐츠 관리
RedisKey-Value인메모리, 초고속캐시, 세션
CassandraWide Column높은 쓰기 처리량IoT, 시계열
Neo4jGraph관계 탐색 최적화소셜 네트워크

2. SQL 기초

SQL(Structured Query Language)은 관계형 데이터베이스를 다루는 표준 언어다.

SELECT와 기본 문법

-- 기본 조회
SELECT name, email, created_at
FROM users
WHERE status = 'active'
ORDER BY created_at DESC
LIMIT 10;
-- 별칭(alias) 활용
SELECT
    u.name AS user_name,
    COUNT(o.id) AS order_count,
    SUM(o.total_amount) AS total_spent
FROM users u
JOIN orders o ON u.id = o.user_id
GROUP BY u.name;

JOIN의 종류

JOIN은 여러 테이블의 데이터를 결합하는 핵심 연산이다.

-- INNER JOIN: 양쪽 모두 일치하는 행만
SELECT u.name, o.order_date
FROM users u
INNER JOIN orders o ON u.id = o.user_id;

-- LEFT JOIN: 왼쪽 테이블 전체 + 일치하는 오른쪽
SELECT u.name, o.order_date
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;

-- RIGHT JOIN: 오른쪽 테이블 전체 + 일치하는 왼쪽
SELECT u.name, o.order_date
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;

-- FULL OUTER JOIN: 양쪽 모두 포함
SELECT u.name, o.order_date
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id;

JOIN 선택 가이드:

  • INNER JOIN -- 양쪽 모두 존재하는 데이터만 필요할 때
  • LEFT JOIN -- 기준 테이블의 모든 행을 보존해야 할 때
  • RIGHT JOIN -- LEFT JOIN으로 대체 가능하므로 실무에서는 드물게 사용
  • FULL OUTER JOIN -- 양쪽 모든 데이터를 합칠 때

GROUP BY와 HAVING

-- 부서별 평균 급여가 500만원 이상인 부서
SELECT
    department,
    COUNT(*) AS employee_count,
    AVG(salary) AS avg_salary
FROM employees
GROUP BY department
HAVING AVG(salary) >= 5000000
ORDER BY avg_salary DESC;

WHERE는 개별 행을 필터링하고, HAVING은 그룹화된 결과를 필터링한다.

서브쿼리

-- 평균 급여보다 높은 급여를 받는 직원
SELECT name, salary
FROM employees
WHERE salary > (
    SELECT AVG(salary) FROM employees
);

-- EXISTS를 사용한 서브쿼리
SELECT u.name
FROM users u
WHERE EXISTS (
    SELECT 1 FROM orders o
    WHERE o.user_id = u.id
    AND o.created_at >= '2026-01-01'
);

-- IN을 사용한 서브쿼리
SELECT name, department
FROM employees
WHERE department IN (
    SELECT department
    FROM employees
    GROUP BY department
    HAVING COUNT(*) > 10
);

3. 정규화 (Normalization)

정규화는 데이터 중복을 줄이고 무결성을 높이기 위해 테이블을 분리하는 과정이다.

제1정규형 (1NF)

모든 컬럼 값이 원자적(Atomic)이어야 한다. 하나의 셀에 여러 값이 들어가면 안 된다.

-- 1NF 위반: phone_numbers에 여러 값 저장
-- name: '홍길동', phone_numbers: '010-1234-5678, 010-9876-5432'

-- 1NF 준수: 별도 테이블로 분리
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

CREATE TABLE phone_numbers (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    phone VARCHAR(20) NOT NULL
);

제2정규형 (2NF)

1NF를 만족하면서, 부분 종속(Partial Dependency)이 없어야 한다. 복합 키의 일부에만 종속되는 컬럼을 분리한다.

-- 2NF 위반: student_name은 student_id에만 종속
-- (student_id, course_id) -> grade  (OK)
-- student_id -> student_name  (부분 종속!)

-- 2NF 준수
CREATE TABLE students (
    student_id SERIAL PRIMARY KEY,
    student_name VARCHAR(100) NOT NULL
);

CREATE TABLE enrollments (
    student_id INTEGER REFERENCES students(student_id),
    course_id INTEGER REFERENCES courses(course_id),
    grade CHAR(2),
    PRIMARY KEY (student_id, course_id)
);

제3정규형 (3NF)

2NF를 만족하면서, 이행 종속(Transitive Dependency)이 없어야 한다.

-- 3NF 위반: department_name은 department_id를 통해 이행 종속
-- employee_id -> department_id -> department_name

-- 3NF 준수
CREATE TABLE departments (
    department_id SERIAL PRIMARY KEY,
    department_name VARCHAR(100) NOT NULL
);

CREATE TABLE employees (
    employee_id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    department_id INTEGER REFERENCES departments(department_id)
);

BCNF (Boyce-Codd 정규형)

3NF를 만족하면서, 모든 결정자가 후보키인 형태다. 3NF보다 더 엄격한 조건을 적용한다.

반정규화는 언제 하는가

반정규화(Denormalization)는 조회 성능을 위해 의도적으로 중복을 허용하는 것이다.

반정규화를 고려할 상황:

  • 읽기가 쓰기보다 압도적으로 많을 때
  • 복잡한 JOIN이 반복적으로 발생할 때
  • 실시간 집계가 필요할 때
  • 캐시만으로 성능 요구사항을 충족하지 못할 때
-- 반정규화 예: 주문 테이블에 사용자 이름을 중복 저장
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    user_name VARCHAR(100),  -- 반정규화: 조회 시 JOIN 제거
    total_amount DECIMAL(10, 2),
    created_at TIMESTAMP DEFAULT NOW()
);

4. 인덱스 (Index)

인덱스는 데이터 검색 속도를 높이는 자료구조다. 책의 색인과 같은 역할을 한다.

B-Tree 인덱스

가장 범용적인 인덱스 유형이다. 범위 검색, 정렬, 동등 비교 모두에 효과적이다.

-- 단일 컬럼 인덱스
CREATE INDEX idx_users_email ON users(email);

-- 인덱스를 사용하는 쿼리 예시
SELECT * FROM users WHERE email = 'test@example.com';  -- 인덱스 사용
SELECT * FROM users WHERE email LIKE 'test%';           -- 인덱스 사용
SELECT * FROM users WHERE email LIKE '%test';           -- 인덱스 미사용 (선행 와일드카드)

Hash 인덱스

동등 비교(=)에만 사용 가능하다. 범위 검색은 불가능하지만 동등 비교에서는 B-Tree보다 빠를 수 있다.

CREATE INDEX idx_users_email_hash ON users USING HASH (email);

GIN 인덱스 (Generalized Inverted Index)

배열, JSONB, 전문 검색에 적합한 인덱스다.

-- JSONB 컬럼에 GIN 인덱스
CREATE INDEX idx_products_tags ON products USING GIN (tags);

-- 전문 검색용 GIN 인덱스
CREATE INDEX idx_articles_search ON articles USING GIN (
    to_tsvector('korean', title || ' ' || content)
);

GiST 인덱스 (Generalized Search Tree)

공간 데이터, 범위 타입, 전문 검색에 사용한다.

-- 공간 데이터 인덱스
CREATE INDEX idx_locations_coords ON locations USING GIST (coordinates);

복합 인덱스

여러 컬럼을 조합한 인덱스다. 컬럼 순서가 매우 중요하다.

-- 복합 인덱스: 선행 컬럼부터 매칭
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);

-- 이 인덱스가 효과적인 쿼리들:
SELECT * FROM orders WHERE user_id = 1;                          -- 사용
SELECT * FROM orders WHERE user_id = 1 AND created_at > '2026-01-01'; -- 사용
SELECT * FROM orders WHERE created_at > '2026-01-01';            -- 미사용 (선행 컬럼 누락)

커버링 인덱스

쿼리에 필요한 모든 컬럼을 인덱스에 포함시켜, 테이블 접근 없이 인덱스만으로 결과를 반환한다.

-- 커버링 인덱스
CREATE INDEX idx_orders_covering ON orders(user_id, created_at, total_amount);

-- 인덱스만으로 처리 가능 (Index Only Scan)
SELECT user_id, created_at, total_amount
FROM orders
WHERE user_id = 1 AND created_at > '2026-01-01';

인덱스 설계 원칙

  1. 카디널리티(고유값 수)가 높은 컬럼에 인덱스를 건다
  2. WHERE, JOIN, ORDER BY에 자주 사용되는 컬럼을 우선한다
  3. 쓰기 성능과 저장 공간 트레이드오프를 고려한다
  4. 복합 인덱스에서는 선택도가 높은 컬럼을 앞에 둔다

5. 트랜잭션 (Transaction)

트랜잭션은 데이터베이스의 상태를 변화시키는 하나의 논리적 작업 단위다.

ACID 속성

  • Atomicity (원자성) -- 트랜잭션 내 모든 연산은 전부 성공하거나 전부 실패한다
  • Consistency (일관성) -- 트랜잭션 전후로 데이터베이스는 일관된 상태를 유지한다
  • Isolation (격리성) -- 동시에 실행되는 트랜잭션이 서로 영향을 주지 않는다
  • Durability (지속성) -- 완료된 트랜잭션의 결과는 영구적으로 보존된다
-- 계좌 이체 트랜잭션 예시
BEGIN;

UPDATE accounts SET balance = balance - 100000
WHERE account_id = 'A001';

UPDATE accounts SET balance = balance + 100000
WHERE account_id = 'B001';

-- 잔액 확인
SELECT balance FROM accounts WHERE account_id = 'A001';

COMMIT;  -- 또는 문제 발생 시 ROLLBACK;

격리 수준 (Isolation Level)

격리 수준이 높을수록 데이터 정합성은 높아지지만 동시성은 낮아진다.

격리 수준Dirty ReadNon-Repeatable ReadPhantom Read성능
Read Uncommitted가능가능가능최고
Read Committed불가가능가능높음
Repeatable Read불가불가가능보통
Serializable불가불가불가낮음
-- 격리 수준 설정
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

BEGIN;
SELECT balance FROM accounts WHERE account_id = 'A001';
-- 이 트랜잭션 동안 다른 트랜잭션의 변경이 보이지 않음
COMMIT;

각 격리 수준의 문제:

  • Dirty Read -- 커밋되지 않은 데이터를 읽는 현상
  • Non-Repeatable Read -- 같은 쿼리를 두 번 실행했을 때 결과가 달라지는 현상
  • Phantom Read -- 같은 조건으로 조회했을 때 새로운 행이 나타나는 현상

데드락 (Deadlock)

두 트랜잭션이 서로 상대방이 보유한 잠금을 기다리며 영원히 대기하는 상태다.

-- 데드락 시나리오
-- 트랜잭션 1: A -> B 순서로 잠금
-- 트랜잭션 2: B -> A 순서로 잠금

-- 해결 방법:
-- 1. 잠금 순서를 통일한다 (항상 ID가 작은 것부터)
-- 2. 타임아웃을 설정한다
-- 3. 트랜잭션 범위를 최소화한다

SET lock_timeout = '5s';

6. 쿼리 최적화

느린 쿼리는 서비스 전체 성능을 저하시킨다. 체계적인 최적화가 필수다.

EXPLAIN ANALYZE

EXPLAIN ANALYZE
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at >= '2026-01-01'
GROUP BY u.name
ORDER BY order_count DESC
LIMIT 10;

실행 계획에서 주요하게 확인할 항목은 다음과 같다.

  • Seq Scan -- 전체 테이블 스캔. 대형 테이블에서 발생하면 인덱스 필요
  • Index Scan -- 인덱스를 사용한 조회. 효율적
  • Index Only Scan -- 인덱스만으로 데이터 반환. 가장 효율적
  • Nested Loop -- 소규모 데이터 JOIN에 적합
  • Hash Join -- 중대규모 데이터 JOIN에 적합
  • Merge Join -- 정렬된 대규모 데이터 JOIN에 적합
  • Actual Time -- 실제 실행 시간
  • Rows -- 예상 행 수 vs 실제 행 수 차이 확인

N+1 문제

N+1 문제는 1번의 쿼리 후 N번의 추가 쿼리가 발생하는 비효율적 패턴이다.

-- N+1 문제 발생
-- 1번: SELECT * FROM users LIMIT 100;
-- 100번: SELECT * FROM orders WHERE user_id = ?;  (각 유저마다)

-- 해결: JOIN으로 한 번에 조회
SELECT u.*, o.*
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id IN (SELECT id FROM users LIMIT 100);

-- 또는 서브쿼리 활용
SELECT u.*, (
    SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id
) AS order_count
FROM users u
LIMIT 100;

쿼리 최적화 체크리스트

  1. 불필요한 SELECT * 대신 필요한 컬럼만 명시한다
  2. WHERE 절에서 함수를 사용하면 인덱스가 무효화될 수 있다
  3. LIKE 검색은 선행 와일드카드를 피한다
  4. 대량 INSERT는 배치로 처리한다
  5. 적절한 인덱스를 생성한다
-- 안 좋은 예: 인덱스 무효화
SELECT * FROM users WHERE YEAR(created_at) = 2026;

-- 좋은 예: 인덱스 활용 가능
SELECT * FROM users
WHERE created_at >= '2026-01-01'
  AND created_at < '2027-01-01';

7. PostgreSQL 심화

PostgreSQL은 확장성과 표준 준수 면에서 가장 강력한 오픈소스 RDBMS다.

JSONB

-- JSONB 컬럼 생성 및 활용
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(200),
    attributes JSONB
);

-- 데이터 삽입
INSERT INTO products (name, attributes)
VALUES ('노트북', '{"brand": "Samsung", "ram": 16, "storage": "512GB", "tags": ["electronics", "portable"]}');

-- JSONB 쿼리
SELECT name, attributes->>'brand' AS brand
FROM products
WHERE attributes->>'ram' = '16';

-- JSONB 포함 연산자
SELECT * FROM products
WHERE attributes @> '{"brand": "Samsung"}';

-- JSONB 배열 조회
SELECT * FROM products
WHERE attributes->'tags' ? 'electronics';

CTE (Common Table Expression)

-- 재귀 CTE: 조직도 탐색
WITH RECURSIVE org_tree AS (
    -- 시작점: 최상위 관리자
    SELECT id, name, manager_id, 1 AS depth
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    -- 재귀: 하위 직원 탐색
    SELECT e.id, e.name, e.manager_id, t.depth + 1
    FROM employees e
    INNER JOIN org_tree t ON e.manager_id = t.id
)
SELECT * FROM org_tree ORDER BY depth, name;

Window Function

-- 부서별 급여 순위
SELECT
    name,
    department,
    salary,
    ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rank_in_dept,
    RANK() OVER (ORDER BY salary DESC) AS overall_rank,
    AVG(salary) OVER (PARTITION BY department) AS dept_avg_salary,
    salary - LAG(salary) OVER (PARTITION BY department ORDER BY salary DESC) AS diff_from_prev
FROM employees;

-- 누적 합계
SELECT
    order_date,
    amount,
    SUM(amount) OVER (ORDER BY order_date ROWS UNBOUNDED PRECEDING) AS running_total
FROM daily_sales;

Partitioning

-- 범위 파티셔닝
CREATE TABLE events (
    id BIGSERIAL,
    event_type VARCHAR(50),
    payload JSONB,
    created_at TIMESTAMP NOT NULL
) PARTITION BY RANGE (created_at);

-- 월별 파티션 생성
CREATE TABLE events_2026_01 PARTITION OF events
    FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');

CREATE TABLE events_2026_02 PARTITION OF events
    FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');

CREATE TABLE events_2026_03 PARTITION OF events
    FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');

-- 파티션별 인덱스 자동 생성
CREATE INDEX idx_events_type ON events(event_type);

8. NoSQL 종류와 선택 기준

Document Store -- MongoDB

문서 지향 데이터베이스로, JSON 형태의 유연한 스키마를 지원한다.

// MongoDB 쿼리 예시
db.users.insertOne({
  name: "홍길동",
  email: "hong@example.com",
  address: {
    city: "서울",
    district: "강남구"
  },
  tags: ["premium", "active"]
});

// 집계 파이프라인
db.orders.aggregate([
  { $match: { status: "completed" } },
  { $group: {
      _id: "$user_id",
      totalSpent: { $sum: "$amount" },
      orderCount: { $sum: 1 }
  }},
  { $sort: { totalSpent: -1 } },
  { $limit: 10 }
]);

적합한 경우: 스키마가 자주 변경되는 서비스, 콘텐츠 관리 시스템, 카탈로그

Key-Value Store -- Redis

인메모리 데이터 저장소로, 초고속 읽기/쓰기를 제공한다. (Redis 상세는 9장에서 다룬다.)

Wide Column Store -- Cassandra

대규모 분산 환경에서 높은 쓰기 처리량을 제공한다.

-- Cassandra CQL
CREATE TABLE sensor_data (
    sensor_id UUID,
    event_time TIMESTAMP,
    temperature DOUBLE,
    humidity DOUBLE,
    PRIMARY KEY (sensor_id, event_time)
) WITH CLUSTERING ORDER BY (event_time DESC);

적합한 경우: IoT 데이터, 시계열 데이터, 대규모 로그 저장

Graph Database -- Neo4j

노드와 관계(Edge)를 기반으로 데이터를 저장하고 탐색한다.

// Neo4j Cypher 쿼리
// 친구의 친구 찾기 (2단계 관계)
MATCH (me:Person)-[:FRIEND]->(friend)-[:FRIEND]->(fof)
WHERE me.name = '홍길동'
  AND NOT (me)-[:FRIEND]->(fof)
  AND fof <> me
RETURN DISTINCT fof.name AS recommendation
LIMIT 10;

적합한 경우: 소셜 네트워크, 추천 시스템, 사기 탐지, 지식 그래프

NoSQL 선택 가이드

요구사항추천 DB
유연한 스키마 + 복잡한 쿼리MongoDB
초고속 캐시/세션Redis
대규모 쓰기 + 고가용성Cassandra
관계 탐색 + 그래프 분석Neo4j
전문 검색Elasticsearch
시계열 데이터TimescaleDB, InfluxDB

9. Redis 활용

Redis는 인메모리 데이터 구조 저장소로, 다양한 자료구조를 지원한다.

캐시 (Cache)

# 캐시 설정 (TTL 3600초)
SET user:1001:profile '{"name":"홍길동","email":"hong@example.com"}' EX 3600

# 캐시 조회
GET user:1001:profile

# 캐시 무효화
DEL user:1001:profile

캐시 전략:

  • Cache-Aside (Lazy Loading) -- 요청 시 캐시를 확인하고, 없으면 DB에서 조회 후 캐시에 저장
  • Write-Through -- 데이터를 DB와 캐시에 동시에 쓴다
  • Write-Behind -- 캐시에 먼저 쓰고, 비동기로 DB에 반영한다

세션 관리

# 세션 저장 (30분 만료)
HSET session:abc123 user_id 1001 role admin login_time "2026-04-12T10:00:00"
EXPIRE session:abc123 1800

# 세션 조회
HGETALL session:abc123

리더보드

# 점수 추가
ZADD game:leaderboard 1500 "player:001"
ZADD game:leaderboard 2300 "player:002"
ZADD game:leaderboard 1800 "player:003"

# 상위 10명 조회 (높은 점수순)
ZREVRANGE game:leaderboard 0 9 WITHSCORES

# 특정 플레이어 순위 조회
ZREVRANK game:leaderboard "player:001"

Pub/Sub

# 채널 구독
SUBSCRIBE notifications:user:1001

# 메시지 발행
PUBLISH notifications:user:1001 '{"type":"order","message":"주문이 완료되었습니다"}'

분산 락 (Distributed Lock)

# 락 획득 (NX: 존재하지 않을 때만, EX: 만료 시간)
SET lock:order:process "worker-1" NX EX 30

# 락 해제 (Lua 스크립트로 원자적 처리)
# 자신이 설정한 락만 해제하도록 값을 확인
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock:order:process "worker-1"

10. 데이터 모델링

ER 다이어그램

Entity-Relationship 다이어그램은 데이터베이스 설계의 시각적 표현이다.

주요 구성 요소는 다음과 같다.

  • 엔티티(Entity) -- 테이블로 변환되는 실세계 객체
  • 속성(Attribute) -- 엔티티의 특성 (컬럼)
  • 관계(Relationship) -- 엔티티 간의 연관

관계 유형

1:1 관계 (One-to-One)

-- 사용자와 프로필: 1:1
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL
);

CREATE TABLE user_profiles (
    user_id INTEGER PRIMARY KEY REFERENCES users(id),
    bio TEXT,
    avatar_url VARCHAR(500)
);

1:N 관계 (One-to-Many)

-- 사용자와 게시글: 1:N
CREATE TABLE posts (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    title VARCHAR(200) NOT NULL,
    content TEXT,
    created_at TIMESTAMP DEFAULT NOW()
);

M:N 관계 (Many-to-Many)

-- 게시글과 태그: M:N (중간 테이블 필요)
CREATE TABLE tags (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50) UNIQUE NOT NULL
);

CREATE TABLE post_tags (
    post_id INTEGER REFERENCES posts(id),
    tag_id INTEGER REFERENCES tags(id),
    PRIMARY KEY (post_id, tag_id)
);

모델링 체크리스트

  1. 비즈니스 요구사항을 먼저 분석한다
  2. 엔티티와 관계를 식별한다
  3. 정규화를 적용한다 (최소 3NF)
  4. 성능 요구사항에 따라 반정규화를 검토한다
  5. 인덱스 전략을 수립한다
  6. 데이터 증가량을 예측한다

11. 마이그레이션

왜 마이그레이션이 필요한가

데이터베이스 스키마는 코드와 함께 변화한다. 마이그레이션 도구는 스키마 변경을 버전 관리하고, 팀 전체가 동일한 스키마를 유지하도록 보장한다.

Flyway (Java/JVM 생태계)

-- V1__create_users_table.sql
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    name VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

-- V2__add_status_to_users.sql
ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'active';
CREATE INDEX idx_users_status ON users(status);

-- V3__create_orders_table.sql
CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT REFERENCES users(id),
    total_amount DECIMAL(10, 2) NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

Flyway 규칙은 다음과 같다.

  • 파일명은 V숫자__설명.sql 형태로 작성한다
  • 한번 적용된 마이그레이션은 수정하지 않는다
  • 새로운 변경은 새 파일로 추가한다

Alembic (Python/SQLAlchemy 생태계)

# alembic/versions/001_create_users.py
from alembic import op
import sqlalchemy as sa

def upgrade():
    op.create_table(
        'users',
        sa.Column('id', sa.Integer(), primary_key=True),
        sa.Column('email', sa.String(255), nullable=False, unique=True),
        sa.Column('name', sa.String(100), nullable=False),
        sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()),
    )

def downgrade():
    op.drop_table('users')

무중단 마이그레이션

프로덕션 환경에서 서비스 중단 없이 스키마를 변경하는 방법이다.

컬럼 추가 (안전)

-- NOT NULL 없이 추가 (기존 행에 영향 없음)
ALTER TABLE users ADD COLUMN phone VARCHAR(20);

컬럼 삭제 (단계적 접근)

1단계 -- 코드에서 해당 컬럼 사용을 제거하고 배포한다.

2단계 -- 충분한 시간이 지난 후 컬럼을 삭제한다.

ALTER TABLE users DROP COLUMN phone;

테이블 이름 변경 (안전한 방법)

1단계 -- 새 테이블을 생성한다.

2단계 -- 양쪽에 모두 쓰도록 코드를 수정한다.

3단계 -- 데이터를 마이그레이션한다.

4단계 -- 새 테이블만 읽도록 전환한다.

5단계 -- 이전 테이블을 삭제한다.

대규모 인덱스 생성

-- CONCURRENTLY 옵션: 테이블 잠금 없이 인덱스 생성
CREATE INDEX CONCURRENTLY idx_orders_user ON orders(user_id);

마이그레이션 도구 비교

도구생태계특징
FlywayJava/JVMSQL 기반, 단순하고 직관적
LiquibaseJava/JVMXML/YAML/JSON/SQL 지원
AlembicPythonSQLAlchemy 통합, 자동 감지
Prisma MigrateNode.jsPrisma ORM 통합
golang-migrateGo경량, CLI 기반
Knex.jsNode.jsJavaScript 마이그레이션

마무리

데이터베이스는 소프트웨어의 심장이다. SQL 기초를 탄탄히 다지고, 정규화와 인덱스 원리를 이해하며, 트랜잭션의 ACID 속성을 체득해야 한다. 그 위에 쿼리 최적화 능력과 적절한 NoSQL 선택 기준을 갖추면, 어떤 규모의 서비스든 견고한 데이터 계층을 설계할 수 있다.

핵심 정리는 다음과 같다.

  • SQL을 깊이 이해하라 -- JOIN, 서브쿼리, Window Function은 필수다
  • 정규화 원칙을 따르되, 성능을 위한 반정규화도 고려하라
  • 인덱스는 읽기 성능의 핵심이지만, 쓰기 비용을 수반한다
  • 트랜잭션 격리 수준은 정합성과 동시성의 트레이드오프다
  • EXPLAIN ANALYZE로 쿼리 성능을 측정하고 개선하라
  • RDBMS와 NoSQL의 장단점을 이해하고 상황에 맞게 선택하라
  • 마이그레이션은 항상 버전 관리하고, 무중단 전략을 수립하라

현재 단락 (1/513)

데이터베이스(Database)는 구조화된 데이터를 저장하고 관리하는 시스템이다. 현대 소프트웨어에서 데이터 저장과 조회는 핵심 기능이며, 올바른 데이터베이스 선택과 설계는 애플리케...

작성 글자: 0원문 글자: 14,553작성 단락: 0/513