Guider/Database/DatabaseDevGuide0006
Database#06

DatabaseDevGuide0006

실전 활용 완전 정복

🗄️ DATABASE DEV GUIDE
DatabaseDevGuide0006
실전 활용 완전 정복 A-Z
백엔드 연결 · ORM · 트랜잭션 심화 · 성능 최적화 · 백업 · 보안
⏱ 예상 학습: 20~30시간  |  📊 난이도: 중급~고급  |  🎯 목표: 현업 즉시 투입

 

📚 DatabaseDevGuide 시리즈 진행 현황
✅ 0001 DB개념 ✅ 0002 SQL기초 ✅ 0003 SQL심화 ✅ 0004 DB설계 ✅ 0005 NoSQL 🔥 0006 실전활용 ← 지금 여기 ⏳ 0007 실전프로젝트
📋 학습 목차 — 이 글 하나로 현업 DB 개발자가 됩니다
챕터 주제 핵심 키워드
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 실전 면접 질문 완전 정리
CHAPTER 01
Node.js + MySQL 백엔드 연결 완전 정복
실제 서비스에서 DB를 어떻게 연결하고 사용하는지 A-Z까지 배웁니다
💡 핵심 개념: Connection Pool이란?

DB 연결은 생성할 때마다 TCP 핸드셰이크 + 인증 과정이 필요해서 평균 5~100ms가 걸립니다. 요청마다 새 연결을 만들면 서버가 뻗습니다! Connection Pool은 미리 연결을 만들어두고 빌려줬다가 반납받는 방식입니다.

📊 Connection Pool 동작 원리
요청 1Pool [연결①] 대출 → DB쿼리 → 반납
요청 2Pool [연결②] 대출 → DB쿼리 → 반납
요청 NPool 가득 참 → 대기열(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 파일 + 환경변수 사용
CHAPTER 02 & 03
ORM 완전 정복 — Sequelize & Prisma
⚖️ Raw SQL vs ORM 비교
비교 항목 Raw SQL (mysql2) ORM (Sequelize/Prisma)
생산성 ⭐⭐ 낮음 ⭐⭐⭐⭐⭐ 높음
성능 ⭐⭐⭐⭐⭐ 최고 ⭐⭐⭐ 중간
DB 이식성 ❌ DB별 문법 다름 ✅ DB 교체 용이
타입 안전성 ❌ 없음 ✅ (특히 Prisma)
// ── 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 } })
]);
CHAPTER 04
트랜잭션 심화 — 격리 수준 · 데드락 · 분산 트랜잭션
🔒 트랜잭션 격리 수준 완전 정리
격리 수준 Dirty Read Non-Repeatable Phantom 성능
READ UNCOMMITTED ⭐⭐⭐⭐⭐
READ COMMITTED ⭐⭐⭐⭐
REPEATABLE READ (MySQL 기본) ⭐⭐⭐
SERIALIZABLE
// ✅ 실전 트랜잭션 — 주문 처리 (재고 차감 + 주문 생성 + 결제)
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();  // 반드시 연결 반납!
  }
}
☠️ 데드락(Deadlock) 방지법
트랜잭션 A: 상품1 락 → 상품2 락 요청 (대기)
트랜잭션 B: 상품2 락 → 상품1 락 요청 (대기) → 데드락!

① 항상 같은 순서로 락 획득 ② 트랜잭션 짧게 유지 ③ 불필요한 FOR UPDATE 금지 ④ 인덱스로 범위 락 최소화 ⑤ 발생 시 재시도 로직 구현

CHAPTER 05
쿼리 성능 최적화 — 느린 쿼리를 100배 빠르게!
🔍 EXPLAIN으로 쿼리 분석하기
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 'DELIVERED';
필드 의미 좋은 값 나쁜 값
type 접근 방식 ref, range ALL (풀스캔!)
key 사용된 인덱스 인덱스 이름 NULL
rows 예상 스캔 행 수 적을수록 좋음 수백만 이상
Extra 추가 정보 Using index Using filesort
-- ✅ 좋은 인덱스 설계
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                      -- ✅
🚨 N+1 문제 — 성능 킬러 #1
❌ N+1 문제 (101번 쿼리)
for (const user of users) {
  // 매번 DB 쿼리!
  user.orders = await findOrders(user.id);
}
✅ Eager Loading (1~2번)
// Sequelize
User.findAll({ include: [Order] });
// Prisma
prisma.user.findMany({
  include: { orders: true }
});
CHAPTER 06
DB 백업 & 복구 완전 정복 — 데이터는 회사의 생명
📦 백업 종류 비교표
종류 설명 장점 단점
논리 백업 mysqldump (SQL 파일) 이식성, 가독성 복구 느림
물리 백업 DB 파일 복사 빠른 복구 같은 버전/OS 필요
Binary Log 변경 이벤트 기록 특정 시점 복구! 스토리지 증가
## 전체 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)"
CHAPTER 07
DB 보안 완전 정복 — 해킹을 막아라!
🛡️ SQL Injection 방어
❌ 취약한 코드
const q = `WHERE id=${userId}`;
// userId = "1 OR 1=1 --"
// → 전체 DB 유출!!
✅ 안전한 코드
pool.execute(
  'WHERE id = ?', [userId]
);
// SQL과 데이터 분리!
-- 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;
✅ DB 보안 체크리스트
☑️ 모든 SQL에 Prepared Statement SQL Injection 방어
☑️ DB 연결 정보 .env 환경변수 하드코딩 절대 금지
☑️ 최소 권한 원칙 (앱 계정 ≠ root) 침해 시 피해 최소화
☑️ 3306 포트 외부 차단 (방화벽) VPN/SSH 터널링 사용
☑️ 패스워드 bcrypt/argon2 해싱 MD5, SHA1 절대 금지!
CHAPTER 08 ~ 11
복제 · 클라우드 · 마이그레이션 · 모니터링
📊 MySQL Master-Slave 복제 아키텍처
애플리케이션 서버
Master DB
쓰기 전용
Slave1
읽기
Slave2
읽기
Slave3
읽기
Master 장애 → Slave가 자동으로 Master 승격 (Failover)
// 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;            -- 오래된 쿼리 종료
📈 Grafana 모니터링 알람 기준
지표 알람 임계값
연결 수 max_connections의 80%
Buffer Pool 히트율 80% 미만
슬로우 쿼리 수 분당 10건 이상
디스크 사용률 80% 이상
CHAPTER 12
현업 면접 Q&A TOP 15
Q1. Connection Pool의 적절한 크기를 어떻게 결정하나요?

공식: (CPU 코어 수 × 2) + 유효 디스크 스핀들 수. 4코어 서버라면 약 10개가 적절합니다. 현업에서는 10~50개로 시작해서 부하 테스트 후 조정합니다.

Q2. ORM N+1 문제가 무엇이고 어떻게 해결하나요?

부모 1개 조회 후 N개의 연관 엔티티를 각각 조회하는 문제입니다. 해결: ① Eager Loading(include/join) ② DataLoader 배치 처리 ③ Prisma는 include 시 자동으로 IN 절 최적화합니다.

Q3. 트랜잭션 격리 수준 중 현업에서 주로 어떤 것을 쓰나요?

MySQL InnoDB 기본값인 REPEATABLE READ를 주로 사용합니다. 금융 결제는 필요 시 FOR UPDATE 행 락으로 대체합니다.

Q4. 데드락이 발생했을 때 어떻게 대응하나요?

MySQL이 자동 롤백 처리합니다. 대응: ① 재시도 로직 구현 ② SHOW ENGINE INNODB STATUS로 원인 분석 ③ 락 순서 통일 ④ 트랜잭션 짧게 유지

Q5. 대용량 테이블 컬럼 추가 시 어떻게 하나요?

MySQL 5.6+ Online DDL 또는 pt-online-schema-change(Percona Toolkit)나 gh-ost(GitHub)를 사용합니다. 새 테이블 만들고 준비되면 원자적으로 교체합니다.

Q6. Read Replica 사용 시 주의사항은?

복제 지연(Replication Lag): Master 쓰기 후 Slave 반영까지 수ms~수초 지연. 방금 쓴 데이터를 바로 읽어야 할 경우 Master에서 읽어야 합니다.

Q7. MVCC(다중 버전 동시성 제어)란?

데이터의 여러 버전을 유지하여 읽기와 쓰기가 서로를 차단하지 않게 하는 기술입니다. 트랜잭션 시작 시점 스냅샷 기준으로 읽기를 수행해 다른 트랜잭션 수정 중에도 블록되지 않습니다.

Q8. 분산 트랜잭션이란?

여러 DB/마이크로서비스에 걸친 트랜잭션입니다. 해결: ① 2PC(Prepare→Commit) ② Saga 패턴(로컬 트랜잭션 + 보상 트랜잭션). Saga가 현업 마이크로서비스 표준입니다.

Q9. DB 성능이 갑자기 느려졌다. 어떻게 디버깅하나요?

① SHOW PROCESSLIST ② Slow Query Log 분석 ③ EXPLAIN으로 풀스캔 확인 ④ SHOW ENGINE INNODB STATUS(데드락) ⑤ CPU/메모리/디스크 I/O ⑥ 최근 배포 코드 확인

Q10. Soft Delete vs Hard Delete

Soft Delete: deleted_at 기록. 감사 로그, 실수 복구에 적합. 모든 쿼리에 WHERE deleted_at IS NULL 필요.
Hard Delete: 실제 삭제. 저장 공간 절약. 임시/캐시 데이터에 적합.

Q11. DB 파티셔닝이란?

큰 테이블을 여러 파티션으로 나누는 기술입니다. RANGE(날짜), HASH(균등 분산), LIST, KEY 파티셔닝이 있습니다. 오래된 파티션을 통째로 삭제 가능합니다.

Q12. Sequelize vs Prisma 선택 기준은?

Prisma: TypeScript, 타입 안전성, Next.js 스택에서 사실상 표준. 자동완성 탁월.
Sequelize: 기존 JavaScript 프로젝트, Raw SQL 혼용 필요 시. 생태계가 더 넓습니다.

Q13. 인덱스는 많이 만들수록 좋은가요?

절대 아닙니다. 인덱스는 읽기는 빠르게, 쓰기는 느리게 만듭니다. WHERE/JOIN/ORDER BY에 자주 쓰이고 카디널리티 높은 컬럼에만 생성합니다.

Q14. 실무 쿼리 튜닝 경험을 이야기해주세요. (예시)

"주문 목록 API가 1초 이상 걸렸습니다. EXPLAIN으로 분석하니 풀스캔(ALL)이었습니다. WHERE 조건에 user_id, status가 있었지만 복합 인덱스가 없었습니다. (user_id, status, created_at) 인덱스 추가 후 응답시간이 50ms로 감소했습니다."

Q15. DB 보안에서 가장 중요한 것 3가지는?

SQL Injection 방어: Prepared Statement 항상 사용
최소 권한 원칙: 앱 계정에 필요한 권한만, root 연결 금지
암호화: bcrypt/argon2 해싱, SSL/TLS, 민감 데이터 AES-256

📖 Guide0006 학습 체크리스트
아래 항목들을 모두 체크하면 현업 투입 준비 완료!
🔌 백엔드 연결

☐ 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 모니터링
☐ 데드락 디버깅

🛠️ 추천 도구 & 학습 환경
도구 용도 비용
DBeaver DB GUI 관리 (MySQL, PostgreSQL 등) 무료
Docker (MySQL) 로컬 MySQL 환경 무료
AWS RDS (Free Tier) 클라우드 DB 실습 (12개월) 무료 Tier
Prisma Studio Prisma GUI 데이터 조회/편집 무료
Percona Toolkit pt-online-schema-change (무중단 DDL) 무료
🎉 DatabaseDevGuide0006 완료!

백엔드 연결 · ORM · 트랜잭션 · 성능 최적화 · 백업 · 보안 · 고가용성 · 클라우드까지
현업 DB 개발자가 되기 위한 모든 내용을 마스터했습니다!

📌 다음 단계: DatabaseDevGuide0007 - 실전 프로젝트
쇼핑몰 + 블로그 서비스를 MySQL + MongoDB + Redis로 전체 설계 및 구현

✅ mysql2 연결 ✅ Sequelize/Prisma ✅ 트랜잭션 심화 ✅ 성능 최적화 ✅ 백업 & 복구 ✅ DB 보안 ✅ 고가용성 ✅ AWS RDS

반응형