메인 콘텐츠로 건너뛰기
transaction() 메서드를 사용하면 트랜잭션을 세밀하게 제어할 수 있습니다.

수동 트랜잭션 개요

세밀한 제어

트랜잭션 경계 명확부분 트랜잭션 가능

조건부 롤백

상황에 따른 롤백커밋 전 검증

중첩 트랜잭션

Savepoint 사용부분 롤백 지원

명시적 관리

트랜잭션 흐름 명확디버깅 용이

기본 사용법

단순 트랜잭션

class UserModel extends BaseModelClass {
  async createUser(data: UserSaveParams): Promise<number> {
    const wdb = this.getPuri("w");
    
    return wdb.transaction(async (trx) => {
      // 트랜잭션 내에서 trx 사용
      trx.ubRegister("users", data);
      const [userId] = await trx.ubUpsert("users");
      
      // 성공 시 자동 커밋
      return userId;
    });
  }
}
transaction() 콜백 함수가 성공적으로 완료되면 자동으로 COMMIT됩니다. 에러가 발생하면 자동으로 ROLLBACK됩니다.

트랜잭션 흐름

실전 예제

예제 1: 기본 트랜잭션

class UserModel extends BaseModelClass {
  async registerUser(
    userData: UserSaveParams,
    profileData: { bio: string }
  ): Promise<number> {
    const wdb = this.getPuri("w");
    
    return wdb.transaction(async (trx) => {
      // 1. User 생성
      const [user] = await trx
        .table("users")
        .insert(userData)
        .returning({ id: "id" });
      
      // 2. Profile 업데이트
      await trx
        .table("users")
        .where("id", user.id)
        .update({ bio: profileData.bio });
      
      // 성공 시 자동 커밋
      return user.id;
    });
  }
}

예제 2: 조건부 롤백

class OrderModel extends BaseModelClass {
  async createOrder(
    userId: number,
    items: OrderItem[]
  ): Promise<number> {
    const wdb = this.getPuri("w");
    
    return wdb.transaction(async (trx) => {
      // 1. 재고 확인
      for (const item of items) {
        const product = await trx
          .table("products")
          .where("id", item.productId)
          .first();
        
        if (!product || product.stock < item.quantity) {
          // 조건부 롤백
          throw new Error(`Insufficient stock for product ${item.productId}`);
        }
      }
      
      // 2. 주문 생성
      const [order] = await trx
        .table("orders")
        .insert({ user_id: userId, status: "pending" })
        .returning({ id: "id" });
      
      // 3. 재고 차감
      for (const item of items) {
        await trx
          .table("products")
          .where("id", item.productId)
          .decrement("stock", item.quantity);
      }
      
      return order.id;
    });
  }
}

예제 3: UpsertBuilder와 함께

class CompanyModel extends BaseModelClass {
  async createOrganization(data: {
    companyName: string;
    departments: string[];
  }): Promise<{ companyId: number; departmentIds: number[] }> {
    const wdb = this.getPuri("w");
    
    return wdb.transaction(async (trx) => {
      // 1. Company 등록
      const companyRef = trx.ubRegister("companies", {
        name: data.companyName,
      });
      
      // 2. Departments 등록
      data.departments.forEach((name) => {
        trx.ubRegister("departments", {
          name,
          company_id: companyRef,
        });
      });
      
      // 3. 순서대로 저장
      const [companyId] = await trx.ubUpsert("companies");
      const departmentIds = await trx.ubUpsert("departments");
      
      return { companyId, departmentIds };
    });
  }
}

중첩 트랜잭션 (Savepoint)

기본 중첩

class UserModel extends BaseModelClass {
  async complexOperation(userId: number): Promise<void> {
    const wdb = this.getPuri("w");
    
    return wdb.transaction(async (trx1) => {
      console.log("Outer transaction started");
      
      // 외부 트랜잭션 작업
      await trx1
        .table("users")
        .where("id", userId)
        .update({ last_login_at: new Date() });
      
      // 중첩 트랜잭션 (Savepoint)
      await trx1.transaction(async (trx2) => {
        console.log("Inner transaction started (savepoint)");
        
        // 내부 트랜잭션 작업
        await trx2
          .table("users")
          .where("id", userId)
          .update({ is_verified: true });
      });
      
      console.log("Outer transaction completed");
    });
  }
}

부분 롤백

class UserModel extends BaseModelClass {
  async updateWithPartialRollback(userId: number): Promise<void> {
    const wdb = this.getPuri("w");
    
    return wdb.transaction(async (trx1) => {
      // 1. 외부 트랜잭션 작업 (유지됨)
      await trx1
        .table("users")
        .where("id", userId)
        .update({ last_login_at: new Date() });
      
      try {
        // 2. 중첩 트랜잭션 (실패 가능)
        await trx1.transaction(async (trx2) => {
          await trx2
            .table("users")
            .where("id", userId)
            .update({ is_verified: true });
          
          // 의도적인 에러
          throw new Error("Inner transaction failed");
        });
      } catch (error) {
        console.log("Inner transaction rolled back");
        // 내부 트랜잭션만 롤백, 외부는 계속 진행
      }
      
      // 3. 외부 트랜잭션 계속 (커밋됨)
      await trx1
        .table("users")
        .where("id", userId)
        .update({ bio: "Updated after inner rollback" });
    });
  }
}
중첩 트랜잭션의 장점:
  • 부분 작업의 독립적 롤백
  • 복잡한 비즈니스 로직 구조화
  • 에러 격리 가능

명시적 롤백

rollback() 호출

class OrderModel extends BaseModelClass {
  async createOrderWithValidation(
    userId: number,
    items: OrderItem[]
  ): Promise<number | null> {
    const wdb = this.getPuri("w");
    
    return wdb.transaction(async (trx) => {
      // 1. 주문 생성
      const [order] = await trx
        .table("orders")
        .insert({ user_id: userId, status: "pending" })
        .returning({ id: "id" });
      
      // 2. 유효성 검사
      const totalAmount = await this.calculateTotal(items);
      
      if (totalAmount > 1000000) {
        // 명시적 롤백
        await trx.rollback();
        return null;
      }
      
      // 3. 주문 상세 저장
      for (const item of items) {
        await trx
          .table("order_items")
          .insert({
            order_id: order.id,
            product_id: item.productId,
            quantity: item.quantity,
          });
      }
      
      return order.id;
    });
  }
  
  private async calculateTotal(items: OrderItem[]): Promise<number> {
    // 총액 계산 로직
    return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }
}
rollback() 호출 후에도 코드는 계속 실행됩니다. 명시적으로 return하거나 throw해야 합니다.

트랜잭션 격리 수준

Isolation Level 설정

class UserModel extends BaseModelClass {
  async updateWithIsolation(userId: number): Promise<void> {
    const wdb = this.getPuri("w");
    
    // PostgreSQL/MySQL 모두 지원
    return wdb.transaction(
      async (trx) => {
        const user = await trx
          .table("users")
          .where("id", userId)
          .first();
        
        if (!user) {
          throw new Error("User not found");
        }
        
        await trx
          .table("users")
          .where("id", userId)
          .update({ last_login_at: new Date() });
      },
      { isolationLevel: "repeatable read" }
    );
  }
}

격리 수준별 특징

LevelDirty ReadNon-repeatable ReadPhantom Read성능
READ UNCOMMITTEDOOO⭐⭐⭐⭐⭐
READ COMMITTEDXOO⭐⭐⭐⭐
REPEATABLE READXXO⭐⭐⭐
SERIALIZABLEXXX⭐⭐
격리 수준 선택 가이드:
  • 일반 조회/수정: READ COMMITTED
  • 일관성 중요: REPEATABLE READ (기본값)
  • 금융 거래: SERIALIZABLE

에러 처리

Try-Catch 패턴

class UserModel extends BaseModelClass {
  async createUserSafely(data: UserSaveParams): Promise<{
    success: boolean;
    userId?: number;
    error?: string;
  }> {
    const wdb = this.getPuri("w");
    
    try {
      const userId = await wdb.transaction(async (trx) => {
        // 중복 체크
        const existing = await trx
          .table("users")
          .where("email", data.email)
          .first();
        
        if (existing) {
          throw new Error("Email already exists");
        }
        
        // User 생성
        const [user] = await trx
          .table("users")
          .insert(data)
          .returning({ id: "id" });
        
        return user.id;
      });
      
      return { success: true, userId };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : "Unknown error",
      };
    }
  }
}

트랜잭션 내 에러 처리

class OrderModel extends BaseModelClass {
  async processOrder(orderId: number): Promise<void> {
    const wdb = this.getPuri("w");
    
    return wdb.transaction(async (trx) => {
      // 1. 주문 조회
      const order = await trx
        .table("orders")
        .where("id", orderId)
        .first();
      
      if (!order) {
        throw new Error("Order not found");
      }
      
      // 2. 상태 검증
      if (order.status !== "pending") {
        throw new Error(`Cannot process order with status: ${order.status}`);
      }
      
      try {
        // 3. 결제 처리 (외부 API)
        await this.processPayment(order);
      } catch (error) {
        // 결제 실패 시 주문 상태 업데이트
        await trx
          .table("orders")
          .where("id", orderId)
          .update({ status: "failed" });
        
        // 에러 재전파 (트랜잭션 롤백)
        throw error;
      }
      
      // 4. 주문 완료
      await trx
        .table("orders")
        .where("id", orderId)
        .update({ status: "completed" });
    });
  }
  
  private async processPayment(order: any): Promise<void> {
    // 결제 로직
  }
}

트랜잭션 디버깅

로깅

class UserModel extends BaseModelClass {
  async createUserWithLogging(data: UserSaveParams): Promise<number> {
    const wdb = this.getPuri("w");
    
    console.log("[TRX] Starting transaction");
    
    return wdb.transaction(async (trx) => {
      console.log("[TRX] Inside transaction");
      
      const [userId] = await trx
        .table("users")
        .insert(data)
        .returning({ id: "id" });
      
      console.log("[TRX] User created:", userId);
      
      // 트랜잭션 상태 확인 (디버그용)
      await trx.debugTransaction();
      
      console.log("[TRX] Committing transaction");
      return userId.id;
    }).then(
      (result) => {
        console.log("[TRX] Transaction committed successfully");
        return result;
      },
      (error) => {
        console.log("[TRX] Transaction rolled back:", error.message);
        throw error;
      }
    );
  }
}

성능 최적화

트랜잭션 최소화

// ❌ 나쁨: 긴 트랜잭션
async badPattern(userId: number): Promise<void> {
  const wdb = this.getPuri("w");
  
  return wdb.transaction(async (trx) => {
    // 불필요한 조회 (트랜잭션 밖에서 가능)
    const user = await trx.table("users").where("id", userId).first();
    
    // 외부 API 호출 (트랜잭션 안에서 불필요)
    await this.sendEmail(user.email);
    
    // 실제 업데이트
    await trx.table("users").where("id", userId).update({ last_login_at: new Date() });
  });
}

// ✅ 좋음: 짧은 트랜잭션
async goodPattern(userId: number): Promise<void> {
  const rdb = this.getPuri("r");
  const wdb = this.getPuri("w");
  
  // 1. 조회 (READ DB, 트랜잭션 밖)
  const user = await rdb.table("users").where("id", userId).first();
  
  if (!user) {
    throw new Error("User not found");
  }
  
  // 2. 외부 작업 (트랜잭션 밖)
  await this.sendEmail(user.email);
  
  // 3. 업데이트 (짧은 트랜잭션)
  return wdb.transaction(async (trx) => {
    await trx.table("users").where("id", userId).update({ last_login_at: new Date() });
  });
}
트랜잭션 최적화 원칙:
  • 트랜잭션은 최대한 짧게
  • 외부 API 호출은 트랜잭션 밖에서
  • 읽기 전용 조회는 트랜잭션 불필요
  • 락 대기 시간 최소화

@transactional vs 수동 트랜잭션

비교

특징@transactional수동 transaction()
코드 간결성⭐⭐⭐⭐⭐⭐⭐⭐
세밀한 제어⭐⭐⭐⭐⭐⭐⭐
부분 트랜잭션
조건부 롤백⭐⭐⭐⭐⭐⭐⭐
디버깅⭐⭐⭐⭐⭐⭐⭐
재사용성⭐⭐⭐⭐⭐⭐⭐⭐

선택 가이드

class UserModel extends BaseModelClass {
  // ✅ @transactional 사용 - 단순한 경우
  @transactional()
  async simpleCreate(data: UserSaveParams): Promise<number> {
    const wdb = this.getPuri("w");
    wdb.ubRegister("users", data);
    const [id] = await wdb.ubUpsert("users");
    return id;
  }
  
  // ✅ 수동 transaction - 복잡한 제어
  async complexCreate(data: UserSaveParams): Promise<number | null> {
    const wdb = this.getPuri("w");
    
    return wdb.transaction(async (trx) => {
      // 조건부 작업
      if (await this.shouldCreateUser(data)) {
        const [id] = await trx.table("users").insert(data).returning({ id: "id" });
        return id.id;
      }
      
      // 조건 불충족 시 롤백
      await trx.rollback();
      return null;
    });
  }
}

다음 단계