해당 내용은 NestJS + mongoDB(mongoose) 으로 작업한 내용.
✅ 문제의 직면: 진행하고 있는 프로젝트에서는 RDBMS(mysql), NoSQL(mongoDB)를 사용하고 있다. 특정 기능에서는 mongoDB의 데이터를 활용하고있는데 mongoDB에서는 트랜잭션을 기본적으로 지원하지 않는다는 것을 알게 되었고, 진행하는 프로젝트에서는 트랜잭션을 환경을 구성하지 않았다. 구성하게 되면 추가적인 리소스 및 비용이 발생할 수 있는데, 현재상황에서는 필요하지 않은 상황이었다. 다만 트랜잭션과 같은 개념을 기능을 사용해야 하는 상황이었다.
그래서, 어떻게 해결했어야 했는가?
👉 실패 또는 에러가 발생할 수 있는 영역에서 트랜잭션 기능이 되지 않는다면 데이터에 대한 오염과 데이터 신뢰 보장이 되지 않기때문에 기존의 롤백(원복)을 위한 추가적인 처리가 필요했다. 그래서 가장 원시적으로보상 트랜잭션을 선택했다. 게다가, 이번문제는 외부 API 호출까지 포함된 내용(애플리케이션 레벨 트랜잭션)으로 이런 개념은애플리케이션 레벨의 보상 트랜잭션의 개념으로 접근해야한다.
트랜잭션이 지원되지 않는 환경에서 데이터 일관성을 어떻게 지킬 것인가?
일반적으로 우리는 데이터베이스 트랜잭션 기능(ACID)을 통해 작업의 원자성을 보장합니다. 하지만 현실에서는 다음과 같은 상황에 자주 부딪힙니다:
이럴 때는 DB 트랜잭션을 쓸 수 없기 때문에, 대신 "애플리케이션 레벨 보상 트랜잭션"을 구현하여 시스템의 일관성과 신뢰성을 유지해야 합니다.
실제로 DB 트랜잭션 기능을 쓰지 않지만, 트랜잭션처럼 작동하는 구조를 코드 수준에서 구현하는 것을 말합니다.
원자성(Atomicity)과 일관성(Consistency)입니다.let backup = null;
try {
// 1. 기존 상태 백업 (lean()으로 순수 객체 확보)
backup = await this.DocumentModel.findOne({ _id }).lean();
// 2. 업데이트 시도
const updated = await this.DocumentModel.findOneAndUpdate(
{ _id },
{ $set: { targetField: JSON.parse(Dto.field) } },
{ new: true }
);
if (!updated) {
throw new Error("업데이트 실패");
}
// 3. 외부 보상/리워드 서비스 호출 또는 외부 API 호출
await this.rewardService.createReward(); // 여기서 실패 가능
} catch (err) {
// 4. 실패 시 수동 롤백 또는 별도의 util 함수 생성
if (learnHistoryBackup) {
await this.LearnHistoryModel.updateOne(
{ _id: learnHistoryBackup._id },
{ $set: { targetField: backup.field } }
);
}
throw err;
}
lean()을 통해 원래 데이터를 백업| 항목 | 설명 |
|---|---|
| 트랜잭션 기능 사용 여부 | ❌ 없음 |
| 트랜잭션 개념 포함 여부 | ✅ 포함됨 |
| 실패 시 롤백 가능 여부 | ✅ 명시적 복원 |
| 설계 목적 | 작업의 원자성과 데이터 일관성 유지 |
| 용어 적절성 | ✅ “보상 트랜잭션”이라는 용어는 업계에서 통용됨 |
lean() 또는 별도 Snapshot)"보상 트랜잭션은, 트랜잭션이 기능이 아니라 책임이라는 사실을 기억하게 만든다. 기능보다는 개념에 주안점을 둘 수 있도록 한다."