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

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

이 글의 핵심

Node.js 데이터베이스 연동에 대한 실전 가이드입니다. MongoDB, PostgreSQL, MySQL 등을 예제와 함께 상세히 설명합니다.

들어가며

데이터베이스 종류

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

// 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
생산성✅ 높음⭕ 낮음
성능⭕ 오버헤드✅ 최적화 가능
타입 안전성
복잡한 쿼리⭕ 어려움✅ 쉬움
유지보수✅ 쉬움⭕ 어려움

다음 단계

  • Node.js 인증과 보안
  • Node.js 테스트
  • Node.js 배포

추천 학습 자료

MongoDB:

PostgreSQL:

MySQL:


관련 글

  • 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]