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)로 연결
중간 테이블(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
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 문제 이해 및 해결
☐ 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, 암호화, 보안 감사
JWT 심화, OAuth2, HTTPS, 암호화, 보안 감사
반응형