목차
- 트랜잭션이란 무엇인가?
- MongoDB에서 트랜잭션과 동시성 관리
- MySQL에서 트랜잭션과 동시성 관리
- MongoDB와 MySQL의 트랜잭션 비교
- 장점과 단점
- 결론
1. 트랜잭션이란 무엇인가?
1.1 트랜잭션의 개념
**트랜잭션(Transaction)**이란 데이터베이스에서 한 번에 수행되는 작업의 단위를 의미합니다. 이는 여러 작업이 하나의 논리적 단위로 묶여 한꺼번에 처리되며, 모든 작업이 성공하거나 모두 실패하는 **원자성(Atomicity)**을 보장합니다.
트랜잭션의 주요 속성은 다음과 같습니다:
- Atomicity(원자성): 트랜잭션 내의 모든 작업은 성공하거나 모두 실패합니다.
- Consistency(일관성): 트랜잭션이 완료되면 데이터베이스는 일관된 상태를 유지해야 합니다.
- Isolation(격리성): 각 트랜잭션은 서로 독립적으로 실행되어야 하며, 트랜잭션이 완료되기 전에는 다른 트랜잭션이 그 작업을 참조할 수 없습니다.
- Durability(지속성): 트랜잭션이 완료되면 그 결과는 영구적으로 반영되어야 합니다.
1.2 트랜잭션이 필요한 이유
트랜잭션을 사용하는 이유는 여러 쿼리를 묶어서 실행할 때 데이터 일관성을 보장하기 위해서입니다. 여러 작업 중 하나라도 실패하면 전체 작업을 롤백하고, 데이터가 손상되지 않도록 해야 합니다.
예시 시나리오:
- 은행 송금: 한 계좌에서 출금하고 다른 계좌에 입금하는 작업 중 하나라도 실패하면 전체 작업을 취소해야 합니다.
- 주문 처리: 고객이 주문을 완료하고 재고를 차감하는 과정에서 문제가 생기면, 주문과 재고 차감 모두를 취소해야 합니다.
2. MongoDB에서 트랜잭션과 동시성 관리
2.1. MongoDB에서의 트랜잭션
MongoDB는 기본적으로 NoSQL 데이터베이스이지만, 4.0 버전부터 다중 문서 트랜잭션을 지원하기 시작했습니다. MongoDB의 트랜잭션은 관계형 데이터베이스의 트랜잭션과 비슷하게 동작하며, ACID 속성을 보장합니다. 다만, 트랜잭션 기능은 replica set 환경에서만 사용할 수 있습니다.
MongoDB 트랜잭션의 주요 특징:
- 여러 문서에 걸친 일관된 데이터 처리 가능
- 트랜잭션이 커밋되기 전까지는 트랜잭션 내의 변경 사항을 다른 클라이언트가 볼 수 없습니다.
- 트랜잭션 충돌 시 롤백 처리 가능
2.2. MongoDB에서 SELECT FOR UPDATE
와 동시성 관리
관계형 데이터베이스에서 흔히 사용하는 SELECT FOR UPDATE
는 특정 데이터를 수정하기 전 다른 트랜잭션이 해당 데이터를 변경하지 못하도록 잠금을 설정하는 기능입니다. MongoDB는 이와 유사한 방식으로 데이터를 수정하는 트랜잭션에서 데이터 일관성을 보장할 수 있습니다. 다만, MongoDB는 트랜잭션 중간에 락을 거는 대신, 트랜잭션 격리 수준을 통해 이를 보장합니다.
MongoDB 트랜잭션은 **스냅샷 격리(Snapshot Isolation)**를 사용하여 동작합니다. 즉, 트랜잭션이 시작되면 트랜잭션 내에서 해당 데이터를 읽는 다른 클라이언트는 트랜잭션이 완료되기 전까지 해당 데이터를 수정할 수 없습니다.
예시: MongoDB에서의 트랜잭션 사용 (NestJS)
먼저 필요한 패키지를 설치합니다:
npm install @nestjs/mongoose mongoose
MongoDB 트랜잭션을 사용하는 NestJS 서비스는 다음과 같이 구현할 수 있습니다.
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { InjectConnection } from '@nestjs/mongoose';
import { Connection } from 'mongoose';
import { Account } from './schemas/account.schema';
@Injectable()
export class AppService {
constructor(
@InjectModel('Account') private accountModel: Model<Account>,
@InjectConnection() private readonly connection: Connection,
) {}
async transferFunds(fromAccountId: string, toAccountId: string, amount: number) {
const session = await this.connection.startSession();
session.startTransaction();
try {
// 출금 계좌 업데이트
const fromAccount = await this.accountModel.findById(fromAccountId).session(session);
if (fromAccount.balance < amount) {
throw new Error('Insufficient balance');
}
fromAccount.balance -= amount;
await fromAccount.save({ session });
// 입금 계좌 업데이트
const toAccount = await this.accountModel.findById(toAccountId).session(session);
toAccount.balance += amount;
await toAccount.save({ session });
// 커밋
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
}
}
2.3. 트랜잭션 동시성 문제 해결: 충돌 및 재시도
MongoDB 트랜잭션이 충돌할 수 있는 상황에서는 재시도 로직을 추가하는 것이 일반적입니다. 충돌이 발생하면 트랜잭션은 자동으로 롤백되며, 클라이언트는 이를 감지하고 다시 시도해야 합니다.
const MAX_RETRIES = 3;
let retries = 0;
while (retries < MAX_RETRIES) {
const session = await this.connection.startSession();
session.startTransaction();
try {
// 트랜잭션 내에서 작업 수행
await session.commitTransaction();
break; // 성공 시 종료
} catch (error) {
await session.abortTransaction();
retries++;
if (retries >= MAX_RETRIES) {
throw new Error('Transaction failed after maximum retries.');
}
} finally {
session.endSession();
}
}
이러한 재시도 로직을 통해 트랜잭션 충돌 시에도 안전하게 데이터를 처리할 수 있습니다.
3. MySQL에서 트랜잭션과 동시성 관리
3.1. MySQL에서의 트랜잭션
MySQL은 ACID 특성을 보장하는 관계형 데이터베이스로, InnoDB 스토리지 엔진을 통해 트랜잭션을 지원합니다. InnoDB는 특히 **행 수준의 잠금(row-level locking)**을 지원하여 동시성 문제를 해결하는데 효과적입니다. 트랜잭션은 MySQL에서 매우 중요한 역할을 하며, 여러 작업을 안전하게 수행할 수 있도록 도와줍니다.
MySQL 트랜잭션의 기본 흐름:
START TRANSACTION
으로 트랜잭션 시작- 작업 수행 (INSERT, UPDATE, DELETE)
COMMIT
또는ROLLBACK
으로 트랜잭션 완료
MySQL 트랜잭션 예시
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
위 예시는 계좌 간 송금을 처리하는 기본적인 트랜잭션입니다. 첫 번째 계좌에서 100을 출금하고 두 번째 계좌에 100을 입금합니다.
3.2. SELECT FOR UPDATE
를 활용한 동시성 관리
MySQL에서는 SELECT FOR UPDATE
구문을 사용하여 특정 행을 잠그고, 다른 트랜잭션이 그 데이터를 읽거나 수정하지 못하도록 할 수 있습니다. 이는 동시성 제어에 중요한 기능입니다.
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- 계좌에서 금액을 업데이트
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
3.3. NestJS에서 MySQL 트랜잭션 구현
NestJS와 TypeORM을 사용하여 MySQL에서 트랜잭션을 구현할 수 있습니다.
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Account } from './account.entity';
import { Connection } from 'typeorm';
@Injectable()
export class AppService {
constructor(
@InjectRepository(Account) private accountRepository: Repository<Account>,
private connection: Connection,
) {}
async transferFunds(fromAccountId: number, toAccountId: number, amount: number) {
return await this.connection.transaction(async (manager) => {
const fromAccount = await manager
.getRepository(Account)
.createQueryBuilder('account')
.setLock('pessimistic_write')
.where('account.id = :id', { id: fromAccountId })
.getOne();
if (!fromAccount || fromAccount.balance < amount) {
throw new Error('Insufficient balance');
}
fromAccount.balance -= amount;
await manager.save(fromAccount);
const toAccount = await manager
.getRepository(Account)
.createQueryBuilder('account')
.setLock('pessimistic_write')
.where('account.id = :id', { id: toAccountId })
.getOne();
if (!toAccount) {
throw new Error('To account not found');
}
toAccount.balance += amount;
await manager.save(toAccount);
});
}
}
위 코드는 트랜잭션을 사용하여 두 계좌 간 송금을 안전하게 처리하며, SELECT FOR UPDATE
를 통해 동시성 문제를 해결합니다.
4. MongoDB와 MySQL의 트랜잭션 비교
4.1. 주요 차이점
- 트랜잭션 지원: MySQL은 모든 트랜잭션 작업을 기본적으로 지원하는 반면, MongoDB는 4.0 이상 버전에서만 트랜잭션을 지원하며, 이는 replica set 환경에서만 가능합니다.
- 동시성 제어 방식: MySQL은
SELECT FOR UPDATE
와 같은 구문으로 잠금을 통해 동시성 문제를 해결하지만, MongoDB는 트랜잭션 격리 수준으로 동시성 문제를 처리합니다.
4.2. 장점
- MongoDB의 장점:
- 스키마 유연성: JSON-like 문서 기반으로 스키마를 자유롭게 변경할 수 있습니다.
- 수평 확장성: NoSQL의 특성상 대규모 분산 시스템에 적합합니다.
- MySQL의 장점:
- 성숙한 트랜잭션 시스템: ACID 트랜잭션 지원이 오래된 기능이며, 복잡한 데이터 무결성 보장이 가능합니다.
- 복잡한 쿼리 처리: 복잡한 조인 및 데이터 관계를 쉽게 처리할 수 있습니다.
4.3. 단점
- MongoDB의 단점:
- 트랜잭션의 제한적 지원: 트랜잭션이 모든 상황에서 사용 가능한 것은 아니며, 성능 이슈가 발생할 수 있습니다.
- 복잡한 데이터 관계 처리의 어려움: 조인이 제한적이므로, 복잡한 데이터 관계를 처리하기 위해서는 다른 방법이 필요합니다.
- MySQL의 단점:
- 수평 확장성의 어려움: MySQL은 수직 확장에 더 적합하며, MongoDB만큼 분산 처리에 적합하지 않습니다.
- 스키마 유연성 부족: 스키마가 고정되어 있어 데이터 구조 변경이 어렵습니다.
5. 장점과 단점 정리
- MongoDB와 MySQL의 트랜잭션 및 동시성 관리는 각 시스템의 장단점에 따라 다릅니다. 두 시스템 모두 트랜잭션을 지원하지만, 사용 방법과 동시성 제어 방식에 차이가 있습니다.
- MongoDB는 대규모 데이터 처리와 수평 확장이 중요한 경우 유리하며, MySQL은 복잡한 데이터 관계와 트랜잭션이 중요한 경우 더 적합합니다.
6. 결론
이번 글에서는 MongoDB와 MySQL에서의 트랜잭션과 동시성 관리에 대해 살펴보았습니다. 두 시스템은 모두 ACID 트랜잭션을 지원하지만, 각자의 장단점이 있습니다. MongoDB는 NoSQL 데이터베이스로서 스키마 유연성과 확장성에 강점을 지니고 있으며, MySQL은 관계형 데이터베이스로서 강력한 트랜잭션 관리와 데이터 무결성을 보장합니다.
NestJS와 같은 현대적 프레임워크를 사용하면 이러한 데이터베이스와 쉽게 연동하여 트랜잭션을 관리할 수 있으며, 트랜잭션 충돌을 처리하고 안전한 동시성 제어를 구현할 수 있습니다.
각 데이터베이스의 특성을 이해하고, 요구 사항에 맞는 시스템을 선택하는 것이 중요합니다. 대규모 데이터 처리가 필요하다면 MongoDB를, 복잡한 데이터 관계와 강력한 트랜잭션 관리가 필요하다면 MySQL을 선택하는 것이 좋습니다.
이 글을 통해 트랜잭션과 동시성 문제를 관리하는 데 필요한 기본적인 개념과 구현 방법을 이해하고, 실무에 적용할 수 있기를 바랍니다.
'DataBase' 카테고리의 다른 글
MySQL에서 문자 결합하는 방법: 다양한 함수와 사용 예시(feat. CONCAT, CONCAT_WS, GROUP_CONCAT) (1) | 2024.10.02 |
---|---|
Index Scan과 Index Seek (0) | 2024.09.17 |
MySQL에서의 잠금 메커니즘과 격리 수준: 공유 잠금(S LOCK)과 배타 잠금(EXCLUSIVE LOCK)의 동작 및 문제 발생 시나리오 (0) | 2024.08.19 |
SQL Server에서 Clustered Index와 Non-Clustered Index의 차이점 (0) | 2024.08.12 |
데이터베이스 인덱스란 무엇인가요? (MySQL vs. SQL Server) (0) | 2024.08.12 |