728x90
Transaction 이란?
데이터베이스의 여러 테이블의 값을 수정할 때 일관성을 유지하기 위해서 트랜잭션 단위로 처리
QueryRunner
TypeORM에서 트랜잭션 처리를 위해서는 QueryRunner를 사용합니다.
QueryRunner는 데이터베이스 트랜잭션과 쿼리 실행을 관리하는 객체로, 여러 데이터베이스 쿼리를 하나의 트랜잭션 내에서 실행할 수 있도록 도와줍니다. 보통 복잡한 트랜잭션을 다룰 때 유용하게 사용되며, 여러 쿼리를 안전하게 묶어 실행할 수 있도록 돕습니다.
import { getManager } from "typeorm";
import { User } from "./entity/User";
import { Post } from "./entity/Post";
async function example() {
const queryRunner = getManager().connection.createQueryRunner();
// 트랜잭션 시작
await queryRunner.startTransaction();
try {
// 첫 번째 쿼리: 사용자 생성
const user = await queryRunner.manager.save(User, { name: "Alice" });
// 두 번째 쿼리: 게시글 생성
const post = await queryRunner.manager.save(Post, { title: "Hello", user });
// 트랜잭션 커밋
await queryRunner.commitTransaction();
} catch (error) {
// 오류 발생 시 롤백
await queryRunner.rollbackTransaction();
console.error(error);
} finally {
// 쿼리 러너 해제
await queryRunner.release();
}
}
Interceptor로 분리하기
QueryRunner를 사용할때마다 가져와서 사용하는 것은 비효율적이고 코드가 중복됩니다. 이를 인터셉터로 분리하면 중복을 줄일 수 있습니다.
transaction.Interceptor.ts
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from "@nestjs/common";
import { catchError, Observable, tap } from "rxjs";
import { DataSource } from "typeorm";
@Injectable()
export default class TransactionInterceptor implements NestInterceptor {
constructor(private readonly dataSource: DataSource) {}
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<unknown>> {
const req = context.switchToHttp().getRequest();
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
req.queryRunner = queryRunner;
return next.handle().pipe(
catchError(async (err) => {
await queryRunner.rollbackTransaction();
await queryRunner.release();
throw err;
}),
tap(async () => {
await queryRunner.commitTransaction();
await queryRunner.release();
}),
);
}
}
decorator를 활용하여 @Runner를 사용하기
@Req는 보다는 명시적으로 @Runner라는 데코레이터를 만들어서 사용하면 가독성이 좋아집니다.
transaction.decorator.ts
import {
createParamDecorator,
ExecutionContext,
InternalServerErrorException,
} from "@nestjs/common";
export const Runner = createParamDecorator(
(data, context: ExecutionContext) => {
const req = context.switchToHttp().getRequest();
if (!req.queryRunner) {
throw new InternalServerErrorException(
"No TransactionInterceptor not found",
);
}
return req.queryRunner;
},
);
export default Runner;
사용 예시
class postsController {
@Post()
@UseInterceptors(TransactionInterceptor) // 👈 TransactionInterceptor 사용하도록 명시
async postPosts(@Body() body: CreatePostDto, @Runner() qr: QueryRunner) { // 👈 QueryRunner를 가져온다. TransactionInterceptor를 사용하지 않으면 오류가 발생한다.
const post = await this.postsService.createPost(userId, body, qr);
return post;
}
}
class postsService {
getRepository(qr?: QueryRunner) {
if (!qr) { // 👈 qr이 없으면 주입받은 this.postsRepository를 사용하고
return this.postsRepository;
}
return qr.manager.getRepository<PostsModel>(PostsModel); // 👈 qr이 있으면 qr.manager에서 Repository를 사용한다.
}
async createPost(userId: string, body: CreatePostDto, qr?: QueryRunner) { // 👈 QueryRunner를 nullable로 받는다.
const repository = this.getRepository(qr); // 👈 qr을 통해 Repository를 가져온다.
const post = repository.create({ ...body, user });
return repository.save(post);
}
}
반응형
'NestJS & Fastify' 카테고리의 다른 글
[Fastify] Prisma에서 Transaction 추상화 (0) | 2025.03.16 |
---|