| 챕터 | 주제 | 핵심 키워드 |
|---|---|---|
| 01 | Node.js + MySQL 연결 (mysql2) | Connection Pool, 쿼리 실행, Prepared Statement |
| 02 | ORM 완전 정복 (Sequelize) | Model 정의, Association, Migration, Query |
| 03 | ORM 완전 정복 (Prisma) | Schema 정의, CRUD, Relation, Type Safety |
| 04 | 트랜잭션 심화 | 격리 수준, 데드락, 분산 트랜잭션, 2PC |
| 05 | 쿼리 성능 최적화 | EXPLAIN, 인덱스 전략, 슬로우 쿼리, N+1 문제 |
| 06 | DB 백업 & 복구 | mysqldump, Point-in-Time Recovery, RDS 백업 |
| 07 | DB 보안 완전 정복 | SQL Injection, 권한 관리, 암호화, 감사 로그 |
| 08 | DB 복제 & 고가용성 | Master-Slave, Read Replica, Failover |
| 09 | 클라우드 DB 실전 (AWS RDS) | RDS 설정, Parameter Group, 모니터링 |
| 10 | DB 마이그레이션 전략 | Zero-Downtime, Blue-Green, 스키마 변경 |
| 11 | 모니터링 & 알람 | Slow Query Log, Prometheus, Grafana |
| 12 | 현업 면접 Q&A TOP 15 | 실전 면접 질문 완전 정리 |
DB 연결은 생성할 때마다 TCP 핸드셰이크 + 인증 과정이 필요해서 평균 5~100ms가 걸립니다. 요청마다 새 연결을 만들면 서버가 뻗습니다! Connection Pool은 미리 연결을 만들어두고 빌려줬다가 반납받는 방식입니다.
요청 2 → Pool [연결②] 대출 → DB쿼리 → 반납
요청 N → Pool 가득 참 → 대기열(Queue)에서 대기
// npm install mysql2
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: 'localhost',
user: 'app_user', // root 사용 금지!
password: process.env.DB_PASSWORD,
database: 'shopdb',
connectionLimit: 10, // 최대 동시 연결 수
waitForConnections: true,
charset: 'utf8mb4',
timezone: '+09:00'
});
module.exports = pool;
// ✅ Prepared Statement — SQL Injection 방지의 핵심!
const UserModel = {
findAll: async ({ page = 1, limit = 10, search = '' } = {}) => {
const offset = (page - 1) * limit;
const [rows] = await pool.execute(
`SELECT id, username, email FROM users
WHERE username LIKE ? OR email LIKE ?
ORDER BY created_at DESC LIMIT ? OFFSET ?`,
[`%${search}%`, `%${search}%`, limit, offset]
);
return rows;
},
create: async ({ username, email, passwordHash, role = 'user' }) => {
const [result] = await pool.execute(
'INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, ?)',
[username, email, passwordHash, role]
);
return { id: result.insertId, username, email, role };
},
softDelete: async (id) => {
const [result] = await pool.execute(
'UPDATE users SET deleted_at = NOW() WHERE id = ? AND deleted_at IS NULL', [id]
);
return result.affectedRows > 0;
}
};
| ❌ 잘못된 코드 | ✅ 올바른 코드 |
| WHERE id = ${userId} (SQL Injection!) | 'WHERE id = ?' [userId] |
| 코드에 DB 비밀번호 하드코딩 | .env 파일 + 환경변수 사용 |
// ── Sequelize 모델 정의 ──
const { DataTypes, Model } = require('sequelize');
class User extends Model {}
User.init({
username: { type: DataTypes.STRING(50), allowNull: false,
validate: { len: [2, 50] } },
email: { type: DataTypes.STRING, allowNull: false, unique: true,
validate: { isEmail: true } },
role: { type: DataTypes.ENUM('user', 'admin'), defaultValue: 'user' }
}, {
sequelize, modelName: 'User', tableName: 'users',
timestamps: true, // createdAt, updatedAt 자동 관리
paranoid: true, // deletedAt: Soft Delete 지원
underscored: true // camelCase → snake_case 변환
});
User.hasMany(Order, { foreignKey: 'userId', as: 'orders' });
// Sequelize CRUD
const users = await User.findAll({
where: { isActive: true },
include: [{ model: Order, as: 'orders', limit: 5 }],
order: [['createdAt', 'DESC']], limit: 10
});
// ── Prisma Schema (prisma/schema.prisma) ──
model User {
id Int @id @default(autoincrement())
username String @db.VarChar(50)
email String @unique
role Role @default(USER)
orders Order[]
createdAt DateTime @default(now()) @map("created_at")
deletedAt DateTime? @map("deleted_at")
@@map("users")
}
enum Role { USER ADMIN MODERATOR }
// ── Prisma CRUD ──
const prisma = new PrismaClient({ log: ['query', 'slow', 'error'] });
const userWithOrders = await prisma.user.findUnique({
where: { id: userId },
include: { orders: { where: { status: { not: 'CANCELLED' } }, take: 10 } }
});
// 페이지네이션 + 총 개수 (트랜잭션으로 한 번에)
const [users, total] = await prisma.$transaction([
prisma.user.findMany({ where: { deletedAt: null },
skip: (page-1)*limit, take: limit }),
prisma.user.count({ where: { deletedAt: null } })
]);
// ✅ 실전 트랜잭션 — 주문 처리 (재고 차감 + 주문 생성 + 결제)
async function createOrder({ userId, items, paymentInfo }) {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
for (const item of items) {
const [stocks] = await conn.execute(
'SELECT stock FROM products WHERE id = ? FOR UPDATE', [item.productId]);
if (stocks[0].stock < item.quantity) throw new Error('재고 부족');
await conn.execute('UPDATE products SET stock = stock - ? WHERE id = ?',
[item.quantity, item.productId]);
}
const totalPrice = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
const [result] = await conn.execute(
'INSERT INTO orders (user_id, total_price, status) VALUES (?, ?, ?)',
[userId, totalPrice, 'PENDING']);
await conn.commit(); // ✅ 성공 → COMMIT
return { orderId: result.insertId };
} catch (err) {
await conn.rollback(); // ❌ 실패 → ROLLBACK
throw err;
} finally {
conn.release(); // 반드시 연결 반납!
}
}
트랜잭션 B: 상품2 락 → 상품1 락 요청 (대기) → 데드락!
① 항상 같은 순서로 락 획득 ② 트랜잭션 짧게 유지 ③ 불필요한 FOR UPDATE 금지 ④ 인덱스로 범위 락 최소화 ⑤ 발생 시 재시도 로직 구현
-- ✅ 좋은 인덱스 설계
CREATE INDEX idx_users_email ON users(email); -- 카디널리티 높은 컬럼
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at DESC); -- 복합 인덱스
-- 커버링 인덱스 (SELECT 컬럼도 포함 → 테이블 접근 불필요)
CREATE INDEX idx_products_cover ON products(category_id, price, id, name);
-- ❌ 인덱스 무용지물
WHERE DATE(created_at) = '2026-03-27' -- 함수로 감싸기
WHERE created_at >= '2026-03-27' -- ✅ 이렇게 바꿔야
WHERE name LIKE '%김%' -- 앞 와일드카드 ❌
WHERE name LIKE '김%' -- ✅
WHERE user_id = '123' -- 타입 불일치 ❌ (INT인데 문자열)
WHERE user_id = 123 -- ✅
for (const user of users) {
// 매번 DB 쿼리!
user.orders = await findOrders(user.id);
}
// Sequelize
User.findAll({ include: [Order] });
// Prisma
prisma.user.findMany({
include: { orders: true }
});
## 전체 DB 백업 + 압축
mysqldump -u root -p --all-databases --single-transaction | gzip > /backup/all_$(date +%Y%m%d_%H%M%S).sql.gz
## 특정 DB 백업
mysqldump -u root -p shopdb > /backup/shopdb_$(date +%Y%m%d).sql
## 복구
gunzip < /backup/shopdb_20260327.sql.gz | mysql -u root -p shopdb
## Point-in-Time Recovery (특정 시점 복구!)
# 09:00 백업, 10:30에 실수로 삭제 → 10:29:59로 복구
mysql -u root -p shopdb < /backup/shopdb_0900.sql # 백업 복구
mysqlbinlog --start-datetime="2026-03-27 09:00:00" --stop-datetime="2026-03-27 10:29:59" /var/lib/mysql/binlog.000001 | mysql -u root -p shopdb
## 자동 백업 (crontab: 매일 새벽 2시)
# 0 2 * * * /home/ubuntu/backup.sh
#!/bin/bash
BACKUP_DIR="/backup/mysql"
mysqldump -u backup_user -p"${DB_PASSWORD}" --all-databases --single-transaction | gzip > "${BACKUP_DIR}/all_$(date +%Y%m%d).sql.gz"
find ${BACKUP_DIR} -name "*.sql.gz" -mtime +7 -delete # 7일 이상 삭제
echo "✅ 백업 완료: $(date)"
-- DB 권한 관리 — 최소 권한 원칙
CREATE USER 'shopapp'@'10.0.1.%' IDENTIFIED BY 'StrongP@ssw0rd!';
GRANT SELECT, INSERT, UPDATE, DELETE ON shopdb.* TO 'shopapp'@'10.0.1.%';
-- 읽기 전용 계정 (보고서용)
CREATE USER 'report_user'@'10.0.2.%' IDENTIFIED BY '...';
GRANT SELECT ON shopdb.* TO 'report_user'@'10.0.2.%';
SHOW GRANTS FOR 'shopapp'@'10.0.1.%';
FLUSH PRIVILEGES;
쓰기 전용
읽기
읽기
읽기
// Read/Write 분리 구현
const writePool = mysql.createPool({ host: process.env.DB_MASTER_HOST, ... });
const readPools = ['slave1', 'slave2', 'slave3'].map(h => mysql.createPool({ host: h, ... }));
let idx = 0;
const getReadPool = () => readPools[idx++ % readPools.length];
await writePool.execute('INSERT INTO orders ...', []); // 쓰기 → Master
const [rows] = await getReadPool().execute('SELECT ...'); // 읽기 → Slave 분산
// ── AWS RDS 연결 설정 ──
const pool = mysql.createPool({
host: process.env.DB_HOST, // xxx.rds.amazonaws.com
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
ssl: { ca: fs.readFileSync('./certs/rds-ca.pem') } // SSL 필수!
});
// ── Zero-Downtime 마이그레이션 ──
-- ❌ ALTER TABLE users ADD COLUMN phone VARCHAR(20) NOT NULL; (수십 분 잠금!)
-- ✅ 4단계 무중단 방식:
-- 1. nullable 컬럼 추가 (잠금 최소화)
ALTER TABLE users ADD COLUMN phone VARCHAR(20) NULL;
-- 2. 배치로 기존 데이터 채우기
UPDATE users SET phone = '' WHERE phone IS NULL LIMIT 10000;
-- 3. NOT NULL 제약 추가
ALTER TABLE users MODIFY COLUMN phone VARCHAR(20) NOT NULL DEFAULT '';
-- ── Slow Query Log 설정 ──
slow_query_log = 1 -- 활성화
long_query_time = 1 -- 1초 이상 기록
log_queries_not_using_indexes = 1
SHOW FULL PROCESSLIST; -- 실행 중인 쿼리 확인
KILL QUERY 1234; -- 오래된 쿼리 종료
공식: (CPU 코어 수 × 2) + 유효 디스크 스핀들 수. 4코어 서버라면 약 10개가 적절합니다. 현업에서는 10~50개로 시작해서 부하 테스트 후 조정합니다.
부모 1개 조회 후 N개의 연관 엔티티를 각각 조회하는 문제입니다. 해결: ① Eager Loading(include/join) ② DataLoader 배치 처리 ③ Prisma는 include 시 자동으로 IN 절 최적화합니다.
MySQL InnoDB 기본값인 REPEATABLE READ를 주로 사용합니다. 금융 결제는 필요 시 FOR UPDATE 행 락으로 대체합니다.
MySQL이 자동 롤백 처리합니다. 대응: ① 재시도 로직 구현 ② SHOW ENGINE INNODB STATUS로 원인 분석 ③ 락 순서 통일 ④ 트랜잭션 짧게 유지
MySQL 5.6+ Online DDL 또는 pt-online-schema-change(Percona Toolkit)나 gh-ost(GitHub)를 사용합니다. 새 테이블 만들고 준비되면 원자적으로 교체합니다.
복제 지연(Replication Lag): Master 쓰기 후 Slave 반영까지 수ms~수초 지연. 방금 쓴 데이터를 바로 읽어야 할 경우 Master에서 읽어야 합니다.
데이터의 여러 버전을 유지하여 읽기와 쓰기가 서로를 차단하지 않게 하는 기술입니다. 트랜잭션 시작 시점 스냅샷 기준으로 읽기를 수행해 다른 트랜잭션 수정 중에도 블록되지 않습니다.
여러 DB/마이크로서비스에 걸친 트랜잭션입니다. 해결: ① 2PC(Prepare→Commit) ② Saga 패턴(로컬 트랜잭션 + 보상 트랜잭션). Saga가 현업 마이크로서비스 표준입니다.
① SHOW PROCESSLIST ② Slow Query Log 분석 ③ EXPLAIN으로 풀스캔 확인 ④ SHOW ENGINE INNODB STATUS(데드락) ⑤ CPU/메모리/디스크 I/O ⑥ 최근 배포 코드 확인
Soft Delete: deleted_at 기록. 감사 로그, 실수 복구에 적합. 모든 쿼리에 WHERE deleted_at IS NULL 필요.
Hard Delete: 실제 삭제. 저장 공간 절약. 임시/캐시 데이터에 적합.
큰 테이블을 여러 파티션으로 나누는 기술입니다. RANGE(날짜), HASH(균등 분산), LIST, KEY 파티셔닝이 있습니다. 오래된 파티션을 통째로 삭제 가능합니다.
Prisma: TypeScript, 타입 안전성, Next.js 스택에서 사실상 표준. 자동완성 탁월.
Sequelize: 기존 JavaScript 프로젝트, Raw SQL 혼용 필요 시. 생태계가 더 넓습니다.
절대 아닙니다. 인덱스는 읽기는 빠르게, 쓰기는 느리게 만듭니다. WHERE/JOIN/ORDER BY에 자주 쓰이고 카디널리티 높은 컬럼에만 생성합니다.
"주문 목록 API가 1초 이상 걸렸습니다. EXPLAIN으로 분석하니 풀스캔(ALL)이었습니다. WHERE 조건에 user_id, status가 있었지만 복합 인덱스가 없었습니다. (user_id, status, created_at) 인덱스 추가 후 응답시간이 50ms로 감소했습니다."
① SQL Injection 방어: Prepared Statement 항상 사용
② 최소 권한 원칙: 앱 계정에 필요한 권한만, root 연결 금지
③ 암호화: bcrypt/argon2 해싱, SSL/TLS, 민감 데이터 AES-256
☐ mysql2 Connection Pool
☐ Prepared Statement
☐ 환경변수 DB 설정
☐ Sequelize 모델
☐ Prisma 스키마
☐ EXPLAIN 쿼리 분석
☐ 복합 인덱스 설계
☐ N+1 문제 해결
☐ Slow Query Log
☐ 커버링 인덱스
☐ SQL Injection 방어
☐ 최소 권한 계정
☐ mysqldump 자동화
☐ PITR 복구
☐ SSL/TLS 연결
☐ Read/Write 분리
☐ AWS RDS 연결
☐ Zero-Downtime DDL
☐ Grafana 모니터링
☐ 데드락 디버깅
백엔드 연결 · ORM · 트랜잭션 · 성능 최적화 · 백업 · 보안 · 고가용성 · 클라우드까지
현업 DB 개발자가 되기 위한 모든 내용을 마스터했습니다!
✅ mysql2 연결 ✅ Sequelize/Prisma ✅ 트랜잭션 심화 ✅ 성능 최적화 ✅ 백업 & 복구 ✅ DB 보안 ✅ 고가용성 ✅ AWS RDS