Node.js 데이터베이스 연동(Database Integration) 완벽 가이드
Node.js 애플리케이션에서 데이터베이스 연동은 백엔드 개발의 핵심 영역이다. 이 글에서는 Node.js에서 다양한 데이터베이스를 연동하는 방법과 핵심 개념들을 실용적인 관점에서 다룬다.
1. Node.js에서 데이터베이스 연동의 중요성
Node.js는 비동기 I/O 기반의 런타임으로, 데이터베이스 작업과 같은 I/O 집약적 작업에 최적화되어 있다. 데이터베이스 연동이 중요한 이유는 다음과 같다.
데이터 영속성 확보: 애플리케이션 재시작 후에도 데이터가 유지되어야 한다. 메모리에 저장된 데이터는 프로세스 종료 시 사라지므로, 데이터베이스를 통한 영속성 확보가 필수다.
확장성 있는 아키텍처: 여러 Node.js 인스턴스가 동일한 데이터베이스에 접근하여 데이터를 공유할 수 있다. 이는 수평적 확장(horizontal scaling)의 기본 조건이다.
효율적인 데이터 관리: 데이터베이스는 인덱싱, 쿼리 최적화, 트랜잭션 처리 등 데이터 관리에 필요한 기능을 제공한다.
2. 지원되는 데이터베이스 종류
Node.js는 SQL과 NoSQL 모든 종류의 데이터베이스를 지원한다.
SQL 데이터베이스
관계형 데이터베이스는 정형화된 스키마와 ACID 특성을 제공한다.
| 데이터베이스 | 주요 드라이버 | 특징 |
|---|---|---|
| MySQL | mysql2 | 가장 널리 사용되는 오픈소스 RDBMS |
| PostgreSQL | pg | 고급 기능과 확장성, JSON 지원 |
| SQLite | better-sqlite3 | 파일 기반 경량 데이터베이스 |
| MariaDB | mariadb | MySQL 포크, 성능 개선 |
| Microsoft SQL Server | mssql | 엔터프라이즈 환경에서 사용 |
NoSQL 데이터베이스
비관계형 데이터베이스는 유연한 스키마와 수평적 확장성을 제공한다.
| 데이터베이스 | 주요 드라이버 | 특징 |
|---|---|---|
| MongoDB | mongodb, mongoose | 문서 지향 데이터베이스 |
| Redis | ioredis, redis | 인메모리 키-값 저장소 |
| Cassandra | cassandra-driver | 분산형 와이드 컬럼 저장소 |
| DynamoDB | @aws-sdk/client-dynamodb | AWS 관리형 NoSQL |
| Elasticsearch | @elastic/elasticsearch | 검색 엔진 및 분석 도구 |
3. 데이터베이스 드라이버와 ORM/ODM 개념
네이티브 드라이버
데이터베이스 드라이버는 Node.js와 데이터베이스 간의 저수준 통신을 담당한다. SQL 쿼리를 직접 작성하여 데이터베이스와 통신한다.
// MySQL 네이티브 드라이버 예제 (mysql2)
const mysql = require('mysql2/promise');
async function queryWithDriver() {
const connection = await mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'password',
database: 'myapp'
});
const [rows] = await connection.execute(
'SELECT * FROM users WHERE id = ?',
[1]
);
await connection.end();
return rows;
}
ORM (Object-Relational Mapping)
ORM은 객체와 관계형 데이터베이스 테이블을 매핑하여 SQL을 직접 작성하지 않고도 데이터베이스를 조작할 수 있게 한다.
주요 Node.js ORM:
- Sequelize: MySQL, PostgreSQL, SQLite, MariaDB 지원
- TypeORM: TypeScript 기반, 다양한 데이터베이스 지원
- Prisma: 타입 안전성과 자동 생성 쿼리 제공
- Knex.js: SQL 쿼리 빌더 (엄밀히는 ORM이 아닌 쿼리 빌더)
// Sequelize ORM 예제
const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize('mysql://user:pass@localhost:3306/myapp');
const User = sequelize.define('User', {
username: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true
}
});
// 사용 예
async function createUser() {
const user = await User.create({
username: 'john_doe',
email: 'john@example.com'
});
return user;
}
ODM (Object-Document Mapping)
ODM은 NoSQL 문서 데이터베이스에서 ORM과 유사한 역할을 한다. MongoDB의 경우 Mongoose가 대표적이다.
// Mongoose ODM 예제
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: { type: String, required: true },
email: { type: String, required: true, unique: true },
createdAt: { type: Date, default: Date.now }
});
const User = mongoose.model('User', userSchema);
async function findUsers() {
await mongoose.connect('mongodb://localhost:27017/myapp');
const users = await User.find({ username: /^john/i });
return users;
}
4. 연결 풀링(Connection Pooling) 개념과 중요성
연결 풀링은 데이터베이스 연결을 미리 생성하여 풀에 보관하고, 요청 시 재사용하는 기법이다.
연결 풀링이 필요한 이유
연결 생성 비용 절감: 데이터베이스 연결 수립에는 TCP 핸드셰이크, 인증, 세션 초기화 등 비용이 발생한다. 풀링을 통해 이 비용을 줄인다.
동시 요청 처리: 여러 요청이 동시에 들어올 때 각각 새 연결을 만들지 않고 풀의 연결을 공유한다.
리소스 관리: 데이터베이스 서버의 최대 연결 수 제한을 효율적으로 관리할 수 있다.
연결 풀 구현 예제
// mysql2 연결 풀 예제
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'password',
database: 'myapp',
waitForConnections: true,
connectionLimit: 10, // 풀의 최대 연결 수
queueLimit: 0, // 대기 큐 제한 (0은 무제한)
idleTimeout: 60000, // 유휴 연결 타임아웃 (ms)
enableKeepAlive: true,
keepAliveInitialDelay: 0
});
// 풀에서 연결 획득 및 반환
async function queryWithPool() {
const connection = await pool.getConnection();
try {
const [rows] = await connection.execute('SELECT * FROM users');
return rows;
} finally {
connection.release(); // 풀에 연결 반환
}
}
// 또는 풀 직접 사용 (자동 연결 관리)
async function simpleQuery() {
const [rows] = await pool.execute('SELECT * FROM users WHERE active = ?', [true]);
return rows;
}
연결 풀 설정 권장값
| 설정 | 권장값 | 설명 |
|---|---|---|
| connectionLimit | CPU 코어 수 * 2 ~ 4 | 동시 연결 최대 수 |
| waitForConnections | true | 풀이 가득 찰 때 대기 여부 |
| queueLimit | 0 또는 적절한 값 | 대기 큐 크기 제한 |
| idleTimeout | 60000 (60초) | 유휴 연결 유지 시간 |
5. 비동기 쿼리 처리 패턴
Node.js의 비동기 특성을 활용한 데이터베이스 쿼리 처리 패턴을 살펴본다.
async/await 패턴
가장 권장되는 현대적인 비동기 처리 방식이다.
async function getUserWithPosts(userId) {
const user = await User.findByPk(userId);
if (!user) {
throw new Error('User not found');
}
const posts = await Post.findAll({
where: { userId: user.id }
});
return { user, posts };
}
병렬 쿼리 실행
독립적인 쿼리는 병렬로 실행하여 성능을 향상시킨다.
async function getDashboardData(userId) {
// 병렬 실행으로 전체 응답 시간 단축
const [user, orders, notifications] = await Promise.all([
User.findByPk(userId),
Order.findAll({ where: { userId }, limit: 10 }),
Notification.findAll({ where: { userId, read: false } })
]);
return { user, orders, notifications };
}
순차 쿼리 실행
이전 쿼리 결과에 의존하는 경우 순차 실행이 필요하다.
async function processOrder(orderId) {
const order = await Order.findByPk(orderId);
// order 결과를 사용하여 다음 쿼리 실행
const items = await OrderItem.findAll({
where: { orderId: order.id }
});
// items 결과를 사용
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
await order.update({ total, status: 'processed' });
return order;
}
스트림 기반 대용량 데이터 처리
대량의 데이터를 처리할 때는 스트림을 활용한다.
const { pipeline } = require('stream/promises');
const { Transform } = require('stream');
async function exportLargeDataset() {
const connection = await pool.getConnection();
try {
const stream = connection.query('SELECT * FROM large_table')
.stream();
const transform = new Transform({
objectMode: true,
transform(row, encoding, callback) {
// 각 행을 JSON 문자열로 변환
this.push(JSON.stringify(row) + '\n');
callback();
}
});
await pipeline(stream, transform, fs.createWriteStream('export.json'));
} finally {
connection.release();
}
}
6. 에러 처리 및 트랜잭션 기본 개념
데이터베이스 에러 처리
데이터베이스 작업에서 발생할 수 있는 에러를 적절히 처리해야 한다.
async function safeQuery(userId) {
try {
const user = await User.findByPk(userId);
return user;
} catch (error) {
if (error.code === 'ECONNREFUSED') {
console.error('Database connection failed');
throw new Error('Service temporarily unavailable');
}
if (error.code === 'ER_DUP_ENTRY') {
throw new Error('Duplicate entry exists');
}
if (error.name === 'SequelizeValidationError') {
throw new Error(`Validation failed: ${error.message}`);
}
console.error('Unexpected database error:', error);
throw error;
}
}
재시도 로직 구현
일시적인 연결 문제에 대응하는 재시도 로직이다.
async function queryWithRetry(queryFn, maxRetries = 3, delay = 1000) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await queryFn();
} catch (error) {
const isRetryable = ['ECONNREFUSED', 'ETIMEDOUT', 'ECONNRESET']
.includes(error.code);
if (!isRetryable || attempt === maxRetries) {
throw error;
}
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay * attempt));
}
}
}
// 사용 예
const result = await queryWithRetry(() => User.findAll());
트랜잭션 처리
트랜잭션은 여러 데이터베이스 작업을 하나의 원자적 단위로 묶어 실행한다. 모든 작업이 성공하면 커밋되고, 하나라도 실패하면 전체가 롤백된다.
// Sequelize 트랜잭션 예제
async function transferFunds(fromUserId, toUserId, amount) {
const transaction = await sequelize.transaction();
try {
// 출금
const fromAccount = await Account.findOne({
where: { userId: fromUserId },
transaction,
lock: transaction.LOCK.UPDATE // 비관적 잠금
});
if (fromAccount.balance < amount) {
throw new Error('Insufficient funds');
}
await fromAccount.decrement('balance', { by: amount, transaction });
// 입금
const toAccount = await Account.findOne({
where: { userId: toUserId },
transaction
});
await toAccount.increment('balance', { by: amount, transaction });
// 거래 기록
await Transaction.create({
fromUserId,
toUserId,
amount,
type: 'transfer'
}, { transaction });
await transaction.commit();
return { success: true };
} catch (error) {
await transaction.rollback();
throw error;
}
}
관리 트랜잭션 패턴
Sequelize의 관리 트랜잭션을 사용하면 자동으로 커밋/롤백이 처리된다.
async function managedTransaction() {
const result = await sequelize.transaction(async (t) => {
const user = await User.create({
username: 'newuser',
email: 'new@example.com'
}, { transaction: t });
await Profile.create({
userId: user.id,
bio: 'Hello!'
}, { transaction: t });
return user;
// 정상 완료 시 자동 커밋, 에러 발생 시 자동 롤백
});
return result;
}
7. 데이터베이스 선택 가이드
프로젝트 특성에 따라 적합한 데이터베이스를 선택해야 한다.
SQL 데이터베이스 선택 시
PostgreSQL 선택 권장 상황:
- 복잡한 쿼리와 조인이 많은 경우
- JSON 데이터와 관계형 데이터를 함께 다루는 경우
- 지리공간 데이터 처리 (PostGIS)
- ACID 준수가 중요한 금융, 결제 시스템
MySQL 선택 권장 상황:
- 읽기 작업이 많은 웹 애플리케이션
- 기존 MySQL 기반 시스템과의 호환성
- 풍부한 호스팅 옵션 필요
SQLite 선택 권장 상황:
- 단일 사용자 애플리케이션
- 프로토타입 및 개발 환경
- 임베디드 시스템
NoSQL 데이터베이스 선택 시
MongoDB 선택 권장 상황:
- 스키마가 자주 변경되는 경우
- 문서 형태의 비정형 데이터
- 빠른 개발 속도가 중요한 경우
- 수평적 확장이 필요한 대규모 시스템
Redis 선택 권장 상황:
- 세션 저장소
- 캐싱 레이어
- 실시간 리더보드, 카운터
- 메시지 큐, Pub/Sub
Elasticsearch 선택 권장 상황:
- 전문 검색(Full-text search) 기능
- 로그 분석 및 모니터링
- 대규모 텍스트 데이터 검색
혼합 사용 전략
실제 프로덕션 환경에서는 여러 데이터베이스를 조합하여 사용하는 경우가 많다.
// 복합 데이터 저장소 아키텍처 예시
const architecture = {
primaryData: 'PostgreSQL', // 핵심 비즈니스 데이터
cache: 'Redis', // 캐싱 및 세션
search: 'Elasticsearch', // 검색 기능
analytics: 'ClickHouse' // 분석용 데이터
};
결론
Node.js에서 데이터베이스 연동은 연결 풀링을 통한 효율적인 연결 관리, 비동기 패턴을 활용한 쿼리 처리, 트랜잭션을 통한 데이터 무결성 보장이 핵심이다. 프로젝트 요구사항에 맞는 데이터베이스를 선택하고, ORM/ODM 사용 여부를 결정하여 생산성과 성능의 균형을 맞추는 것이 중요하다.
'Node.js' 카테고리의 다른 글
| Node.js의 쿠키 관리(Cookie Management) (0) | 2026.03.16 |
|---|---|
| Node.js의 세션 관리(Session Management) (0) | 2026.03.15 |
| Node.js의 OAuth2.0 구현 (1) | 2026.03.15 |
| Node.js의 JSON 웹 토큰(JWT) 사용법 (0) | 2026.03.14 |
| Node.js의 Socket.IO 사용법 (0) | 2026.03.14 |