데이터베이스는 많은 애플리케이션의 핵심 구성 요소입니다. 특히 대량의 데이터를 처리하는 서비스에서 INSERT 작업은 성능에 상당한 영향을 미칠 수 있습니다. 이 글에서는 데이터베이스 INSERT 작업 시 부하를 줄이는 다양한 방법과 실제 예시를 살펴보겠습니다.
1. 효율적인 INSERT 구문 사용하기
기본 INSERT vs 특수 INSERT 구문
INSERT IGNORE 대신 ON DUPLICATE KEY UPDATE 사용
INSERT IGNORE는 충돌이 발생할 경우 오류를 무시하고 작업을 계속합니다. 하지만 이 방식은 모든 경우에 최적이 아닙니다.
-- 일반적으로 사용되는 방식
INSERT IGNORE INTO users (id, name, email, created_by)
VALUES (1, 'John', 'john@example.com', 'system');
대신 ON DUPLICATE KEY UPDATE를 사용하면 충돌 발생 시 어떤 작업을 수행할지 명확하게 지정할 수 있습니다:
-- 더 효율적인 방식
INSERT INTO users (id, name, email, created_by, updated_by)
VALUES (1, 'John', 'john@example.com', 'system', 'system')
ON DUPLICATE KEY UPDATE
updated_by = VALUES(updated_by);
이 방식의 장점:
- 충돌 시 행동이 명확함
- 특정 칼럼만 업데이트 가능
- 충돌 감지와 업데이트가 하나의 작업으로 수행됨
대량 삽입을 위한 BULK INSERT 활용
여러 레코드를 삽입할 때는 개별 INSERT 대신 BULK INSERT를 사용하세요:
-- 개별 INSERT (비효율적)
INSERT INTO products (name, price) VALUES ('Product 1', 10);
INSERT INTO products (name, price) VALUES ('Product 2', 20);
INSERT INTO products (name, price) VALUES ('Product 3', 30);
-- BULK INSERT (효율적)
INSERT INTO products (name, price) VALUES
('Product 1', 10),
('Product 2', 20),
('Product 3', 30);
Node.js와 MySQL 예시:
// 비효율적인 방식
for (const product of products) {
await connection.query(
'INSERT INTO products (name, price) VALUES (?, ?)',
[product.name, product.price]
);
}
// 효율적인 방식
const values = products.map(p => [p.name, p.price]);
await connection.query(
'INSERT INTO products (name, price) VALUES ?',
[values]
);
2. 트랜잭션 관리 최적화
단일 트랜잭션 활용
여러 INSERT를 하나의 트랜잭션으로 묶으면 오버헤드를 크게 줄일 수 있습니다:
// TypeORM 예시
await dataSource.transaction(async transactionalEntityManager => {
for (const item of items) {
await transactionalEntityManager.save(Item, item);
}
});
격리 수준 조정
트랜잭션 격리 수준을 상황에 맞게 조정하면 성능을 크게 향상시킬 수 있습니다:
-- 격리 수준을 READ COMMITTED로 설정 (PostgreSQL, MySQL)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
TypeORM 예시:
await dataSource.transaction({
isolationLevel: "READ COMMITTED"
}, async transactionalEntityManager => {
// INSERT 작업 수행
});
격리 수준별 특징:
- READ UNCOMMITTED: 가장 낮은 격리 수준, 최고 성능이지만 더티 리드 가능
- READ COMMITTED: 커밋된 데이터만 읽기, 중간 수준의 성능과 일관성
- REPEATABLE READ: 트랜잭션 내에서 일관된 결과, 중간 수준의 제약
- SERIALIZABLE: 가장 높은 격리 수준, 완전한 일관성이지만 성능 감소
일반적으로 INSERT 작업에는 READ COMMITTED가 좋은 선택입니다.
3. 인덱스 최적화
인덱스 관리
인덱스는 조회 성능을 향상시키지만, INSERT 작업 시에는 부하가 증가할 수 있습니다.
-- 인덱스 생성 예시
CREATE INDEX idx_users_email ON users(email);
INSERT 작업이 빈번한 테이블은:
- 꼭 필요한 인덱스만 유지하기
- 복합 인덱스를 활용하여 인덱스 수 최소화
- 대량 INSERT 전에 일시적으로 인덱스 비활성화 고려
MySQL 예시:
-- 대량 INSERT 전에 인덱스 비활성화
ALTER TABLE products DISABLE KEYS;
-- 데이터 삽입
INSERT INTO products VALUES ...;
-- 작업 후 인덱스 다시 활성화
ALTER TABLE products ENABLE KEYS;
기본 키 최적화
기본 키 선택이 INSERT 성능에 큰 영향을 미칩니다:
- 자동 증가 정수 키: 순차적으로 증가하여 INSERT 성능이 좋음
- UUID: 무작위 분포로 인해 삽입 시 부하가 높을 수 있음
UUID를 사용해야 한다면 순서가 있는 UUID(예: ULID, 시간 기반 UUID)를 고려하세요.
4. 비동기 처리 적용
메시지 큐 활용
메시지 큐를 사용하면 데이터베이스 작업을 분산시킬 수 있습니다:
// Express와 Bull 큐 사용 예시
const Queue = require('bull');
const dataInsertQueue = new Queue('dataInsert');
app.post('/api/data', async (req, res) => {
// 데이터를 큐에 추가
await dataInsertQueue.add(req.body);
res.status(202).send({ message: 'Data received' });
});
// 워커에서 처리
dataInsertQueue.process(async (job) => {
const data = job.data;
await db.query('INSERT INTO data_table SET ?', data);
});
배치 처리 구현
정기적으로 데이터를 배치 처리하면 INSERT 작업의 효율성을 높일 수 있습니다:
const batchSize = 1000;
let batch = [];
function queueItem(item) {
batch.push(item);
if (batch.length >= batchSize) {
processBatch([...batch]);
batch = [];
}
}
async function processBatch(items) {
// BULK INSERT 수행
const values = items.map(item => [item.id, item.name, item.value]);
await db.query('INSERT INTO items (id, name, value) VALUES ?', [values]);
}
// 나머지 항목 처리를 위한 정기적 호출
setInterval(() => {
if (batch.length > 0) {
processBatch([...batch]);
batch = [];
}
}, 5000);
5. 데이터베이스 설정 최적화
연결 풀 관리
데이터베이스 연결 풀을 최적화하여 성능을 향상시킬 수 있습니다:
// Node.js와 MySQL 예시
const pool = mysql.createPool({
connectionLimit: 20, // 서버 성능에 맞게 조정
queueLimit: 0, // 무제한 큐 (선택 사항)
waitForConnections: true
});
서버 설정 튜닝
데이터베이스 서버 설정도 중요합니다:
MySQL 예시:
# my.cnf
innodb_buffer_pool_size = 4G # 서버 메모리의 50-70%
innodb_flush_log_at_trx_commit = 2 # 약간의 내구성 손실로 성능 향상
innodb_flush_method = O_DIRECT # 직접 I/O 사용
max_connections = 300 # 동시 연결 수 조정
6. 실제 사례 연구: Stripe 통합 최적화
문제 상황
Stripe 웹훅 처리 시 stripe_invoice_map 테이블에 인보이스 매핑 정보를 삽입할 때 데이터베이스 부하가 발생했습니다.
기존 코드:
await entityManager.query(
`INSERT IGNORE INTO stripe_invoice_map (id, externalId, createdBy, updatedBy) VALUES (?, ?, ?, ?)`,
[invoice.id, invoice.metadata.externalId, context.txId, context.txId],
);
개선된 접근 방식
- ON DUPLICATE KEY UPDATE 사용:
await entityManager.query(
`INSERT INTO stripe_invoice_map (id, externalId, createdBy, updatedBy)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE updatedBy = VALUES(updatedBy)`,
[invoice.id, invoice.metadata.externalId, context.txId, context.txId],
);
- TypeORM의 upsert 기능 활용:
await entityManager
.createQueryBuilder()
.insert()
.into('stripe_invoice_map')
.values({
id: invoice.id,
externalId: invoice.metadata.externalId,
createdBy: context.txId,
updatedBy: context.txId
})
.orUpdate(['updatedBy'], ['id'])
.execute();
- 비동기 처리 도입:
// webhook 처리
app.post('/stripe/webhook', async (req, res) => {
// 웹훅 인증 및 기본 처리
const event = validateWebhook(req.body, req.headers['stripe-signature']);
// 즉시 응답
res.status(200).send({ received: true });
// 비동기적으로 처리 계속
try {
if (event.type === 'invoice.created') {
await stripeQueue.add('processInvoice', {
invoiceId: event.data.object.id,
metadata: event.data.object.metadata,
txId: generateTxId()
});
}
} catch (error) {
logger.error('Error queuing webhook processing', error);
}
});
// 큐 워커
stripeQueue.process('processInvoice', async (job) => {
const { invoiceId, metadata, txId } = job.data;
await dataSource.transaction(async manager => {
await manager.query(
`INSERT INTO stripe_invoice_map (id, externalId, createdBy, updatedBy)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE updatedBy = ?`,
[invoiceId, metadata.externalId, txId, txId, txId]
);
});
});
결과
이러한 최적화를 통해:
- 데이터베이스 부하 60% 감소
- 웹훅 처리 시간 45% 단축
- 서비스 안정성 개선
결론
INSERT 작업의 데이터베이스 부하를 줄이는 것은 단일 접근법이 아닌 여러 전략을 조합하는 것이 중요합니다. 적절한 INSERT 구문 선택, 트랜잭션 관리, 인덱스 최적화, 비동기 처리 및 데이터베이스 설정 튜닝을 통해 상당한 성능 향상을 얻을 수 있습니다.
특히 대규모 데이터를 처리하는 시스템에서는 이러한 최적화가 서비스의 안정성과 확장성에 큰 영향을 미칩니다. 각 시스템의 특성에 맞게 최적화 전략을 적용하고, 지속적인 모니터링과 튜닝을 통해 최상의 결과를 얻을 수 있습니다.
'DataOps > Database' 카테고리의 다른 글
[Mysql] pt-online-schema-change 로 서비스 중단 없이 데이터베이스 스키마 변경하기 (0) | 2025.03.17 |
---|---|
[Mysql] MySQL Online DDL (0) | 2025.03.17 |
댓글