[2026] Node.js 데이터베이스 연동 | MongoDB, PostgreSQL, MySQL
이 글의 핵심
Node.js 데이터베이스 연동: MongoDB, PostgreSQL, MySQL. MongoDB (Mongoose)부터 핵심 개념·패턴·실무 함정을 코드 예제로 풉니다.
들어가며
데이터베이스 종류
Node에서 DB에 붙을 때는 연결 한 번에 쿼리 하나가 아니라, 보통 풀(pool)에 연결을 재사용하고, 드라이버·ORM이 비동기 API로 결과를 Promise로 돌려줍니다. Express 라우트 안에서는 await User.find(...)처럼 쓰되, 연결 끊김·재시도·트랜잭션은 설정과 코드 패턴으로 다루는 것이 일반적입니다.
SQL (관계형):
- ✅ PostgreSQL: 강력한 기능, 표준 SQL
- ✅ MySQL: 빠른 속도, 널리 사용
- ✅ SQLite: 파일 기반, 간단한 프로젝트 NoSQL (비관계형):
- ✅ MongoDB: 문서 기반, 유연한 스키마
- ✅ Redis: 인메모리, 캐싱
- ✅ Cassandra: 분산 데이터베이스
선택 가이드
| 데이터베이스 | 장점 | 사용 사례 |
|---|---|---|
| PostgreSQL | 강력한 기능, ACID | 복잡한 쿼리, 금융 |
| MySQL | 빠름, 안정적 | 웹 앱, CMS |
| MongoDB | 유연한 스키마 | 프로토타입, 실시간 |
| Redis | 매우 빠름 | 캐싱, 세션 |
| 스택과 연결하기: PostgreSQL·MySQL 실습은 아래 각 절에서 이어지며, C++에서는 libpq·연결 풀로 Node 드라이버·ORM과 같은 문제(풀링, 파라미터 바인딩, 트랜잭션)를 다룹니다. ORM과 Raw Query 트레이드오프는 본문 ORM vs Raw Query와 PostgreSQL vs MySQL 선택을 함께 보세요. 캐시 계층은 Redis 캐싱 패턴으로, Docker Compose로 API·DB·Redis를 묶고 Nginx·Kubernetes(minikube)로 배포를 이어가면 됩니다. 서버 디스크·inode 이슈는 Linux 디스크/inode 트러블슈팅과 맞물립니다. |
실무에서 마주한 현실
개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.
1. MongoDB (Mongoose)
설치
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# MongoDB 드라이버
npm install mongodb
# Mongoose (ODM)
npm install mongoose
연결
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// db.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect('mongodb://localhost:27017/mydb', {
useNewUrlParser: true,
useUnifiedTopology: true
});
console.log('MongoDB 연결 성공');
} catch (err) {
console.error('MongoDB 연결 실패:', err.message);
process.exit(1);
}
};
module.exports = connectDB;
스키마 정의
다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// models/User.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, '이름이 필요합니다'],
trim: true,
minlength: 2,
maxlength: 50
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, '유효한 이메일이 아닙니다']
},
age: {
type: Number,
min: 0,
max: 150
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
isActive: {
type: Boolean,
default: true
},
createdAt: {
type: Date,
default: Date.now
}
});
// 인덱스
userSchema.index({ email: 1 });
// 가상 필드
userSchema.virtual('info').get(function() {
return `${this.name} (${this.email})`;
});
// 인스턴스 메서드
userSchema.methods.greet = function() {
return `안녕하세요, ${this.name}님!`;
};
// 정적 메서드
userSchema.statics.findByEmail = function(email) {
return this.findOne({ email });
};
// 미들웨어 (pre hook)
userSchema.pre('save', function(next) {
console.log('저장 전:', this.name);
next();
});
// 미들웨어 (post hook)
userSchema.post('save', function(doc) {
console.log('저장 후:', doc.name);
});
module.exports = mongoose.model('User', userSchema);
CRUD 작업
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
const User = require('./models/User');
// CREATE
async function createUser() {
const user = new User({
name: '홍길동',
email: 'hong@example.com',
age: 25
});
await user.save();
console.log('사용자 생성:', user);
// 또는
const user2 = await User.create({
name: '김철수',
email: 'kim@example.com',
age: 30
});
}
// READ
async function readUsers() {
// 모두 조회
const users = await User.find();
// 조건 조회
const adults = await User.find({ age: { $gte: 18 } });
// 하나만 조회
const user = await User.findOne({ email: 'hong@example.com' });
// ID로 조회
const userById = await User.findById('507f1f77bcf86cd799439011');
// 필드 선택
const names = await User.find().select('name email -_id');
// 정렬
const sorted = await User.find().sort({ age: -1 }); // 내림차순
// 페이지네이션
const page = 1;
const limit = 10;
const paginated = await User.find()
.skip((page - 1) * limit)
.limit(limit);
return users;
}
// UPDATE
async function updateUser(id) {
// 방법 1: findByIdAndUpdate
const user = await User.findByIdAndUpdate(
id,
{ age: 26 },
{ new: true, runValidators: true }
);
// 방법 2: save
const user2 = await User.findById(id);
user2.age = 26;
await user2.save();
// 여러 개 업데이트
await User.updateMany(
{ age: { $lt: 18 } },
{ isActive: false }
);
}
// DELETE
async function deleteUser(id) {
// ID로 삭제
await User.findByIdAndDelete(id);
// 조건으로 삭제
await User.deleteOne({ email: 'hong@example.com' });
// 여러 개 삭제
await User.deleteMany({ isActive: false });
}
관계 (Relationship)
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// models/Post.js
const postSchema = new mongoose.Schema({
title: { type: String, required: true },
content: { type: String, required: true },
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User', // User 모델 참조
required: true
},
tags: [String],
createdAt: { type: Date, default: Date.now }
});
const Post = mongoose.model('Post', postSchema);
// 포스트 생성
const post = await Post.create({
title: '첫 글',
content: '내용',
author: userId // User의 ObjectId
});
// Populate (조인)
const posts = await Post.find().populate('author');
// author 필드에 User 객체가 채워짐
// 선택적 populate
const posts2 = await Post.find().populate('author', 'name email');
// author에서 name과 email만 가져옴
2. PostgreSQL (pg, Sequelize)
설치
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# PostgreSQL 드라이버
npm install pg
# Sequelize (ORM)
npm install sequelize
Raw Query (pg)
다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// db.js
const { Pool } = require('pg');
const pool = new Pool({
host: 'localhost',
port: 5432,
database: 'mydb',
user: 'postgres',
password: 'password',
max: 20, // 최대 연결 수
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000
});
module.exports = pool;
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// queries.js
const pool = require('./db');
// SELECT
async function getUsers() {
const result = await pool.query('SELECT * FROM users');
return result.rows;
}
// INSERT
async function createUser(name, email) {
const query = 'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *';
const values = [name, email];
const result = await pool.query(query, values);
return result.rows[0];
}
// UPDATE
async function updateUser(id, name) {
const query = 'UPDATE users SET name = $1 WHERE id = $2 RETURNING *';
const result = await pool.query(query, [name, id]);
return result.rows[0];
}
// DELETE
async function deleteUser(id) {
const query = 'DELETE FROM users WHERE id = $1';
await pool.query(query, [id]);
}
// 트랜잭션
async function transferMoney(fromId, toId, amount) {
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
[amount, fromId]
);
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[amount, toId]
);
await client.query('COMMIT');
console.log('이체 성공');
} catch (err) {
await client.query('ROLLBACK');
console.error('이체 실패:', err.message);
throw err;
} finally {
client.release();
}
}
Sequelize (ORM)
다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// db.js
const { Sequelize } = require('sequelize');
const sequelize = new Sequelize('mydb', 'postgres', 'password', {
host: 'localhost',
dialect: 'postgres',
logging: false, // SQL 로그 비활성화
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
});
module.exports = sequelize;
다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// models/User.js
// 변수 선언 및 초기화
const { DataTypes } = require('sequelize');
const sequelize = require('../db');
const User = sequelize.define('User', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: DataTypes.STRING(100),
allowNull: false,
validate: {
len: [2, 50]
}
},
email: {
type: DataTypes.STRING(255),
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
age: {
type: DataTypes.INTEGER,
validate: {
min: 0,
max: 150
}
},
role: {
type: DataTypes.ENUM('user', 'admin'),
defaultValue: 'user'
}
}, {
tableName: 'users',
timestamps: true // createdAt, updatedAt 자동 생성
});
module.exports = User;
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// CRUD
const User = require('./models/User');
// CREATE
const user = await User.create({
name: '홍길동',
email: 'hong@example.com',
age: 25
});
// READ
const users = await User.findAll();
const user = await User.findByPk(1);
const filtered = await User.findAll({
where: { age: { [Op.gte]: 18 } },
order: [['createdAt', 'DESC']],
limit: 10,
offset: 0
});
// UPDATE
await User.update(
{ age: 26 },
{ where: { id: 1 } }
);
// DELETE
await User.destroy({ where: { id: 1 } });
3. MySQL
설치
npm install mysql2
연결
아래 코드는 javascript를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// db.js
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'password',
database: 'mydb',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
module.exports = pool;
쿼리 실행
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
const pool = require('./db');
// SELECT
async function getUsers() {
const [rows] = await pool.query('SELECT * FROM users');
return rows;
}
// INSERT
async function createUser(name, email) {
const [result] = await pool.query(
'INSERT INTO users (name, email) VALUES (?, ?)',
[name, email]
);
return {
id: result.insertId,
name,
email
};
}
// UPDATE
async function updateUser(id, name) {
const [result] = await pool.query(
'UPDATE users SET name = ? WHERE id = ?',
[name, id]
);
return result.affectedRows;
}
// DELETE
async function deleteUser(id) {
const [result] = await pool.query(
'DELETE FROM users WHERE id = ?',
[id]
);
return result.affectedRows;
}
// 트랜잭션
async function transferMoney(fromId, toId, amount) {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
await connection.query(
'UPDATE accounts SET balance = balance - ? WHERE id = ?',
[amount, fromId]
);
await connection.query(
'UPDATE accounts SET balance = balance + ? WHERE id = ?',
[amount, toId]
);
await connection.commit();
console.log('이체 성공');
} catch (err) {
await connection.rollback();
console.error('이체 실패:', err.message);
throw err;
} finally {
connection.release();
}
}
4. 실전 예제: REST API
MongoDB + Express
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// app.js
const express = require('express');
const mongoose = require('mongoose');
const User = require('./models/User');
const app = express();
app.use(express.json());
// MongoDB 연결
mongoose.connect('mongodb://localhost:27017/mydb')
.then(() => console.log('MongoDB 연결됨'))
.catch(err => console.error('연결 실패:', err));
// 모든 사용자 조회
app.get('/api/users', async (req, res) => {
try {
const { page = 1, limit = 10, sort = '-createdAt' } = req.query;
const users = await User.find()
.sort(sort)
.skip((page - 1) * limit)
.limit(parseInt(limit));
const total = await User.countDocuments();
res.json({
users,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 특정 사용자 조회
app.get('/api/users/:id', async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다' });
}
res.json(user);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 사용자 생성
app.post('/api/users', async (req, res) => {
try {
const user = await User.create(req.body);
res.status(201).json(user);
} catch (err) {
if (err.name === 'ValidationError') {
return res.status(400).json({ error: err.message });
}
res.status(500).json({ error: err.message });
}
});
// 사용자 수정
app.put('/api/users/:id', async (req, res) => {
try {
const user = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!user) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다' });
}
res.json(user);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 사용자 삭제
app.delete('/api/users/:id', async (req, res) => {
try {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다' });
}
res.status(204).send();
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.listen(3000, () => {
console.log('서버 실행 중: http://localhost:3000');
});
PostgreSQL + Express
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// app.js
const express = require('express');
const pool = require('./db');
const app = express();
app.use(express.json());
// 모든 사용자 조회
app.get('/api/users', async (req, res) => {
try {
const { page = 1, limit = 10 } = req.query;
const offset = (page - 1) * limit;
const [users] = await pool.query(
'SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?',
[parseInt(limit), offset]
);
const [[{ total }]] = await pool.query('SELECT COUNT(*) as total FROM users');
res.json({
users,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 사용자 생성
app.post('/api/users', async (req, res) => {
try {
const { name, email, age } = req.body;
const [result] = await pool.query(
'INSERT INTO users (name, email, age) VALUES (?, ?, ?)',
[name, email, age]
);
const [users] = await pool.query(
'SELECT * FROM users WHERE id = ?',
[result.insertId]
);
res.status(201).json(users[0]);
} catch (err) {
if (err.code === 'ER_DUP_ENTRY') {
return res.status(400).json({ error: '이미 존재하는 이메일입니다' });
}
res.status(500).json({ error: err.message });
}
});
app.listen(3000);
5. 쿼리 최적화
인덱스
아래 코드는 javascript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// MongoDB
userSchema.index({ email: 1 }); // 단일 인덱스
userSchema.index({ name: 1, age: -1 }); // 복합 인덱스
userSchema.index({ email: 1 }, { unique: true }); // 유니크 인덱스
// PostgreSQL
await pool.query('CREATE INDEX idx_email ON users(email)');
await pool.query('CREATE INDEX idx_name_age ON users(name, age)');
await pool.query('CREATE UNIQUE INDEX idx_email_unique ON users(email)');
N+1 문제 해결
문제: 아래 코드는 javascript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ N+1 쿼리 (느림)
const posts = await Post.find(); // 1번 쿼리
for (const post of posts) {
const author = await User.findById(post.author); // N번 쿼리
console.log(author.name);
}
// 총 1 + N번 쿼리
해결: 아래 코드는 javascript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ Populate 사용 (2번 쿼리)
const posts = await Post.find().populate('author');
for (const post of posts) {
console.log(post.author.name); // 추가 쿼리 없음
}
// 총 2번 쿼리 (posts + authors)
쿼리 선택 (Projection)
아래 코드는 javascript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 모든 필드 가져오기
const users = await User.find();
// ✅ 필요한 필드만 가져오기
const users = await User.find().select('name email');
// PostgreSQL
const [users] = await pool.query('SELECT name, email FROM users');
페이지네이션
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// MongoDB
async function paginateUsers(page = 1, limit = 10) {
const skip = (page - 1) * limit;
const [users, total] = await Promise.all([
User.find().skip(skip).limit(limit),
User.countDocuments()
]);
return {
users,
page,
limit,
total,
pages: Math.ceil(total / limit)
};
}
// PostgreSQL
async function paginateUsers(page = 1, limit = 10) {
const offset = (page - 1) * limit;
const [users] = await pool.query(
'SELECT * FROM users LIMIT ? OFFSET ?',
[limit, offset]
);
const [[{ total }]] = await pool.query('SELECT COUNT(*) as total FROM users');
return {
users,
page,
limit,
total,
pages: Math.ceil(total / limit)
};
}
6. 커넥션 풀 (Connection Pool)
설정
다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// MongoDB
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/mydb', {
maxPoolSize: 10, // 최대 연결 수
minPoolSize: 2, // 최소 연결 수
maxIdleTimeMS: 30000
});
// PostgreSQL
const { Pool } = require('pg');
const pool = new Pool({
max: 20, // 최대 연결 수
min: 5, // 최소 연결 수
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000
});
// MySQL
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
connectionLimit: 10,
queueLimit: 0,
waitForConnections: true
});
모니터링
다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// PostgreSQL
pool.on('connect', () => {
console.log('새 연결 생성');
});
pool.on('acquire', () => {
console.log('연결 획득');
});
pool.on('release', () => {
console.log('연결 반환');
});
// 풀 상태 확인
console.log('총 연결:', pool.totalCount);
console.log('유휴 연결:', pool.idleCount);
console.log('대기 중:', pool.waitingCount);
7. 마이그레이션
Sequelize 마이그레이션
npm install --save-dev sequelize-cli
npx sequelize-cli init
마이그레이션 생성:
npx sequelize-cli migration:generate --name create-users-table
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// migrations/20260329-create-users-table.js
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('users', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: Sequelize.STRING(100),
allowNull: false
},
email: {
type: Sequelize.STRING(255),
allowNull: false,
unique: true
},
age: {
type: Sequelize.INTEGER
},
created_at: {
type: Sequelize.DATE,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updated_at: {
type: Sequelize.DATE,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
}
});
await queryInterface.addIndex('users', ['email']);
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('users');
}
};
실행: 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 마이그레이션 실행
npx sequelize-cli db:migrate
# 롤백
npx sequelize-cli db:migrate:undo
# 모두 롤백
npx sequelize-cli db:migrate:undo:all
8. 실전 프로젝트: 블로그 API
다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// models/Post.js (MongoDB)
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true,
minlength: 1,
maxlength: 200
},
content: {
type: String,
required: true
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
tags: [String],
published: {
type: Boolean,
default: false
},
views: {
type: Number,
default: 0
}
}, {
timestamps: true
});
// 인덱스
postSchema.index({ title: 'text', content: 'text' }); // 전문 검색
postSchema.index({ author: 1, createdAt: -1 });
// 가상 필드
postSchema.virtual('url').get(function() {
return `/posts/${this._id}`;
});
module.exports = mongoose.model('Post', postSchema);
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// routes/posts.js
const express = require('express');
const router = express.Router();
const Post = require('../models/Post');
// 모든 글 조회
router.get('/', async (req, res) => {
try {
const { page = 1, limit = 10, tag, author } = req.query;
const query = {};
if (tag) query.tags = tag;
if (author) query.author = author;
const posts = await Post.find(query)
.populate('author', 'name email')
.sort({ createdAt: -1 })
.skip((page - 1) * limit)
.limit(parseInt(limit));
const total = await Post.countDocuments(query);
res.json({
posts,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 글 검색
router.get('/search', async (req, res) => {
try {
const { q } = req.query;
if (!q) {
return res.status(400).json({ error: '검색어가 필요합니다' });
}
const posts = await Post.find(
{ $text: { $search: q } },
{ score: { $meta: 'textScore' } }
)
.sort({ score: { $meta: 'textScore' } })
.populate('author', 'name');
res.json({ posts, count: posts.length });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 글 작성
router.post('/', async (req, res) => {
try {
const post = await Post.create({
...req.body,
author: req.user.id // 인증 미들웨어에서 설정
});
await post.populate('author', 'name email');
res.status(201).json(post);
} catch (err) {
if (err.name === 'ValidationError') {
return res.status(400).json({ error: err.message });
}
res.status(500).json({ error: err.message });
}
});
// 조회수 증가
router.post('/:id/view', async (req, res) => {
try {
const post = await Post.findByIdAndUpdate(
req.params.id,
{ $inc: { views: 1 } },
{ new: true }
);
if (!post) {
return res.status(404).json({ error: '글을 찾을 수 없습니다' });
}
res.json({ views: post.views });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;
9. 자주 발생하는 문제
문제 1: 연결 누수
원인: 연결을 반환하지 않음 다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 연결 누수
async function bad() {
const connection = await pool.getConnection();
const [rows] = await connection.query('SELECT * FROM users');
return rows; // connection.release() 누락!
}
// ✅ finally로 보장
async function good() {
const connection = await pool.getConnection();
try {
const [rows] = await connection.query('SELECT * FROM users');
return rows;
} finally {
connection.release(); // 항상 실행
}
}
문제 2: SQL Injection
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ SQL Injection 취약
async function vulnerable(email) {
const query = `SELECT * FROM users WHERE email = '${email}'`;
const [rows] = await pool.query(query);
return rows;
}
// 공격: email = "' OR '1'='1"
// ✅ Prepared Statement 사용
async function safe(email) {
const [rows] = await pool.query(
'SELECT * FROM users WHERE email = ?',
[email]
);
return rows;
}
문제 3: 트랜잭션 누락
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 트랜잭션 없음 (데이터 불일치 가능)
async function bad(fromId, toId, amount) {
await pool.query('UPDATE accounts SET balance = balance - ? WHERE id = ?', [amount, fromId]);
// 여기서 에러 발생 시 첫 번째 쿼리만 실행됨!
await pool.query('UPDATE accounts SET balance = balance + ? WHERE id = ?', [amount, toId]);
}
// ✅ 트랜잭션 사용
async function good(fromId, toId, amount) {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
await connection.query('UPDATE accounts SET balance = balance - ? WHERE id = ?', [amount, fromId]);
await connection.query('UPDATE accounts SET balance = balance + ? WHERE id = ?', [amount, toId]);
await connection.commit();
} catch (err) {
await connection.rollback();
throw err;
} finally {
connection.release();
}
}
10. 실전 팁
환경별 설정
다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// config/database.js
module.exports = {
development: {
mongodb: 'mongodb://localhost:27017/mydb-dev',
postgres: {
host: 'localhost',
database: 'mydb_dev',
user: 'postgres',
password: 'password'
}
},
production: {
mongodb: process.env.MONGODB_URI,
postgres: {
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: true
}
}
};
const env = process.env.NODE_ENV || 'development';
module.exports = module.exports[env];
연결 재시도
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
async function connectWithRetry(maxRetries = 5) {
for (let i = 0; i < maxRetries; i++) {
try {
await mongoose.connect('mongodb://localhost:27017/mydb');
console.log('MongoDB 연결 성공');
return;
} catch (err) {
console.error(`연결 실패 (${i + 1}/${maxRetries}):`, err.message);
if (i === maxRetries - 1) {
throw err;
}
const delay = Math.pow(2, i) * 1000;
console.log(`${delay}ms 후 재시도...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
connectWithRetry();
Graceful Shutdown
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
const mongoose = require('mongoose');
async function gracefulShutdown() {
console.log('서버 종료 중...');
try {
await mongoose.connection.close();
console.log('MongoDB 연결 종료');
await pool.end();
console.log('PostgreSQL 연결 종료');
process.exit(0);
} catch (err) {
console.error('종료 실패:', err.message);
process.exit(1);
}
}
process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);
정리
핵심 요약
- MongoDB: 문서 기반, Mongoose ODM
- PostgreSQL: 관계형, pg 드라이버, Sequelize ORM
- MySQL: 관계형, mysql2 드라이버
- 커넥션 풀: 연결 재사용, 성능 향상
- 인덱스: 쿼리 성능 최적화
- 트랜잭션: 데이터 일관성 보장
데이터베이스 비교
| 특징 | MongoDB | PostgreSQL | MySQL |
|---|---|---|---|
| 타입 | NoSQL | SQL | SQL |
| 스키마 | 유연 | 엄격 | 엄격 |
| 트랜잭션 | ✅ | ✅ | ✅ |
| 조인 | Populate | JOIN | JOIN |
| 확장성 | 수평 확장 쉬움 | 수직 확장 | 수직 확장 |
| 학습 곡선 | 낮음 | 높음 | 중간 |
ORM vs Raw Query
| 특징 | ORM | Raw Query |
|---|---|---|
| 생산성 | ✅ 높음 | ⭕ 낮음 |
| 성능 | ⭕ 오버헤드 | ✅ 최적화 가능 |
| 타입 안전성 | ✅ | ❌ |
| 복잡한 쿼리 | ⭕ 어려움 | ✅ 쉬움 |
| 유지보수 | ✅ 쉬움 | ⭕ 어려움 |
다음 단계
추천 학습 자료
MongoDB:
- MongoDB 공식 문서
- Mongoose 공식 문서 PostgreSQL:
- PostgreSQL 공식 문서
- node-postgres
- Sequelize MySQL:
- MySQL 공식 문서
- mysql2
관련 글
- Python 데이터베이스 | SQLite, PostgreSQL, ORM 완벽 정리
- PostgreSQL vs MySQL 차이와 선택 가이드
- Redis 캐싱 전략 패턴
- Docker Compose로 API·PostgreSQL·Redis
- Nginx 리버스 프록시 · Kubernetes minikube 배포
- Linux 디스크 full vs inode full
- C++ 데이터베이스 연동 완벽 가이드 | SQLite·PostgreSQL·연결 풀·트랜잭션 [#31-3]
- C++ 쿼리 최적화 완벽 가이드 | 인덱스 선택·실행 계획·통계·비용 모델·프로덕션 패턴 [#49-3]
- C++ 데이터베이스 쿼리 최적화 완벽 가이드 | 인덱스·실행 계획·캐싱·N+1 해결 [#51-8]
- C++ MongoDB 완벽 가이드 | mongocxx·CRUD·연결·문제 해결·성능 최적화 [#52-3]