CRUD
Create, Read, Update, Delete
데이터의 기본 연산 4가지(생성, 읽기, 수정, 삭제)를 의미합니다. 대부분의 애플리케이션과 REST API가 CRUD 패턴을 기반으로 설계되며, 데이터베이스의 INSERT, SELECT, UPDATE, DELETE와 직접 매핑됩니다.
Create, Read, Update, Delete
데이터의 기본 연산 4가지(생성, 읽기, 수정, 삭제)를 의미합니다. 대부분의 애플리케이션과 REST API가 CRUD 패턴을 기반으로 설계되며, 데이터베이스의 INSERT, SELECT, UPDATE, DELETE와 직접 매핑됩니다.
Create (생성): 새로운 데이터를 생성합니다. SQL에서는 INSERT, HTTP에서는 POST 메서드에 해당합니다. 회원가입, 글 작성, 주문 생성 등이 대표적인 예입니다.
Read (읽기): 기존 데이터를 조회합니다. SQL에서는 SELECT, HTTP에서는 GET 메서드에 해당합니다. 단일 조회(getById)와 목록 조회(getAll, 검색, 필터링)로 구분됩니다.
Update (수정): 기존 데이터를 변경합니다. SQL에서는 UPDATE, HTTP에서는 PUT(전체 교체) 또는 PATCH(부분 수정)에 해당합니다. 프로필 수정, 상품 정보 변경 등이 있습니다.
Delete (삭제): 데이터를 제거합니다. SQL에서는 DELETE, HTTP에서는 DELETE 메서드에 해당합니다. 실제 삭제(Hard Delete)와 논리적 삭제(Soft Delete: is_deleted 플래그)로 구분됩니다.
CRUD와 REST API 매핑: RESTful API 설계의 기본입니다. 리소스 URL(예: /users)에 HTTP 메서드를 조합하여 CRUD를 표현합니다. GET /users(목록), GET /users/:id(상세), POST /users(생성), PUT /users/:id(수정), DELETE /users/:id(삭제).
# CRUD API 예제 - Python FastAPI + SQLAlchemy
from fastapi import FastAPI, HTTPException, Depends
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from pydantic import BaseModel, EmailStr
from datetime import datetime
from typing import List, Optional
# 데이터베이스 설정
DATABASE_URL = "postgresql://user:pass@localhost/mydb"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(bind=engine)
Base = declarative_base()
# 모델 정의
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String(255), unique=True, nullable=False)
username = Column(String(100), nullable=False)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, onupdate=datetime.utcnow)
# Pydantic 스키마
class UserCreate(BaseModel):
email: EmailStr
username: str
class UserUpdate(BaseModel):
email: Optional[EmailStr] = None
username: Optional[str] = None
is_active: Optional[bool] = None
class UserResponse(BaseModel):
id: int
email: str
username: str
is_active: bool
created_at: datetime
class Config:
from_attributes = True
# FastAPI 앱
app = FastAPI()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# ==============================================
# CREATE - 새 사용자 생성
# ==============================================
@app.post("/users", response_model=UserResponse, status_code=201)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
"""
Create: 새로운 리소스 생성
- HTTP Method: POST
- SQL: INSERT INTO users (email, username) VALUES (...)
"""
# 중복 체크
existing = db.query(User).filter(User.email == user.email).first()
if existing:
raise HTTPException(status_code=400, detail="Email already registered")
db_user = User(email=user.email, username=user.username)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
# ==============================================
# READ - 사용자 조회
# ==============================================
@app.get("/users", response_model=List[UserResponse])
def get_users(
skip: int = 0,
limit: int = 100,
is_active: Optional[bool] = None,
db: Session = Depends(get_db)
):
"""
Read (List): 목록 조회 + 페이지네이션 + 필터링
- HTTP Method: GET
- SQL: SELECT * FROM users WHERE ... LIMIT ... OFFSET ...
"""
query = db.query(User)
if is_active is not None:
query = query.filter(User.is_active == is_active)
return query.offset(skip).limit(limit).all()
@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: int, db: Session = Depends(get_db)):
"""
Read (Detail): 단일 리소스 조회
- HTTP Method: GET
- SQL: SELECT * FROM users WHERE id = ?
"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
# ==============================================
# UPDATE - 사용자 정보 수정
# ==============================================
@app.put("/users/{user_id}", response_model=UserResponse)
def update_user(user_id: int, user_update: UserUpdate, db: Session = Depends(get_db)):
"""
Update: 리소스 수정
- HTTP Method: PUT (전체 교체) / PATCH (부분 수정)
- SQL: UPDATE users SET ... WHERE id = ?
"""
db_user = db.query(User).filter(User.id == user_id).first()
if not db_user:
raise HTTPException(status_code=404, detail="User not found")
# None이 아닌 필드만 업데이트
update_data = user_update.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(db_user, field, value)
db.commit()
db.refresh(db_user)
return db_user
# ==============================================
# DELETE - 사용자 삭제
# ==============================================
@app.delete("/users/{user_id}", status_code=204)
def delete_user(user_id: int, db: Session = Depends(get_db)):
"""
Delete: 리소스 삭제 (Hard Delete)
- HTTP Method: DELETE
- SQL: DELETE FROM users WHERE id = ?
"""
db_user = db.query(User).filter(User.id == user_id).first()
if not db_user:
raise HTTPException(status_code=404, detail="User not found")
db.delete(db_user)
db.commit()
return None # 204 No Content
# Soft Delete (논리적 삭제) 버전
@app.delete("/users/{user_id}/soft")
def soft_delete_user(user_id: int, db: Session = Depends(get_db)):
"""
Soft Delete: 실제 삭제하지 않고 비활성화
- is_active = False 또는 deleted_at 타임스탬프 설정
- 데이터 복구 가능, 감사 추적 용이
"""
db_user = db.query(User).filter(User.id == user_id).first()
if not db_user:
raise HTTPException(status_code=404, detail="User not found")
db_user.is_active = False
db.commit()
return {"message": "User deactivated"}
// CRUD API 예제 - Node.js Express + Prisma
const express = require('express');
const { PrismaClient } = require('@prisma/client');
const app = express();
const prisma = new PrismaClient();
app.use(express.json());
// ==============================================
// CREATE - 새 사용자 생성
// ==============================================
app.post('/users', async (req, res) => {
/**
* Create: POST /users
* SQL: INSERT INTO users (email, username) VALUES ($1, $2)
*/
try {
const { email, username } = req.body;
// 유효성 검사
if (!email || !username) {
return res.status(400).json({
error: 'email and username are required'
});
}
const user = await prisma.user.create({
data: { email, username }
});
res.status(201).json(user);
} catch (error) {
if (error.code === 'P2002') { // Unique constraint violation
return res.status(400).json({ error: 'Email already exists' });
}
res.status(500).json({ error: error.message });
}
});
// ==============================================
// READ - 사용자 목록 조회
// ==============================================
app.get('/users', async (req, res) => {
/**
* Read (List): GET /users?page=1&limit=10&search=john
* SQL: SELECT * FROM users WHERE ... LIMIT ... OFFSET ...
*/
try {
const {
page = 1,
limit = 10,
search,
isActive
} = req.query;
const skip = (parseInt(page) - 1) * parseInt(limit);
const take = parseInt(limit);
// 필터 조건 구성
const where = {};
if (search) {
where.OR = [
{ email: { contains: search, mode: 'insensitive' } },
{ username: { contains: search, mode: 'insensitive' } }
];
}
if (isActive !== undefined) {
where.isActive = isActive === 'true';
}
// 병렬로 데이터와 총 개수 조회
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
skip,
take,
orderBy: { createdAt: 'desc' }
}),
prisma.user.count({ where })
]);
res.json({
data: users,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
totalPages: Math.ceil(total / take)
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// ==============================================
// READ - 단일 사용자 조회
// ==============================================
app.get('/users/:id', async (req, res) => {
/**
* Read (Detail): GET /users/:id
* SQL: SELECT * FROM users WHERE id = $1
*/
try {
const { id } = req.params;
const user = await prisma.user.findUnique({
where: { id: parseInt(id) },
include: {
posts: true, // 관계 데이터 포함
profile: true
}
});
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// ==============================================
// UPDATE - 사용자 정보 수정
// ==============================================
app.put('/users/:id', async (req, res) => {
/**
* Update (Full): PUT /users/:id
* SQL: UPDATE users SET email=$1, username=$2 WHERE id=$3
*/
try {
const { id } = req.params;
const { email, username, isActive } = req.body;
const user = await prisma.user.update({
where: { id: parseInt(id) },
data: { email, username, isActive }
});
res.json(user);
} catch (error) {
if (error.code === 'P2025') { // Record not found
return res.status(404).json({ error: 'User not found' });
}
res.status(500).json({ error: error.message });
}
});
// PATCH - 부분 수정
app.patch('/users/:id', async (req, res) => {
/**
* Update (Partial): PATCH /users/:id
* 전달된 필드만 업데이트
*/
try {
const { id } = req.params;
// undefined 필드 제외
const data = Object.fromEntries(
Object.entries(req.body).filter(([_, v]) => v !== undefined)
);
const user = await prisma.user.update({
where: { id: parseInt(id) },
data
});
res.json(user);
} catch (error) {
if (error.code === 'P2025') {
return res.status(404).json({ error: 'User not found' });
}
res.status(500).json({ error: error.message });
}
});
// ==============================================
// DELETE - 사용자 삭제
// ==============================================
app.delete('/users/:id', async (req, res) => {
/**
* Delete (Hard): DELETE /users/:id
* SQL: DELETE FROM users WHERE id = $1
*/
try {
const { id } = req.params;
await prisma.user.delete({
where: { id: parseInt(id) }
});
res.status(204).send(); // No Content
} catch (error) {
if (error.code === 'P2025') {
return res.status(404).json({ error: 'User not found' });
}
res.status(500).json({ error: error.message });
}
});
// Soft Delete
app.delete('/users/:id/soft', async (req, res) => {
try {
const { id } = req.params;
const user = await prisma.user.update({
where: { id: parseInt(id) },
data: {
isActive: false,
deletedAt: new Date()
}
});
res.json({ message: 'User soft deleted', user });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
-- ============================================
-- CRUD SQL 예제 (PostgreSQL)
-- ============================================
-- 테이블 생성
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
username VARCHAR(100) NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP -- Soft Delete용
);
-- ============================================
-- CREATE (INSERT)
-- ============================================
-- 단일 레코드 삽입
INSERT INTO users (email, username)
VALUES ('john@example.com', 'john_doe')
RETURNING *; -- 삽입된 레코드 반환 (PostgreSQL)
-- 다중 레코드 삽입
INSERT INTO users (email, username) VALUES
('alice@example.com', 'alice'),
('bob@example.com', 'bob'),
('charlie@example.com', 'charlie');
-- UPSERT (있으면 업데이트, 없으면 삽입)
INSERT INTO users (email, username)
VALUES ('john@example.com', 'john_updated')
ON CONFLICT (email) DO UPDATE SET
username = EXCLUDED.username,
updated_at = CURRENT_TIMESTAMP;
-- ============================================
-- READ (SELECT)
-- ============================================
-- 전체 조회
SELECT * FROM users;
-- 조건 조회
SELECT * FROM users WHERE is_active = TRUE;
-- 단일 레코드 조회
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE email = 'john@example.com';
-- 검색 (부분 일치)
SELECT * FROM users
WHERE username ILIKE '%john%' -- 대소문자 무시 (PostgreSQL)
OR email ILIKE '%john%';
-- 페이지네이션
SELECT * FROM users
ORDER BY created_at DESC
LIMIT 10 OFFSET 0; -- 1페이지 (0~9)
SELECT * FROM users
ORDER BY created_at DESC
LIMIT 10 OFFSET 10; -- 2페이지 (10~19)
-- 커서 기반 페이지네이션 (대용량에 효율적)
SELECT * FROM users
WHERE id > 100 -- 마지막 조회 ID
ORDER BY id ASC
LIMIT 10;
-- 집계와 함께 조회
SELECT
COUNT(*) as total,
COUNT(*) FILTER (WHERE is_active) as active_count
FROM users;
-- ============================================
-- UPDATE
-- ============================================
-- 단일 필드 업데이트
UPDATE users
SET username = 'john_updated'
WHERE id = 1
RETURNING *;
-- 다중 필드 업데이트
UPDATE users SET
username = 'john_updated',
is_active = FALSE,
updated_at = CURRENT_TIMESTAMP
WHERE id = 1;
-- 조건부 업데이트
UPDATE users SET
is_active = FALSE
WHERE created_at < NOW() - INTERVAL '1 year'
AND is_active = TRUE;
-- CASE를 이용한 조건부 값 설정
UPDATE users SET
is_active = CASE
WHEN last_login_at < NOW() - INTERVAL '6 months' THEN FALSE
ELSE is_active
END;
-- ============================================
-- DELETE
-- ============================================
-- Hard Delete (물리적 삭제)
DELETE FROM users WHERE id = 1;
-- 조건부 삭제
DELETE FROM users
WHERE is_active = FALSE
AND created_at < NOW() - INTERVAL '1 year';
-- 관련 데이터도 함께 삭제 (CASCADE 설정 필요)
DELETE FROM users WHERE id = 1; -- posts, profiles도 삭제됨
-- Soft Delete (논리적 삭제)
UPDATE users SET
is_active = FALSE,
deleted_at = CURRENT_TIMESTAMP
WHERE id = 1;
-- Soft Delete된 데이터 제외하고 조회
SELECT * FROM users
WHERE deleted_at IS NULL;
-- Soft Delete 복구
UPDATE users SET
is_active = TRUE,
deleted_at = NULL
WHERE id = 1;
-- ============================================
-- CRUD 매핑 요약
-- ============================================
-- | CRUD | SQL | HTTP | 설명 |
-- |--------|----------|--------|----------------|
-- | Create | INSERT | POST | 새 리소스 생성 |
-- | Read | SELECT | GET | 리소스 조회 |
-- | Update | UPDATE | PUT | 전체 리소스 교체 |
-- | Update | UPDATE | PATCH | 부분 리소스 수정 |
-- | Delete | DELETE | DELETE | 리소스 삭제 |
"이 리소스의 CRUD API부터 정의합시다. POST /products로 생성, GET /products/:id로 조회, PUT으로 전체 수정, PATCH로 부분 수정, DELETE로 삭제. RESTful 컨벤션 따라가면 됩니다."
"사용자 삭제는 Soft Delete로 가죠. is_deleted 플래그 쓰면 나중에 복구도 가능하고, 감사 로그 추적도 쉽습니다. 실제 물리 삭제는 90일 후 배치로 처리하는 게 좋겠어요."
"Read 비율이 90%가 넘으니까 읽기 최적화에 집중해야 해요. SELECT 쿼리에 적절한 인덱스 추가하고, 자주 조회되는 데이터는 캐싱 레이어 추가합시다. Write는 비동기로 처리하면 되고요."
GET은 조회 전용입니다. GET /users/1/delete처럼 GET으로 삭제하면 안 됩니다. 크롤러나 프리페치가 의도치 않은 삭제를 유발할 수 있습니다.
WHERE 절 없이 UPDATE나 DELETE 실행 시 전체 데이터가 변경/삭제됩니다. 항상 조건절을 확인하고, 프로덕션에서는 트랜잭션으로 감싸세요.
목록 조회 시 각 항목의 관계 데이터를 개별 쿼리로 조회하면 성능이 급격히 저하됩니다. JOIN이나 Eager Loading을 사용하세요.
멱등성 보장(PUT, DELETE는 여러 번 호출해도 같은 결과), 적절한 HTTP 상태코드 반환, 페이지네이션 필수, Soft Delete 고려, 입력 유효성 검사.