로직을 만들다 보면 transation을 사용해야 하는 경우가 필연적으로 생기기 마련이다. 서비스 로직을 만들면서 transaction을 사용할 경우가 있었는데, NestJS와 mongoose에서 어떻게 사용하는지 포스팅 하고자 한다.
먼저 트랜잭션이 무엇인지에 대해 설명해보자면
- 트랜잭션은 데이터베이스 시스템에서 병행 제어 및 회복 작업 시 처리되는 작업의 논리적 단위이다.
- 사용자가 시스템에 대한 서비스 요구 시 시스템이 응답하기 위한 상태 변환 과정의 작업단위이다.
- 하나의 트랜잭션은 Commit되거나 Rollback된다.
Atomicity(원자성)
Consistency(일관성)
Isolation(독립성,격리성)
Durablility(영속성,지속성)
Commit
하나의 트랜잭션이 성공적으로 끝났고, DB가 일관성있는 상태일 때 이를 알려주기 위해 사용하는 연산이다.
Rollback
하나의 트랜잭션 처리가 비정상적으로 종료되어 트랜잭션 원자성이 깨진 경우
transaction이 정상적으로 종료되지 않았을 때, Transaction의 시작 상태로 roll back 할 수 있음.
NestJS + mongoose에서의 Transaction 적용
@Injectable()
export class OrderRepository {
constructor(
@InjectModel(Order.name)
private readonly orderModel: Model<Order>,
private readonly orderProductRepository: OrderproductRepository,
private readonly optionRepository: OptionRepository,
@InjectConnection() private readonly connection: mongoose.Connection,
) {}
...
}
@InjectConnection 데코레이터를 사용하여 mongoose connection을 DI 해준다.
async create(
createOrderProductDTO: CreateOrderProductDTO[],
createOrderDTO: CreateOrderDTO,
): Promise<Order> {
const order = await this.orderModel.create(createOrderDTO);
if (!order) {
throw new NotFoundException();
}
// Transaction session 생성
const session = await this.connection.startSession();
let finalOrderResult; // return value 지정 위한 변수
await session.withTransaction(async () => {
const orderProduct = await this.orderProductRepository.create(
createOrderProductDTO,
);
if (!orderProduct) {
throw new NotFoundException();
}
// orderProduct에 orderId 저장
orderProduct.map(
async (orderProduct) =>
await this.orderProductRepository.update(
orderProduct._id.toString(),
{
orderId: order._id.toString(),
},
),
);
// orderProduct quantity만큼 option의 stockAmount 감소
orderProduct.map(async (orderProduct) =>
this.optionRepository.update(
orderProduct.selectedOption._id.toString(),
{
stockAmount:
(
await this.optionRepository.findOption(
orderProduct.selectedOption._id.toString(),
)
).stockAmount - orderProduct.quantity,
},
session,
),
);
// order에 orderProductId 새로운 필드로 추가
const updateOrderResult = await this.orderModel.findByIdAndUpdate(
order._id.toString(),
{
orderProduct: orderProduct.map((orderProduct) => orderProduct._id),
},
{ new: true },
);
finalOrderResult = updateOrderResult;
});
session.endSession();
return finalOrderResult;
}
withTransaction 메소드를 사용하여 트랜잭션의 모든 단계를 수동으로 제어하는 것을 좀 더 편리하게 하나의 메소드로 사용할 수 있다.
수동으로 제어하기
async create(
createOrderProductDTO: CreateOrderProductDTO[],
createOrderDTO: CreateOrderDTO,
): Promise<Order> {
const order = await this.orderModel.create(createOrderDTO);
if (!order) {
throw new NotFoundException();
}
// Transaction session 생성
const session = await this.connection.startSession();
session.startTransaction();
// Transaction 성공시
try {
let finalOrderResult; // return value 지정 위한 변수
const orderProduct = await this.orderProductRepository.create(
createOrderProductDTO,
);
if (!orderProduct) {
throw new NotFoundException();
}
// orderProduct에 orderId 저장
orderProduct.map(
async (orderProduct) =>
await this.orderProductRepository.update(
orderProduct._id.toString(),
{
orderId: order._id.toString(),
},
),
);
// orderProduct quantity만큼 option의 stockAmount 감소
orderProduct.map(async (orderProduct) =>
this.optionRepository.update(
orderProduct.selectedOption._id.toString(),
{
stockAmount:
(
await this.optionRepository.findOption(
orderProduct.selectedOption._id.toString(),
)
).stockAmount - orderProduct.quantity,
},
session,
),
);
// order에 orderProductId 새로운 필드로 추가
const updateOrderResult = await this.orderModel.findByIdAndUpdate(
order._id.toString(),
{
orderProduct: orderProduct.map((orderProduct) => orderProduct._id),
},
{ new: true },
);
// eslint-disable-next-line prefer-const
finalOrderResult = updateOrderResult;
await session.commitTransaction();
return finalOrderResult;
// Transaction 실패시
} catch (err) {
await session.abortTransaction(); // 실패시 rollback
throw err;
} finally {
session.endSession();
}
}
제가 작성한 글에서 틀린 부분이 있다면 말씀해주시면 감사하겠습니다.
감사합니다 :)