NestJS에서 트랜잭션을 사용해 보기 위해서 공식홈페이지에서 transaction을 검색해 보면 아래와 같이 사용하는 ORM에 따른 트랜잭션 작성방법을 안내해 준다. 필자는 TypeORM을 사용해서 트랜잭션 관리를 해보았다.
TypeORM을 사용한 트랜잭션은 공식 페이지에서 아래와 같이 예시코드를 보여준다.
@Injectable()
export class UsersService {
constructor(private dataSource: DataSource) {}
async createMany(users: User[]) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await queryRunner.manager.save(users[0]);
await queryRunner.manager.save(users[1]);
await queryRunner.commitTransaction();
} catch (err) {
// since we have errors lets rollback the changes we made
await queryRunner.rollbackTransaction();
} finally {
// you need to release a queryRunner which was manually instantiated
await queryRunner.release();
}
}
}
TypeORM 공식페이지의 트랜잭션 사용방법에 가보면 위 방법에 대해서는 "Using QueryRunner to create and control state of single database connection" 즉 QueryRunner를 사용해서 단일 데이터베이스 연결 상태를 생성하고 제어하기라고 설명해준다.
트랜잭션 제어방법은 간단하게 아래의 3가지 방법과 마지막으로 release를 통한 연결 해제 까지가 완료이다.
startTransaction- 쿼리 러너 인스턴스 내에서 새 트랜잭션을 시작
commitTransaction- 쿼리 러너 인스턴스를 사용하여 변경한 모든 내용을 커밋
rollbackTransaction- 쿼리 러너 인스턴스를 사용하여 수행한 모든 변경 사항을 롤백
⛔항상 생각해야 할 것은 반드시 connect()
를 하고나면 마지막으로 release()
를 통해 연결을 끊어 줘야한다. 연결을 끊지않으면 연결이 계속 유지되는데 DB에는 연결의 최대갯수가 존재한다. 따라서 연결을 끊지않고 계속 연결, 연결, 연결을 하다가 최대 갯수에 도달하게 되면, 더이상 연결을 시도하지 못하고, 다음 쿼리 트랜잭션이 동작하지 않게 된다.
위의 코드를 이용해 필자가 작성한 회원가입 로직에 트랜잭션을 적용한 코드는 다음과 같다.
@Injectable()
export class UserService {
constructor(
@InjectRepository(User) private userRepository: Repository<User>,
//typeORM의 DataSource객체 주입
private dataSource: DataSource,
) {}
//유저 정보 저장
async saveUserInfo(userInfo: SignUpDto): Promise<SignUpDto> {
// 주입한 dataSource객체로 QueryRunner생성
const queryRunner = this.dataSource.createQueryRunner();
//QueryRunner로 DB연결
await queryRunner.connect();
//트랜잭션 시작
await queryRunner.startTransaction();
try {
//비밀번호 해싱처리
await this.transformPassword(userInfo);
const user = new User();
user.email = userInfo.email;
user.nickname = userInfo.nickname;
user.password = userInfo.password;
const result = await queryRunner.manager.save(user);
//트랜잭션 성공 후 적용
await queryRunner.commitTransaction();
return result;
} catch (err) {
console.error(err);
//트랜잭션중 실패하면 롤백
await queryRunner.rollbackTransaction();
throw new HttpException('회원가입 트랜잭션 롤백 에러', 500);
} finally {
//트랜잭션 완료 후 연결끊기
await queryRunner.release();
}
// return await this.userRepository.save(userInfo);
}
//패스워드 해싱
async transformPassword(userDto: SignUpDto): Promise<void> {
userDto.password = await bcrypt.hash(userDto.password, 10);
}