icon

트랜잭션 관리와 데이터 정합성


 데이터베이스 트랜잭션과 데이터 정합성은 안정적이고 신뢰할 수 있는 애플리케이션 구축의 핵심입니다.

 NestJS 애플리케이션에서 이를 효과적으로 관리하는 것은 매우 중요합니다.

트랜잭션의 개념과 ACID 속성

 트랜잭션은 데이터베이스의 상태를 변화시키는 하나의 논리적 작업 단위를 말합니다.

 ACID는 트랜잭션의 네 가지 주요 속성을 나타냅니다.

  • 원자성(Atomicity) : 트랜잭션의 모든 연산이 완전히 수행되거나 전혀 수행되지 않아야 함
  • 일관성(Consistency) : 트랜잭션 실행 전후의 데이터베이스 상태가 일관되어야 함
  • 격리성(Isolation) : 동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않아야 함
  • 지속성(Durability) : 성공적으로 완료된 트랜잭션의 결과는 영구적으로 반영되어야 함

 NestJS 애플리케이션에서 ACID 속성을 준수하는 것은 데이터의 무결성을 보장하고 시스템의 신뢰성을 높이는 데 중요합니다.

다양한 ORM에서의 트랜잭션 구현

 TypeORM

@Injectable()
export class UserService {
  constructor(private connection: Connection) {}
 
  async createUserWithProfile(userData: CreateUserDto, profileData: CreateProfileDto) {
    const queryRunner = this.connection.createQueryRunner();
 
    await queryRunner.connect();
    await queryRunner.startTransaction();
 
    try {
      const user = await queryRunner.manager.save(User, userData);
      await queryRunner.manager.save(Profile, { ...profileData, user });
 
      await queryRunner.commitTransaction();
      return user;
    } catch (err) {
      await queryRunner.rollbackTransaction();
      throw new Error('Failed to create user with profile');
    } finally {
      await queryRunner.release();
    }
  }
}

 Mongoose

 Mongoose는 트랜잭션을 네이티브로 지원하지 않지만 MongoDB 4.0 이상에서는 다음과 같이 구현할 수 있습니다.

@Injectable()
export class UserService {
  constructor(@InjectConnection() private readonly connection: Connection) {}
 
  async createUserWithProfile(userData: CreateUserDto, profileData: CreateProfileDto) {
    const session = await this.connection.startSession();
    session.startTransaction();
 
    try {
      const user = await this.userModel.create([userData], { session });
      await this.profileModel.create([{ ...profileData, user: user[0]._id }], { session });
 
      await session.commitTransaction();
      return user[0];
    } catch (error) {
      await session.abortTransaction();
      throw error;
    } finally {
      session.endSession();
    }
  }
}

 Prisma

@Injectable()
export class UserService {
  constructor(private prisma: PrismaService) {}
 
  async createUserWithProfile(userData: CreateUserDto, profileData: CreateProfileDto) {
    return this.prisma.$transaction(async (prisma) => {
      const user = await prisma.user.create({ data: userData });
      await prisma.profile.create({ data: { ...profileData, userId: user.id } });
      return user;
    });
  }
}

트랜잭션 자동화

 NestJS의 인터셉터를 사용하여 트랜잭션을 자동화할 수 있습니다.

@Injectable()
export class TransactionInterceptor implements NestInterceptor {
  constructor(private dataSource: DataSource) {}
 
  async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();
 
    try {
      const result = await next.handle().toPromise();
      await queryRunner.commitTransaction();
      return of(result);
    } catch (err) {
      await queryRunner.rollbackTransaction();
      throw err;
    } finally {
      await queryRunner.release();
    }
  }
}

 이 인터셉터를 컨트롤러나 메서드에 적용하여 자동으로 트랜잭션을 관리할 수 있습니다.

분산 트랜잭션

 분산 트랜잭션은 여러 데이터베이스 또는 서비스에 걸쳐 있는 트랜잭션을 말합니다.

 NestJS에서 이를 구현하려면 2단계 커밋(Two-Phase Commit) 프로토콜이나 사가(Saga) 패턴을 사용할 수 있습니다.

 사가 패턴 예시

@Injectable()
export class OrderSaga {
  @Saga()
  orderProcess = (events$: Observable<any>): Observable<ICommand> => {
    return events$.pipe(
      ofType(OrderCreatedEvent),
      map((event) => new ProcessPaymentCommand(event.orderId)),
      catchError((error) => of(new CancelOrderCommand(event.orderId)))
    );
  }
}

낙관적 잠금과 비관적 잠금

 낙관적 잠금

 TypeORM에서 낙관적 잠금 구현

@Entity()
class Product {
  @Version()
  version: number;
}
 
@Injectable()
class ProductService {
  async updateProduct(id: number, data: UpdateProductDto) {
    try {
      await this.productRepository.update(id, data);
    } catch (error) {
      if (error instanceof OptimisticLockVersionMismatchError) {
        // 충돌 처리 로직
      }
      throw error;
    }
  }
}

 비관적 잠금

 TypeORM에서 비관적 잠금 구현

@Injectable()
class ProductService {
  async updateProduct(id: number, data: UpdateProductDto) {
    await this.connection.transaction(async manager => {
      const product = await manager.findOne(Product, id, { lock: { mode: 'pessimistic_write' } });
      if (product) {
        manager.merge(Product, product, data);
        await manager.save(product);
      }
    });
  }
}

데이터 일관성 유지 전략

 이벤트 소싱

 이벤트 소싱은 애플리케이션의 상태 변경을 일련의 이벤트로 저장하는 패턴입니다.

@Injectable()
class OrderService {
  constructor(private eventStore: EventStore) {}
 
  async createOrder(orderData: CreateOrderDto) {
    const orderCreatedEvent = new OrderCreatedEvent(orderData);
    await this.eventStore.saveEvent(orderCreatedEvent);
    // 추가 로직
  }
}

 CQRS (Command Query Responsibility Segregation)

 CQRS는 명령(쓰기)과 쿼리(읽기)를 분리하는 패턴입니다.

@CommandHandler(CreateOrderCommand)
class CreateOrderHandler {
  async execute(command: CreateOrderCommand) {
    // 주문 생성 로직
  }
}
 
@QueryHandler(GetOrderQuery)
class GetOrderHandler {
  async execute(query: GetOrderQuery) {
    // 주문 조회 로직
  }
}

대규모 데이터 처리 전략

  1. 배치 처리 : 대량의 데이터를 작은 배치로 나누어 처리
  2. 비동기 처리 : 장시간 실행되는 작업을 백그라운드 작업으로 처리
  3. 읽기 전용 레플리카 사용 : 읽기 작업을 별도의 데이터베이스로 분산

Best Practices와 주의사항

  1. 트랜잭션은 가능한 짧게 유지하여 락 경합을 줄입니다.
  2. 데드락을 방지하기 위해 일관된 순서로 리소스에 접근합니다.
  3. 트랜잭션 내에서 외부 서비스 호출을 피합니다.
  4. 대규모 데이터 처리 시 배치 처리나 스트리밍 처리를 고려합니다.
  5. 분산 시스템에서는 최종 일관성(Eventual Consistency)을 고려합니다.
  6. 트랜잭션 롤백 시나리오를 테스트합니다.
  7. 성능 병목 지점을 식별하기 위해 주기적으로 프로파일링합니다.
  8. 데이터베이스 인덱스를 적절히 사용하여 트랜잭션 성능을 최적화합니다.
  9. 트랜잭션 격리 수준을 애플리케이션 요구사항에 맞게 설정합니다.
  10. 데이터 정합성 검증을 위한 자동화된 테스트를 작성합니다.

 NestJS 애플리케이션에서 트랜잭션 관리와 데이터 정합성 유지는 견고하고 신뢰할 수 있는 시스템 구축의 핵심입니다.

 다양한 ORM과 데이터베이스 시스템에 대한 이해, 그리고 적절한 패턴과 전략의 적용이 중요합니다.