본문으로 건너뛰기
Previous
Next
Node.js 데이터베이스 연동 | MongoDB, PostgreSQL, MySQL

Node.js 데이터베이스 연동 | MongoDB, PostgreSQL, MySQL

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 QueryPostgreSQL vs MySQL 선택을 함께 보세요. 캐시 계층은 Redis 캐싱 패턴으로, Docker Compose로 API·DB·Redis를 묶고 Nginx·Kubernetes(minikube)로 배포를 이어가면 됩니다. 서버 디스크·inode 이슈는 Linux 디스크/inode 트러블슈팅과 맞물립니다.

실전 경험에서 배운 교훈

이 기술을 실무 프로젝트에 처음 도입했을 때, 공식 문서만으로는 알 수 없었던 많은 함정들이 있었습니다. 특히 프로덕션 환경에서 발생하는 엣지 케이스들은 로컬 개발 환경에서는 재현조차 되지 않았죠.

가장 어려웠던 점은 성능 최적화였습니다. 처음엔 “동작만 하면 되겠지”라고 생각했지만, 실제 사용자 트래픽이 몰리면서 병목 지점들이 하나씩 드러났습니다. 특히 데이터베이스 쿼리 최적화, 캐싱 전략, 에러 핸들링 구조 등은 여러 번의 장애를 겪으면서 개선해 나갔습니다.

이 글에서는 그런 시행착오를 통해 얻은 실전 노하우와, “이렇게 하면 안 된다”는 교훈들을 함께 정리했습니다. 특히 트러블슈팅 섹션은 실제 장애 대응 경험을 바탕으로 작성했으니, 비슷한 문제를 마주했을 때 참고하시면 도움이 될 것입니다.

1. MongoDB (Mongoose)

설치

# MongoDB 드라이버
npm install mongodb
# Mongoose (ODM)
npm install mongoose

연결

// 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;

스키마 정의

// 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 작업

const User = require('./models/User');
// CREATE
async function createUser() {
    const user = new User({
        name: '홍길동',
        email: '[email protected]',
        age: 25
    });
    
    await user.save();
    console.log('사용자 생성:', user);
    
    // 또는
    const user2 = await User.create({
        name: '김철수',
        email: '[email protected]',
        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: '[email protected]' });
    
    // 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: '[email protected]' });
    
    // 여러 개 삭제
    await User.deleteMany({ isActive: false });
}

관계 (Relationship)

// 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)

설치

# PostgreSQL 드라이버
npm install pg
# Sequelize (ORM)
npm install sequelize

Raw Query (pg)

// 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;
// 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)

// 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;
// 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;
// CRUD
const User = require('./models/User');
// CREATE
const user = await User.create({
    name: '홍길동',
    email: '[email protected]',
    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

연결

// 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;

쿼리 실행

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

// 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

// 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. 쿼리 최적화

인덱스

// 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 문제 해결

문제:

// ❌ 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번 쿼리

해결:

// ✅ Populate 사용 (2번 쿼리)
const posts = await Post.find().populate('author');
for (const post of posts) {
    console.log(post.author.name);  // 추가 쿼리 없음
}
// 총 2번 쿼리 (posts + authors)

쿼리 선택 (Projection)

// ❌ 모든 필드 가져오기
const users = await User.find();
// ✅ 필요한 필드만 가져오기
const users = await User.find().select('name email');
// PostgreSQL
const [users] = await pool.query('SELECT name, email FROM users');

페이지네이션

// 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)

설정

// 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
});

모니터링

// 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
// 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');
    }
};

실행:

# 마이그레이션 실행
npx sequelize-cli db:migrate
# 롤백
npx sequelize-cli db:migrate:undo
# 모두 롤백
npx sequelize-cli db:migrate:undo:all

8. 실전 프로젝트: 블로그 API

TypeScript/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);
// 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: 연결 누수

원인: 연결을 반환하지 않음

// ❌ 연결 누수
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

// ❌ 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: 트랜잭션 누락

// ❌ 트랜잭션 없음 (데이터 불일치 가능)
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. 실전 팁

환경별 설정

// 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];

연결 재시도

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

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);

정리

핵심 요약

  1. MongoDB: 문서 기반, Mongoose ODM
  2. PostgreSQL: 관계형, pg 드라이버, Sequelize ORM
  3. MySQL: 관계형, mysql2 드라이버
  4. 커넥션 풀: 연결 재사용, 성능 향상
  5. 인덱스: 쿼리 성능 최적화
  6. 트랜잭션: 데이터 일관성 보장

데이터베이스 비교

특징MongoDBPostgreSQLMySQL
타입NoSQLSQLSQL
스키마유연엄격엄격
트랜잭션
조인PopulateJOINJOIN
확장성수평 확장 쉬움수직 확장수직 확장
학습 곡선낮음높음중간

ORM vs Raw Query

특징ORMRaw Query
생산성✅ 높음⭕ 낮음
성능⭕ 오버헤드✅ 최적화 가능
타입 안전성
복잡한 쿼리⭕ 어려움✅ 쉬움
유지보수✅ 쉬움⭕ 어려움

다음 단계

추천 학습 자료

MongoDB:


관련 글

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「Node.js 데이터베이스 연동 | MongoDB, PostgreSQL, MySQL」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「Node.js 데이터베이스 연동 | MongoDB, PostgreSQL, MySQL」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Node.js 데이터베이스 연동: MongoDB, PostgreSQL, MySQL. MongoDB (Mongoose)부터 핵심 개념·패턴·실무 함정을 코드 예제로 풉니다. Node.js·Database·MongoDB… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • 데이터베이스 선택 가이드 | SQL vs NoSQL 완벽 비교
  • [Sequelize ORM Complete Guide for Node.js | PostgreSQL](/en/blog/nodejs-sequelize-orm-guide/
  • [Node.js Database Integration: MongoDB, PostgreSQL, and MySQL](/en/blog/nodejs-series-06-database/

이 글에서 다루는 키워드 (관련 검색어)

Node.js, Database, MongoDB, PostgreSQL, MySQL, Mongoose, Sequelize 등으로 검색하시면 이 글이 도움이 됩니다.