실무에서 접했던 상황을 바탕으로 설명해보자면,
데이터베이스 저장소(repository)
- user_farm: 사용자의 농장정보를 담고 있다.
- farm: 사용자들의 농장정보를 담고 있다.
- farm_dong: 농장의 동(A농장-1동, A농장- 2동, A농장- 3동...) 정보들을 담고 있다.
사용자가 농장을 추가하면 아래와 같은 순서로 저장되는 흐름이다.
1. this.userFarmRepo.save()
2. this.farmRepo.save()
3. this.farmDongRepo.save()
// service.ts
async saveUserFarm(dto: UserFarmDto){
const model = this.userFarmRepo.create(dto);
const userFarm = await this.userFarmRepo.save(model); // step 1
const farm = await this.createFarm(model.idx); // step 2
const dong = await this.createDong(farm.idx); // step 3
return await this.userFarmRepo.findOne({ where: { idx: userFarm.idx }});
}
async createFarm(userIdx: number){
return await this.farmRepo.save({ userIdx });
}
async createDong(farmIdx: number){
return await this.dongRepo.save({ farmIdx });
}
step1: save 메서드로 user_farm 에 저장된 후 저장된 객체가 userFarm 변수에 할당된다.
step 2: 저장된 객체의 idx 값을 farm 테이블에 컬럼 USER_IDX 에 넣고 새로운 row 를 추가하고 추가된 객체를 farm 변수에 할당한다.
step 3: 저장된 farm 객체의 idx 값으로 farm_dong 테이블에 컬럼 FARM_IDX 에 넣고 새로운 row 를 추가한다.
saveUserFarm 함수에서 세 번의 save 가 발생하는데, 각각 save 할때마다 자신 앞에서 save 된 값을 받아 사용하는, 고리형태를 보인다.
여기서 문제는 이 고리형태에서 누구 하나라도 에러가 발생하면이다. 기획적으로 다른 이견이 없다면 보통 모두 저장이 되지 않아야 한다. 그렇기 때문에 step 1 에서 저장되었더라도 step 2 에서 에러가 발생하면 step 1 을 원복해야 하는 roll-back 이 필요하다.
자세히는 모르지만 대부분의 데이터베이스에서는 Transaction 으로 이를 가능하게 한다고 한다. typeORM 역시 Transaction 을 구현할 수 있고 그 방법은 총 두 가지이다.
1. DataSource 객체에 있는 createQueryRunner 메서드
2. DataSource 객체에 있는 transaction 메서드
두 개의 차이점은 범용성인데,
우선 createQueryRunner 를 사용해서 위 코드를 수정해보자.
// service.ts
constructor(
private readonly dataSource: DataSource
){}
async saveUserFarm(dto: UserFarmDto){
// 쿼리 러너 생성
const qr = this.dataSource.createQueryRunner();
// 쿼리 러너 연결
await qr.connect();
// 트랜잭션 시작
await qr.startTransaction();
try{
const userFarmRepo = qr.manager.getRepository(UserFarmRepo);
const userFarm = await userFarmRepo.save(dto); // step 1
const farmRepo = qr.manager.getRepository(FarmRepo);
const farm = await farmRepo.save({ userIdx: model.idx }); // step 2
const dongRepo = qr.manager.getRepository(DongRepo);
const dong = await dongRepo.save({ farmIdx: farm.idx }); // step 3
// 모든 save 커밋 저장
await qr.commitTransaction();
// 쿼리 러너 종료
await qr.release();
return await userFarmRepo.findOne({ where: { idx: userFarm.idx }});
}catch(e){
await qr.rollbackTransaction();
await qr.release();
throw new InternalServerErrorException('에러 발생: ', e.message);
};
}
constructor 에서 typeorm 에서 제공하는 DataSource 객체를 DI 받은 뒤 위와 같이,
1. createQueryRunner 로 QueryRunner 객체를 만들어 qr 변수에 담고,
2. connect 매서드로 transaction 을 연결,
3. startTransaction 메서드로 transaction 시작
4. QueryRunner 객체는 EntityManager 객체를 manager 프로퍼티에 담고 있다. 이 manager 의 getRepository 메서드로 필요한 리파지토리를 불러와 QueryRunner 와 연결시켜줘야 한다.
5. try, catch 문으로 실행시킬 코드를 넣어준다. 마지막으로 모두 정상적으로 저장되었다면 commitTranstion. 실패했다면 rollbackTransaction 메서드를 호출한다.
6. 성공 또는 실패 후 transaction 을 종료 시킨다.
이번에는 transaction 메서드를 사용해보자.
// service.ts
async saveUserFarm(dto: UserFarmDto){
try{
await this.dataSource.transaction(async manager => {
const userFarmRepo = manager.getRepository(UserFarmRepo);
const userFarm = await userFarmRepo.save(dto); // step 1
const farmRepo = manager.getRepository(FarmRepo);
const farm = await farmRepo.save({ userIdx: userFarm.idx }); // step 2
const dongRepo = manager.getRepository(DongRepo);
const dong = await dongRepo.save({ farmIdx: farm.idx }); // step 3
});
return await userFarmRepo.findOne({ where: { idx: userFarm.idx }});
}catch(e){
throw new InternalServerErrorException('에러 발생: ', e.message);
};
}
첫 번째 방법과 다르게 transaction 메서드의 인자로 콜백함수를 넣어주면 된다. 콜백함수의 파라미터는 EntityManager 객체를 담고 있고 해당 객체에서 getRepository 를 호출해 save 할 리파지토리와 transaction 을 연결한다.
한 눈에 보더라도 두 번째 방법이 코드량도 적고 가독성이 좋다.
QueryRunner 의 범용성
QueryRunner 로 transaction 을 실행하기 위해서는 여러개의 메서드를 호출해야 한다. 레고처럼 메서드들이 블록이라고 생각하면, 이 블록들로 나는 자동차를 만들 수 있고, 비행기를 만들 수도 있다. 즉, 부품을 내가 원하는 맛에 따라 조합할 수 있다는 것이다.
인강에서 알려준 QueryRunner 와 Interceptor 을 조합해서 사용하는 코드를 보자.
// interceptor.ts
import { Injectable, CallHandler, ExecutionContext, InternalServerErrorException, NestInterceptor } from "@nestjs/common";
import { Observable, catchError, tap } from "rxjs";
import { DataSource } from "typeorm";
@Injectable()
export class TransationInterceptor implements NestInterceptor {
constructor(
private readonly dataSource: DataSource
) { }
async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const req = context.switchToHttp().getRequest();
const qr = this.dataSource.createQueryRunner();
await qr.connect();
await qr.startTransaction();
req.queryRunner = qr;
return next.handle().pipe(
catchError(async (e) => {
await qr.rollbackTransaction();
throw new InternalServerErrorException(e.message);
}),
tap(async () => {
await qr.commitTransaction();
await qr.release();
})
);
}
}
// controller.ts
@Post()
@UseInterceptor(TransationInterceptor)
postUserFarm(
@Body() dto: UserFarmDto,
@Request() req: Request extends { queryRunner: QueryRunner } ? Request : { queryRunner: QueryRunner }
){
this.farmService.saveUserFarm(dto, req.qr);
}
// service.ts
async saveUserFarm(dto: UserFarmDto, qr: QueryRunner){
try{
const userFarmRepo = qr.manager.getRepository(UserFarmRepo);
const userFarm = await userFarmRepo.save(dto); // step 1
const farmRepo = qr.manager.getRepository(FarmRepo);
const farm = await farmRepo.save({ userIdx: model.idx }); // step 2
const dongRepo = qr.manager.getRepository(DongRepo);
const dong = await dongRepo.save({ farmIdx: farm.idx }); // step 3
return userFarmRepo.findOne({ where: { idx: userFarm.idx }});
}catch(e){
throw new InternalServerErrorException('에러 발생: ', e.message);
};
}
NestJS 에서 interceptor 는 Request 와 Response 를 하나의 객체에서 다룰 수 있다.
사용자가 API 를 호출할때 interceptor 에서 사용자의 request 를 받고 거기에 QueryRunner 객체를 얹어 controller.ts 에 전달한다. 그리고 transaction 이 필요한 route 에 @UseInterceptor 데코레이터를 넣고 실행할 함수의 인자로 @Request 데코레이터를 넣음으로써 해당 함수는 QueryRunner 객체에 접근할 수 있게된다.
함수 안에 있을 service 는 가져온 QueryRunner 객체에 EntityManager 로 원하는 리파지토리에 복수의 CRUD 하면서 roll-back 기능이 생기게되었다.
굳굳~!
'Backend > NestJS' 카테고리의 다른 글
main.ts 파해치기 (useGlobalPipes 옵션 whitelist) (0) | 2024.06.28 |
---|---|
이미지 파일 업로드 심화 - 선업로드 (0) | 2024.06.25 |
이미지 파일 업로드 (0) | 2024.06.25 |
NestJS에서 validation 관련 설정 방법 (class-validator & class-transformer) (0) | 2024.06.18 |
[NestJs] 끄적끄적 (0) | 2023.03.29 |
댓글