Guider/Backend/BackendDevGuide0003
Backend#03

BackendDevGuide0003

데이터베이스 완전 정복

Backend Developer Guide Series

BackendDevGuide0003

데이터베이스 완전 정복 A-Z

비전공자도 현업에서 바로 쓸 수 있는 DB 완전 가이드 — SQL부터 NoSQL, ORM, 쿼리 최적화까지

예상 학습시간
35~45시간
난이도
입문 ~ 중급
선수 지식
Express.js 기초
목표
현업 투입 가능

📋 학습 목차

01. 데이터베이스란 무엇인가?
02. SQL vs NoSQL 완전 비교
03. PostgreSQL 완전 정복
04. SQL 문법 완전 마스터
05. 데이터 모델링 & 정규화
06. Prisma ORM 완전 정복
07. MongoDB & Mongoose
08. 인덱스 & 쿼리 최적화
09. 트랜잭션 & ACID
10. Redis 캐싱 전략
11. 데이터베이스 보안
12. 실전 프로젝트 & 면접 Q&A

01 데이터베이스란 무엇인가?

💡 핵심 한 줄 요약: 데이터베이스(DB)는 데이터를 체계적으로 저장하고 빠르게 검색·수정·삭제할 수 있는 전자 창고입니다.

🏪 마트 비유로 이해하기

🏪
데이터베이스
마트 전체 — 모든 물건을 체계적으로 보관
📦
테이블(Table)
진열대 — 같은 종류의 물건 모음 (users, posts 등)
🏷️
레코드(Row)
물건 하나 — 실제 데이터 한 줄

📊 데이터베이스 종류 한눈에 보기

종류 대표 DB 특징 사용 상황
관계형 (RDBMS) PostgreSQL, MySQL, SQLite 표(테이블) 형태, SQL 사용, 데이터 일관성 강함 회원정보, 주문, 결제 등 구조화된 데이터
문서형 (Document) MongoDB, CouchDB JSON 형태 저장, 스키마 유연, 수평 확장 쉬움 콘텐츠, 카탈로그, 비정형 데이터
키-값 (Key-Value) Redis, DynamoDB 초고속 읽기/쓰기, 인메모리 가능 캐싱, 세션, 실시간 리더보드
그래프 (Graph) Neo4j, Amazon Neptune 관계를 노드/엣지로 표현 SNS 친구 관계, 추천 엔진
검색엔진 Elasticsearch, Algolia 전문 검색, 역색인 구조 로그 분석, 풀텍스트 검색

02 SQL vs NoSQL 완전 비교

비교 항목 SQL (관계형) NoSQL (비관계형)
데이터 구조 행/열 테이블 (고정 스키마) 문서/키값/그래프 (유연한 스키마)
확장 방식 수직 확장 (서버 업그레이드) 수평 확장 (서버 추가)
ACID 트랜잭션 ✅ 완벽 지원 ⚠️ 제한적 지원
관계(Join) ✅ 강력한 JOIN 지원 ❌ 복잡한 관계 비효율
읽기/쓰기 속도 복잡한 쿼리에 강함 단순한 대량 쓰기에 강함
선택 기준 금융, 쇼핑몰, 회원 시스템 SNS, 로그, 실시간 데이터
💡 현업에서는? 대부분의 백엔드 서비스는 PostgreSQL (메인 DB) + Redis (캐시) 조합을 사용합니다. 특정 기능에 MongoDB를 추가로 사용하기도 합니다. 처음엔 PostgreSQL 하나만 완벽히 마스터하세요!

03 PostgreSQL 완전 정복

📌 왜 PostgreSQL인가? 오픈소스 관계형 DB 중 기능이 가장 강력합니다. JSON 지원, 배열 타입, 전문 검색, 윈도우 함수 등 기업급 기능을 무료로 사용할 수 있습니다. Supabase, Railway 등 클라우드 플랫폼에서 기본으로 제공합니다.

🔧 설치 및 초기 설정

# macOS (Homebrew)
brew install postgresql@16
brew services start postgresql@16

# Ubuntu/Debian
sudo apt install postgresql postgresql-contrib
sudo systemctl start postgresql

# 접속 (psql CLI)
psql -U postgres

# 데이터베이스 생성 및 사용자 생성
CREATE DATABASE myapp_db;
CREATE USER myapp_user WITH PASSWORD 'strongpassword123';
GRANT ALL PRIVILEGES ON DATABASE myapp_db TO myapp_user;

# Node.js에서 연결 (.env)
DATABASE_URL="postgresql://myapp_user:strongpassword123@localhost:5432/myapp_db"

📊 PostgreSQL 주요 데이터 타입

카테고리 타입 예시 용도
숫자형 INTEGER 42 일반 정수 (ID 등)
BIGINT 9223372036854775807 큰 숫자 (조회수 등)
DECIMAL(10,2) 9999.99 금액, 소수점 (정밀도 필요시)
문자형 VARCHAR(n) '홍길동' 가변 길이 문자열
TEXT '긴 글 내용...' 게시글 본문, 제한 없음
CHAR(n) 'M ' 고정 길이 (국가코드 등)
날짜/시간 TIMESTAMP '2026-03-25 14:30:00' 생성일, 수정일
DATE '2026-03-25' 생일, 예약일
기타 BOOLEAN, JSON, UUID, ARRAY true/false, {"a":1}, uuid, {1,2,3} 상태, 설정값, 고유 식별자

04 SQL 문법 완전 마스터

📌 SQL이란? Structured Query Language. 데이터베이스에 명령을 내리는 언어입니다. 영어 문장처럼 읽히기 때문에 배우기 쉽습니다. "users 테이블에서 나이가 20 이상인 사람들을 나이 순으로 10명 가져와" = SELECT * FROM users WHERE age >= 20 ORDER BY age LIMIT 10;

📝 DDL — 테이블 생성/수정/삭제

-- 테이블 생성
CREATE TABLE users (
  id         SERIAL PRIMARY KEY,          -- 자동 증가 기본키
  name       VARCHAR(50) NOT NULL,
  email      VARCHAR(100) UNIQUE NOT NULL,
  password   VARCHAR(255) NOT NULL,
  role       VARCHAR(20) DEFAULT 'USER',
  is_active  BOOLEAN DEFAULT TRUE,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- 외래키 연결 (다른 테이블 참조)
CREATE TABLE posts (
  id         SERIAL PRIMARY KEY,
  title      VARCHAR(200) NOT NULL,
  content    TEXT,
  user_id    INTEGER REFERENCES users(id) ON DELETE CASCADE,
  view_count INTEGER DEFAULT 0,
  created_at TIMESTAMP DEFAULT NOW()
);

-- 컬럼 추가
ALTER TABLE users ADD COLUMN phone VARCHAR(20);

-- 테이블 삭제
DROP TABLE posts;  -- ⚠️ 되돌릴 수 없음! 신중하게!

🔍 DML — 데이터 조회/삽입/수정/삭제 (CRUD)

-- ====== INSERT (생성) ======
INSERT INTO users (name, email, password, role)
VALUES ('홍길동', 'hong@example.com', 'hashed_password', 'USER')
RETURNING id, name, email;  -- 삽입된 데이터 즉시 반환

-- ====== SELECT (조회) ======
-- 기본 조회
SELECT id, name, email FROM users WHERE is_active = TRUE;

-- 페이지네이션 (현업에서 가장 많이 쓰는 패턴)
SELECT id, name, email, created_at
FROM users
WHERE is_active = TRUE
  AND name ILIKE '%홍%'   -- 대소문자 무시 검색
ORDER BY created_at DESC
LIMIT 10 OFFSET 20;         -- 3페이지 (21~30번째)

-- 집계 함수
SELECT
  COUNT(*) AS total_users,
  COUNT(CASE WHEN role = 'ADMIN' THEN 1 END) AS admin_count,
  AVG(age) AS avg_age,
  MAX(created_at) AS last_joined
FROM users;

-- ====== UPDATE (수정) ======
UPDATE users
SET name = '김길동', updated_at = NOW()
WHERE id = 1
RETURNING *;

-- ====== DELETE (삭제) ======
DELETE FROM users WHERE id = 1;
-- ⚠️ WHERE 절 없으면 전체 삭제! 반드시 확인!

🔗 JOIN — 테이블 연결하기 (현업 핵심!)

-- INNER JOIN: 양쪽 테이블 모두 데이터 있을 때만 반환
SELECT
  p.id, p.title, p.created_at,
  u.name AS author_name,
  u.email AS author_email
FROM posts p
INNER JOIN users u ON p.user_id = u.id
WHERE p.created_at > '2026-01-01'
ORDER BY p.created_at DESC;

-- LEFT JOIN: 왼쪽 테이블은 모두 반환 (오른쪽은 없으면 NULL)
SELECT
  u.name,
  COUNT(p.id) AS post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.id, u.name
ORDER BY post_count DESC;

-- 3개 테이블 JOIN (게시글 + 작성자 + 댓글 수)
SELECT
  p.id, p.title,
  u.name AS author,
  COUNT(c.id) AS comment_count,
  COUNT(l.user_id) AS like_count
FROM posts p
INNER JOIN users u ON p.user_id = u.id
LEFT JOIN comments c ON p.id = c.post_id
LEFT JOIN likes l ON p.id = l.post_id
GROUP BY p.id, u.name
ORDER BY like_count DESC
LIMIT 10;

📊 JOIN 종류 시각적 이해

JOIN 종류 반환 데이터 사용 예시
INNER JOIN 두 테이블 모두 일치하는 행만 게시글 + 작성자 (작성자 없는 게시글 제외)
LEFT JOIN 왼쪽 모두 + 오른쪽 일치하는 것 모든 사용자 + 게시글 (글 없는 사용자도 포함)
RIGHT JOIN 오른쪽 모두 + 왼쪽 일치하는 것 LEFT JOIN 방향 반대 (거의 안 씀)
FULL OUTER JOIN 양쪽 모든 행 (없으면 NULL) 데이터 비교/감사에 사용

05 데이터 모델링 & 정규화

📌 데이터 모델링이란? 실제 세계의 정보를 데이터베이스 테이블로 어떻게 표현할지 설계하는 작업입니다. 잘못 설계하면 나중에 엄청난 비용이 발생하므로 처음부터 잘 설계해야 합니다.

🔗 테이블 관계 3가지

1:1 (One-to-One)
사용자 ↔ 사용자 프로필
한 사용자는 프로필 하나만 가짐
users (id)
  ↕
profiles (user_id UNIQUE)
1:N (One-to-Many)
사용자 ↔ 게시글
한 사용자가 여러 글 작성 가능
users (id)
  ↕
posts (user_id FK)
N:M (Many-to-Many)
사용자 ↔ 태그
중간 테이블(Junction)로 연결
posts ↔ post_tags ↔ tags

📐 정규화 — 데이터 중복 제거

단계 내용 위반 예시 해결
1NF 각 컬럼은 원자값(더 이상 나눌 수 없는 값)만 tags: "nodejs,express,js" 별도 tags 테이블로 분리
2NF 모든 컬럼이 기본키 전체에 의존해야 함 주문 테이블에 상품명 중복 저장 상품 테이블 분리
3NF 비기본키 컬럼이 다른 비기본키에 의존하면 안 됨 우편번호, 도시명 같이 저장 주소 테이블 분리

🏗️ 실전 블로그 DB 설계

-- 사용자 테이블
CREATE TABLE users (
  id         SERIAL PRIMARY KEY,
  name       VARCHAR(50) NOT NULL,
  email      VARCHAR(100) UNIQUE NOT NULL,
  password   VARCHAR(255) NOT NULL,
  role       VARCHAR(20) DEFAULT 'USER' CHECK (role IN ('USER', 'ADMIN')),
  created_at TIMESTAMP DEFAULT NOW()
);

-- 게시글 테이블
CREATE TABLE posts (
  id         SERIAL PRIMARY KEY,
  title      VARCHAR(200) NOT NULL,
  content    TEXT NOT NULL,
  published  BOOLEAN DEFAULT FALSE,
  view_count INTEGER DEFAULT 0,
  user_id    INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- 댓글 테이블
CREATE TABLE comments (
  id         SERIAL PRIMARY KEY,
  content    TEXT NOT NULL,
  post_id    INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
  user_id    INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  created_at TIMESTAMP DEFAULT NOW()
);

-- 좋아요 테이블 (N:M 복합키)
CREATE TABLE likes (
  user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
  post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE,
  PRIMARY KEY (user_id, post_id)  -- 중복 좋아요 방지!
);

-- 태그 (N:M 관계)
CREATE TABLE tags (
  id   SERIAL PRIMARY KEY,
  name VARCHAR(50) UNIQUE NOT NULL
);

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

06 Prisma ORM 완전 정복

📌 ORM이란? Object-Relational Mapping. SQL을 직접 쓰지 않고 JavaScript 코드로 DB를 조작할 수 있게 해주는 도구입니다. Prisma는 현재 Node.js 생태계에서 가장 인기있는 ORM으로, 타입 안전성과 자동완성을 제공합니다.

🔧 Prisma 설치 및 초기 설정

# 1. Prisma 설치
npm install prisma @prisma/client
npx prisma init   # prisma/ 폴더 + .env 생성

# 2. .env 설정
DATABASE_URL="postgresql://myapp_user:password@localhost:5432/myapp_db"

# 3. 스키마 작성 후 마이그레이션
npx prisma migrate dev --name init    # 개발 환경
npx prisma migrate deploy             # 프로덕션 환경

# 4. Prisma Studio (GUI DB 탐색기)
npx prisma studio   # localhost:5555 에서 DB 조회 가능!

# 5. Prisma Client 재생성 (스키마 변경 후)
npx prisma generate

📝 Prisma 스키마 완전 예시

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"  // "mysql" | "sqlite" | "mongodb"
  url      = env("DATABASE_URL")
}

model User {
  id        Int       @id @default(autoincrement())
  name      String    @db.VarChar(50)
  email     String    @unique @db.VarChar(100)
  password  String
  role      Role      @default(USER)
  isActive  Boolean   @default(true) @map("is_active")
  posts     Post[]
  comments  Comment[]
  likes     Like[]
  createdAt DateTime  @default(now()) @map("created_at")
  updatedAt DateTime  @updatedAt @map("updated_at")

  @@map("users")   // DB 테이블명 매핑
}

model Post {
  id         Int       @id @default(autoincrement())
  title      String    @db.VarChar(200)
  content    String    @db.Text
  published  Boolean   @default(false)
  viewCount  Int       @default(0) @map("view_count")
  author     User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId     Int       @map("user_id")
  comments   Comment[]
  likes      Like[]
  tags       PostTag[]
  createdAt  DateTime  @default(now()) @map("created_at")
  updatedAt  DateTime  @updatedAt @map("updated_at")

  @@map("posts")
}

model Comment {
  id        Int      @id @default(autoincrement())
  content   String
  post      Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
  postId    Int      @map("post_id")
  author    User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId    Int      @map("user_id")
  createdAt DateTime @default(now()) @map("created_at")

  @@map("comments")
}

model Like {
  user   User @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId Int  @map("user_id")
  post   Post @relation(fields: [postId], references: [id], onDelete: Cascade)
  postId Int  @map("post_id")

  @@id([userId, postId])   // 복합 기본키 (중복 좋아요 방지)
  @@map("likes")
}

enum Role {
  USER
  ADMIN
}

💻 Prisma CRUD 실전 코드

// db/prisma.js — 싱글톤 클라이언트 (중요!)
const { PrismaClient } = require('@prisma/client');

const globalForPrisma = global;
const prisma = globalForPrisma.prisma ??
  new PrismaClient({ log: ['query', 'error'] });

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma;  // 개발 시 핫리로드로 연결 폭발 방지
}

module.exports = prisma;

// ====== 실전 CRUD 패턴 ======

// 1. 생성 (Create)
const user = await prisma.user.create({
  data: { name: '홍길동', email: 'hong@e.com', password: hashedPw },
  select: { id: true, name: true, email: true },  // 비밀번호 제외!
});

// 2. 조회 (Read) — 페이지네이션 + 검색
const [posts, total] = await Promise.all([
  prisma.post.findMany({
    where: {
      published: true,
      title: { contains: search, mode: 'insensitive' },  // 대소문자 무시
    },
    include: {
      author: { select: { name: true, email: true } },
      _count: { select: { comments: true, likes: true } },  // 댓글·좋아요 수
    },
    orderBy: { createdAt: 'desc' },
    skip: (page - 1) * limit,
    take: limit,
  }),
  prisma.post.count({ where: { published: true } }),
]);

// 3. 수정 (Update)
const updated = await prisma.post.update({
  where: { id: postId },
  data: { title, content, updatedAt: new Date() },
});

// 4. 삭제 (Delete)
await prisma.post.delete({ where: { id: postId } });

// 5. upsert (있으면 수정, 없으면 생성)
const like = await prisma.like.upsert({
  where: { userId_postId: { userId, postId } },
  create: { userId, postId },
  update: {},  // 이미 있으면 아무 것도 안 함
});

07 MongoDB & Mongoose

📌 MongoDB란? JSON과 유사한 BSON 형식으로 데이터를 저장하는 NoSQL 데이터베이스입니다. 스키마가 유연하여 빠르게 프로토타입을 만들거나 데이터 구조가 자주 바뀌는 서비스에 적합합니다.

📊 PostgreSQL vs MongoDB 언어 비교

개념 PostgreSQL (SQL) MongoDB (NoSQL)
저장 단위 Table (테이블) Collection (컬렉션)
데이터 단위 Row (행) Document (문서)
컬럼 단위 Column (열) Field (필드)
기본키 id (SERIAL) _id (ObjectId 자동)
조회 SELECT * FROM users db.users.find({})
스키마 고정 스키마 (엄격) 유연한 스키마 (자유)

🔧 Mongoose 설치 및 연결

npm install mongoose

// db/mongoose.js — MongoDB 연결
const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    await mongoose.connect(process.env.MONGODB_URI, {
      dbName: 'myapp',
    });
    console.log('✅ MongoDB 연결 성공!');
  } catch (err) {
    console.error('❌ MongoDB 연결 실패:', err);
    process.exit(1);
  }
};

module.exports = connectDB;

// .env
MONGODB_URI="mongodb://localhost:27017"
# 또는 MongoDB Atlas (클라우드)
MONGODB_URI="mongodb+srv://user:pass@cluster.mongodb.net/myapp"

📝 Mongoose 스키마 & 모델

// models/User.js
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true, '이름은 필수입니다'],
    trim: true,
    maxlength: 50,
  },
  email: {
    type: String,
    required: [true, '이메일은 필수입니다'],
    unique: true,
    lowercase: true,    // 자동으로 소문자 변환
  },
  password: { type: String, required: true, select: false },  // 기본 조회 제외!
  role: {
    type: String,
    enum: ['USER', 'ADMIN'],
    default: 'USER',
  },
  isActive: { type: Boolean, default: true },
}, {
  timestamps: true,         // createdAt, updatedAt 자동 생성
  toJSON: { virtuals: true },
});

// 가상 필드 (DB에 저장 안 됨)
userSchema.virtual('postCount', {
  ref: 'Post',
  localField: '_id',
  foreignField: 'author',
  count: true,
});

module.exports = mongoose.model('User', userSchema);

// models/Post.js
const postSchema = new mongoose.Schema({
  title:     { type: String, required: true, maxlength: 200 },
  content:   { type: String, required: true },
  published: { type: Boolean, default: false },
  viewCount: { type: Number, default: 0 },
  author:    { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  tags:      [{ type: String, trim: true }],  // 배열로 태그 저장
  likes:     [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
}, { timestamps: true });

module.exports = mongoose.model('Post', postSchema);

// ====== Mongoose CRUD ======
const Post = require('./models/Post');

// 생성
const post = await Post.create({ title, content, author: userId });

// 조회 (작성자 정보 포함)
const posts = await Post.find({ published: true })
  .populate('author', 'name email')  // JOIN처럼 author 정보 채우기
  .sort({ createdAt: -1 })
  .limit(10)
  .skip((page - 1) * 10);

// 수정
await Post.findByIdAndUpdate(postId, { title, content }, { new: true });

// 삭제
await Post.findByIdAndDelete(postId);

08 인덱스 & 쿼리 최적화

📌 인덱스란? 책의 목차와 같습니다. 인덱스 없이 검색하면 DB는 모든 행을 처음부터 끝까지 스캔(Full Table Scan)합니다. 인덱스가 있으면 해당 위치로 바로 점프! 수백만 건 데이터에서 검색 속도가 수백~수천 배 차이납니다.

🚀 인덱스 생성 및 관리

-- 단일 컬럼 인덱스 (WHERE email = ? 같은 조건에 사용)
CREATE INDEX idx_users_email ON users(email);

-- 복합 인덱스 (자주 함께 검색하는 컬럼)
-- 예: WHERE user_id = ? AND published = TRUE
CREATE INDEX idx_posts_user_published ON posts(user_id, published);

-- 유니크 인덱스 (중복 방지 + 빠른 검색)
CREATE UNIQUE INDEX idx_users_email_unique ON users(email);

-- 부분 인덱스 (특정 조건의 데이터만 인덱싱)
CREATE INDEX idx_posts_published ON posts(created_at)
  WHERE published = TRUE;  -- 공개 게시글만 인덱싱

-- 인덱스 목록 확인
SELECT * FROM pg_indexes WHERE tablename = 'posts';

-- 쿼리 실행 계획 확인 (느린 쿼리 분석 필수!)
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = 'hong@example.com';
-- "Index Scan" → 인덱스 사용 중 (빠름!)
-- "Seq Scan" → 전체 테이블 스캔 (느림, 인덱스 추가 필요!)

📊 인덱스 사용 가이드라인

인덱스 추가해야 할 때 인덱스 추가하지 말아야 할 때
WHERE 절에 자주 등장하는 컬럼 데이터 수정(INSERT/UPDATE)이 매우 빈번한 컬럼
JOIN 연결 컬럼 (외래키) 데이터 종류가 적은 컬럼 (is_active: true/false)
ORDER BY 절에 자주 사용되는 컬럼 테이블 데이터가 매우 적은 경우 (100행 이하)
UNIQUE 제약 조건 컬럼 (email, username) 인덱스가 너무 많으면 쓰기 성능 저하

⚡ 쿼리 최적화 핵심 팁

-- ❌ 나쁜 예: SELECT * (필요 없는 컬럼까지 모두 가져옴)
SELECT * FROM users;

-- ✅ 좋은 예: 필요한 컬럼만 명시
SELECT id, name, email FROM users;

-- ❌ 나쁜 예: 인덱스 컬럼에 함수 사용 (인덱스 무력화!)
SELECT * FROM users WHERE LOWER(email) = 'hong@e.com';

-- ✅ 좋은 예: 저장 시 소문자로 저장하거나 citext 타입 사용
SELECT * FROM users WHERE email = 'hong@e.com';

-- ❌ 나쁜 예: LIKE %검색어% (앞에 %가 있으면 인덱스 무력화)
SELECT * FROM posts WHERE title LIKE '%Node%';

-- ✅ 좋은 예: 전문 검색 인덱스 사용 (PostgreSQL)
CREATE INDEX idx_posts_title_fts ON posts USING gin(to_tsvector('english', title));
SELECT * FROM posts WHERE to_tsvector('english', title) @@ to_tsquery('Node');

-- Prisma에서 N+1 방지 (가장 흔한 실수!)
// ❌ N+1 문제
const posts = await prisma.post.findMany();
for (const post of posts) {
  post.author = await prisma.user.findUnique({ where: { id: post.userId } });
  // 게시글 100개 → DB 쿼리 101번!
}

// ✅ include로 한 번에
const posts = await prisma.post.findMany({
  include: { author: { select: { name: true } } }
  // DB 쿼리 1번으로 완료!
});

09 트랜잭션 & ACID

📌 트랜잭션이란? "모두 성공하거나 모두 실패해야 하는" 작업 단위입니다. 은행 송금을 예로 들면: A 계좌에서 -10만원, B 계좌에 +10만원이 반드시 같이 되어야 합니다. 중간에 실패하면 둘 다 취소(롤백)해야 돈이 사라지지 않습니다!

🏦 ACID 4가지 특성

A — Atomicity (원자성)
트랜잭션의 모든 연산은 완전히 수행되거나, 전혀 수행되지 않아야 합니다. (All or Nothing)
C — Consistency (일관성)
트랜잭션 전후로 데이터베이스의 무결성 제약 조건이 항상 만족되어야 합니다.
I — Isolation (격리성)
동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않아야 합니다.
D — Durability (지속성)
커밋된 트랜잭션은 시스템 장애가 발생해도 영구적으로 반영되어야 합니다.

💻 트랜잭션 실전 코드

// SQL 직접 트랜잭션
BEGIN;
  UPDATE accounts SET balance = balance - 100000 WHERE id = 1;
  UPDATE accounts SET balance = balance + 100000 WHERE id = 2;
COMMIT;  -- 성공 시 반영
-- ROLLBACK;  -- 실패 시 취소

// Prisma 트랜잭션 — 방법 1: $transaction 배열
const [debit, credit] = await prisma.$transaction([
  prisma.account.update({
    where: { id: fromAccountId },
    data: { balance: { decrement: amount } },
  }),
  prisma.account.update({
    where: { id: toAccountId },
    data: { balance: { increment: amount } },
  }),
]);
// 둘 중 하나라도 실패하면 둘 다 롤백!

// Prisma 트랜잭션 — 방법 2: interactive (복잡한 로직에 사용)
const result = await prisma.$transaction(async (tx) => {
  // tx는 트랜잭션 범위 내의 Prisma 클라이언트
  const sender = await tx.account.findUnique({ where: { id: fromAccountId } });
  
  if (sender.balance < amount) {
    throw new Error('잔액 부족');  // 에러 발생 시 자동 롤백!
  }

  await tx.account.update({
    where: { id: fromAccountId },
    data: { balance: { decrement: amount } },
  });

  await tx.account.update({
    where: { id: toAccountId },
    data: { balance: { increment: amount } },
  });

  await tx.transferLog.create({
    data: { fromAccountId, toAccountId, amount },
  });

  return { success: true, message: '송금 완료' };
});

10 Redis 캐싱 전략

📌 Redis란? Remote Dictionary Server. 메모리 기반 Key-Value 데이터베이스입니다. 디스크 기반 DB(PostgreSQL)보다 100~1000배 빠릅니다. 자주 읽는 데이터를 Redis에 캐시해두면 DB 부하를 크게 줄일 수 있습니다.

📦 Redis 주요 사용 사례

캐싱
자주 조회하는 데이터 임시 저장
(게시글 목록, 사용자 정보)
🔐
세션/토큰 저장
JWT Refresh Token,
이메일 인증 코드
🏆
실시간 기능
리더보드, 실시간 카운터,
Rate Limiting

🔧 Redis 설치 및 Node.js 연결

# Redis 설치
brew install redis           # macOS
brew services start redis    # 서비스 시작

# 또는 Docker
docker run -d -p 6379:6379 redis:alpine

# Node.js 패키지 설치
npm install ioredis

// db/redis.js — Redis 클라이언트 설정
const Redis = require('ioredis');

const redis = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: process.env.REDIS_PORT || 6379,
  password: process.env.REDIS_PASSWORD,
  lazyConnect: true,
  retryStrategy: (times) => Math.min(times * 50, 2000),  // 재연결 전략
});

redis.on('connect', () => console.log('✅ Redis 연결 성공!'));
redis.on('error', (err) => console.error('❌ Redis 오류:', err));

module.exports = redis;

💻 캐싱 패턴 실전 코드

// utils/cache.js — 캐싱 유틸리티
const redis = require('../db/redis');

/**
 * Cache-Aside 패턴 (가장 흔히 사용)
 * 캐시에 있으면 캐시에서, 없으면 DB에서 가져와 캐시에 저장
 */
const cacheGet = async (key, fetchFn, ttl = 3600) => {
  // 1. 캐시 조회
  const cached = await redis.get(key);
  if (cached) {
    return JSON.parse(cached);  // 캐시 히트!
  }
  // 2. DB 조회
  const data = await fetchFn();
  // 3. 캐시 저장 (TTL: 초 단위)
  await redis.setex(key, ttl, JSON.stringify(data));
  return data;
};

const cacheDelete = async (key) => redis.del(key);
const cacheDeletePattern = async (pattern) => {
  const keys = await redis.keys(pattern);
  if (keys.length) await redis.del(...keys);
};

module.exports = { cacheGet, cacheDelete, cacheDeletePattern };

// services/post.service.js — 캐시 적용 예시
const { cacheGet, cacheDelete } = require('../utils/cache');

const getPostById = async (id) => {
  return cacheGet(
    `post:${id}`,  // 캐시 키
    () => prisma.post.findUnique({ where: { id }, include: { author: true } }),
    1800  // 30분 TTL
  );
};

const updatePost = async (id, data) => {
  const post = await prisma.post.update({ where: { id }, data });
  await cacheDelete(`post:${id}`);  // 수정 시 캐시 삭제 (Cache Invalidation)
  return post;
};

// 이메일 인증 코드 저장 (5분 후 자동 만료)
const saveVerifyCode = async (email, code) => {
  await redis.setex(`verify:${email}`, 300, code);
};

const checkVerifyCode = async (email, code) => {
  const stored = await redis.get(`verify:${email}`);
  return stored === code;
};

11 데이터베이스 보안

🔒 보안 필수 체크리스트

보안 위협 방어 방법 중요도
SQL Injection ORM 사용 또는 Prepared Statement. 절대 문자열 직접 조합 금지! 매우 위험
환경변수 노출 .env 파일을 .gitignore에 추가. GitHub에 절대 올리지 않기! 매우 위험
과도한 권한 최소 권한 원칙. 앱 전용 DB 사용자에게 필요한 권한만 부여 중요
비밀번호 평문 저장 bcrypt(saltRounds: 12)로 해시화. 절대 평문 저장 금지! 매우 위험
백업 없음 정기 백업 설정. 클라우드 DB(Railway, Supabase)는 자동 백업 지원 중요
// ❌ SQL Injection 취약 코드 (절대 이렇게 하지 마세요!)
const query = `SELECT * FROM users WHERE email = '${email}'`;
// 해커가 email에 "' OR '1'='1" 입력하면 모든 사용자 조회됨!

// ✅ 안전한 방법 1: Prisma ORM (자동으로 안전하게 처리)
const user = await prisma.user.findUnique({ where: { email } });

// ✅ 안전한 방법 2: Prepared Statement (pg 라이브러리)
const result = await pool.query(
  'SELECT * FROM users WHERE email = $1',
  [email]  // 파라미터 바인딩
);

12 현업/면접 단골 Q&A

Q1. SQL과 NoSQL의 차이점과 각각 어떤 상황에 사용하나요?
A: SQL(관계형 DB)은 고정된 스키마와 테이블 구조로 데이터 무결성이 중요한 서비스(금융, 쇼핑몰)에 적합합니다. NoSQL은 유연한 스키마로 빠른 개발과 대용량 처리가 필요한 서비스(SNS, 로그 분석)에 적합합니다. 실무에서는 주로 PostgreSQL + Redis 조합을 사용합니다.
Q2. 인덱스란 무엇이고 왜 사용하나요?
A: 인덱스는 데이터를 빠르게 검색하기 위한 자료구조입니다. 책의 목차처럼 원하는 데이터의 위치를 빠르게 찾을 수 있게 해줍니다. WHERE 절, ORDER BY, JOIN에 자주 사용되는 컬럼에 인덱스를 추가하면 검색 속도가 크게 향상됩니다. 단, 인덱스가 많으면 쓰기(INSERT/UPDATE) 성능이 저하되므로 적절히 사용해야 합니다.
Q3. 트랜잭션과 ACID를 설명해주세요.
A: 트랜잭션은 하나의 논리적 작업 단위로, 모두 성공하거나 모두 실패해야 합니다. ACID는 원자성(Atomicity: All or Nothing), 일관성(Consistency: 무결성 유지), 격리성(Isolation: 동시 실행 트랜잭션 간 독립), 지속성(Durability: 커밋된 데이터는 영구 저장)을 의미합니다. 예를 들어 은행 송금에서 출금과 입금은 반드시 같이 성공해야 합니다.
Q4. ORM을 사용하는 이유와 단점은?
A: ORM(Prisma, Sequelize)은 SQL 없이 JavaScript 코드로 DB를 조작할 수 있어 개발 속도가 빨라지고, 타입 안전성을 제공합니다. SQL Injection도 자동으로 방어합니다. 단점은 복잡한 쿼리에서 성능이 떨어질 수 있고, ORM이 생성하는 SQL이 비효율적일 수 있습니다. 복잡한 쿼리는 Raw SQL을 직접 사용하기도 합니다.
Q5. N+1 문제가 무엇이고 어떻게 해결하나요?
A: 목록 조회(1번 쿼리) 후 각 항목의 관련 데이터를 개별 조회(N번 쿼리)해서 총 N+1번 쿼리가 발생하는 문제입니다. 게시글 10개를 가져올 때 각 게시글의 작성자 정보를 별도로 10번 조회하는 경우입니다. Prisma에서는 include로 한 번에 JOIN하거나, dataloader 패턴을 사용해 해결합니다.
단계 학습 내용 기간 가이드 상태
0 백엔드 개요 + JS 기초 2~3주 Guide 0000 완료
1 Node.js 기초 + 비동기 2~3주 Guide 0001 완료
2 Express.js + REST API + JWT 3~4주 Guide 0002 완료
3 데이터베이스 완전 정복 3~4주 Guide 0003 현재
4 인증/보안 심화 2~3주 Guide 0004 예정
5 테스트 심화 (Jest, TDD) 2~3주 Guide 0005 예정
6 TypeScript + NestJS 4~5주 Guide 0006 예정
7 배포/인프라 (Docker, AWS) 4~5주 Guide 0007 예정
🎉

수고하셨습니다!

SQL부터 NoSQL, Prisma ORM, Redis, 트랜잭션, 인덱스 최적화까지
이 내용을 완전히 이해했다면 데이터베이스를 다루는 백엔드 개발자로 취업할 수 있습니다!

✅ 데이터베이스 완료 체크리스트
☐ SQL CRUD (SELECT/INSERT/UPDATE/DELETE) 작성 가능
☐ JOIN 종류 이해 및 실전 사용
☐ 데이터 모델링 (1:1, 1:N, N:M 관계 설계)
☐ Prisma 스키마 작성 및 마이그레이션
☐ Prisma CRUD + include + 페이지네이션
☐ MongoDB + Mongoose 기본 사용
☐ 인덱스 생성 및 EXPLAIN ANALYZE 분석
☐ Prisma $transaction으로 트랜잭션 처리
☐ Redis 캐시 적용 (Cache-Aside 패턴)
☐ N+1 문제 이해 및 해결
📚 다음 단계: BackendDevGuide0004 — 인증/보안 심화
JWT 심화, OAuth2, HTTPS, 암호화, 보안 감사
반응형