메인 콘텐츠로 건너뛰기
@transactional 데코레이터는 메서드를 자동으로 트랜잭션으로 감싸주어 보일러플레이트 코드를 줄여줍니다.

데코레이터 개요

자동 트랜잭션

메서드 실행을 자동으로 트랜잭션으로 감쌈성공 시 커밋, 실패 시 롤백

코드 간결화

transaction() 호출 불필요가독성 향상

Isolation Level

트랜잭션 격리 수준 설정동시성 제어

중첩 트랜잭션

자동 컨텍스트 공유Savepoint 지원

기본 사용법

Before: 수동 트랜잭션

class UserModel extends BaseModelClass {
  async createUser(data: UserSaveParams): Promise<number> {
    const wdb = this.getPuri("w");
    
    // ❌ 매번 transaction() 호출 필요
    return wdb.transaction(async (trx) => {
      trx.ubRegister("users", data);
      const [userId] = await trx.ubUpsert("users");
      return userId;
    });
  }
}

After: @transactional 데코레이터

class UserModel extends BaseModelClass {
  @transactional()
  async createUser(data: UserSaveParams): Promise<number> {
    const wdb = this.getPuri("w");
    
    // ✅ transaction() 호출 없이 바로 작업
    wdb.ubRegister("users", data);
    const [userId] = await wdb.ubUpsert("users");
    return userId;
  }
}
@transactional() 데코레이터가 적용된 메서드는 자동으로 트랜잭션 내에서 실행됩니다.

작동 원리

데코레이터 옵션

dbPreset 설정

class UserModel extends BaseModelClass {
  // 기본값: dbPreset = "w" (write DB)
  @transactional()
  async method1() {
    const wdb = this.getPuri("w"); // write DB
    // ...
  }
  
  // 명시적 지정
  @transactional({ dbPreset: "w" })
  async method2() {
    const wdb = this.getPuri("w");
    // ...
  }
}

Isolation Level 설정

class UserModel extends BaseModelClass {
  // READ UNCOMMITTED
  @transactional({ isolation: "read uncommitted" })
  async readUncommitted() {
    const wdb = this.getPuri("w");
    // 커밋되지 않은 데이터 읽기 가능 (Dirty Read)
  }
  
  // READ COMMITTED
  @transactional({ isolation: "read committed" })
  async readCommitted() {
    const wdb = this.getPuri("w");
    // 커밋된 데이터만 읽기 (Non-repeatable Read 가능)
  }
  
  // REPEATABLE READ (기본값)
  @transactional({ isolation: "repeatable read" })
  async repeatableRead() {
    const wdb = this.getPuri("w");
    // 같은 트랜잭션 내에서 동일한 결과 보장
  }
  
  // SERIALIZABLE
  @transactional({ isolation: "serializable" })
  async serializable() {
    const wdb = this.getPuri("w");
    // 가장 엄격한 격리 수준 (Phantom Read 방지)
  }
}
Isolation Level 주의사항:
  • 높은 격리 수준은 동시성 감소
  • SERIALIZABLE은 성능 영향 큼
  • 대부분의 경우 REPEATABLE READ가 적절

실전 예제

예제 1: 단순 트랜잭션

class UserModel extends BaseModelClass {
  @transactional()
  async createMultipleUsers(
    users: UserSaveParams[]
  ): Promise<number[]> {
    const wdb = this.getPuri("w");
    
    // 여러 User 등록
    users.forEach((user) => {
      wdb.ubRegister("users", user);
    });
    
    // 일괄 저장
    const ids = await wdb.ubUpsert("users");
    return ids;
  }
}

// 사용
const ids = await UserModel.createMultipleUsers([
  { email: "[email protected]", username: "user1", ... },
  { email: "[email protected]", username: "user2", ... },
]);

예제 2: 자동 롤백

class UserModel extends BaseModelClass {
  @transactional()
  async createUserWithValidation(
    data: UserSaveParams
  ): Promise<number> {
    const wdb = this.getPuri("w");
    
    // 중복 체크
    const existing = await wdb
      .table("users")
      .where("email", data.email)
      .first();
    
    if (existing) {
      // 에러 발생 → 자동 롤백
      throw new Error("Email already exists");
    }
    
    // User 생성
    wdb.ubRegister("users", data);
    const [userId] = await wdb.ubUpsert("users");
    
    return userId;
  }
}

예제 3: 복잡한 트랜잭션 (Company → Dept → Employee)

class CompanyModel extends BaseModelClass {
  @transactional({ isolation: "serializable" })
  async createOrganization(data: {
    companyName: string;
    departmentName: string;
    employees: Array<{
      email: string;
      username: string;
      salary: string;
    }>;
  }): Promise<{
    companyId: number;
    departmentId: number;
    employeeIds: number[];
  }> {
    const wdb = this.getPuri("w");
    
    // 1. Company
    const companyRef = wdb.ubRegister("companies", {
      name: data.companyName,
    });
    
    // 2. Department
    const deptRef = wdb.ubRegister("departments", {
      name: data.departmentName,
      company_id: companyRef,
    });
    
    // 3. Users & Employees
    data.employees.forEach((emp) => {
      const userRef = wdb.ubRegister("users", {
        email: emp.email,
        username: emp.username,
        password: "hashed",
        role: "normal",
      });
      
      wdb.ubRegister("employees", {
        user_id: userRef,
        department_id: deptRef,
        employee_number: `E${Date.now()}`,
        salary: emp.salary,
      });
    });
    
    // 4. 순서대로 저장
    const [companyId] = await wdb.ubUpsert("companies");
    const [departmentId] = await wdb.ubUpsert("departments");
    await wdb.ubUpsert("users");
    const employeeIds = await wdb.ubUpsert("employees");
    
    return { companyId, departmentId, employeeIds };
  }
}

예제 4: 동시성 제어

class UserModel extends BaseModelClass {
  @transactional({ isolation: "repeatable read" })
  async updateLastLogin(userId: number): Promise<void> {
    const wdb = this.getPuri("w");
    
    // 1. 현재 로그인 시간 조회
    const user = await wdb
      .table("users")
      .select({ last_login: "last_login_at" })
      .where("id", userId)
      .first();
    
    if (!user) {
      throw new Error("User not found");
    }
    
    // 2. 로그인 시간 업데이트
    await wdb
      .table("users")
      .where("id", userId)
      .update({
        last_login_at: new Date(),
      });
    
    // Repeatable Read: 다른 트랜잭션의 변경 영향 없음
  }
}

중첩 트랜잭션

자동 컨텍스트 공유

class UserModel extends BaseModelClass {
  @transactional()
  async createUserWithProfile(
    userData: UserSaveParams,
    bio: string
  ): Promise<number> {
    const wdb = this.getPuri("w");
    
    // User 생성
    const [userId] = await wdb
      .table("users")
      .insert(userData)
      .returning("id");
    
    // 다른 @transactional 메서드 호출
    // 같은 트랜잭션 컨텍스트 공유
    await this.updateBio(userId, bio);
    
    return userId;
  }
  
  @transactional()
  async updateBio(userId: number, bio: string): Promise<void> {
    const wdb = this.getPuri("w");
    
    await wdb
      .table("users")
      .where("id", userId)
      .update({ bio });
  }
}
중첩 트랜잭션의 이점:
  • 코드 재사용성 향상
  • 메서드 조합 자유로움
  • 트랜잭션 경계 자동 관리

@api와 함께 사용

데코레이터 조합

class UserController extends BaseModelClass {
  // 두 데코레이터를 함께 사용 가능
  @api({ httpMethod: "POST" })
  @transactional()
  async createUser(
    params: UserSaveParams
  ): Promise<{ userId: number }> {
    const wdb = this.getPuri("w");
    
    wdb.ubRegister("users", params);
    const [userId] = await wdb.ubUpsert("users");
    
    return { userId };
  }
  
  // 순서는 상관없음
  @transactional()
  @api({ httpMethod: "PUT" })
  async updateUser(
    id: number,
    params: Partial<UserSaveParams>
  ): Promise<void> {
    const wdb = this.getPuri("w");
    
    await wdb
      .table("users")
      .where("id", id)
      .update(params);
  }
}

장단점

장점

코드 간결화

transaction() 호출 불필요보일러플레이트 제거

가독성

트랜잭션 경계 명확비즈니스 로직에 집중

자동 관리

커밋/롤백 자동 처리실수 방지

재사용성

메서드 조합 용이중첩 트랜잭션 지원

단점

제약사항

메서드 레벨에서만 사용부분 트랜잭션 불가

디버깅

트랜잭션 경계가 숨겨짐문제 추적 어려울 수 있음

사용 가이드

언제 사용하나?

사용 권장:
  • 메서드 전체가 하나의 트랜잭션
  • 여러 DB 작업이 원자성 필요
  • 코드 재사용이 중요한 경우
  • API 핸들러 메서드
사용 비권장:
  • 메서드 내 부분적 트랜잭션
  • 복잡한 트랜잭션 제어 필요
  • 트랜잭션 경계가 명확해야 하는 경우

패턴 비교

// Pattern 1: @transactional (권장)
@transactional()
async createUser(data: UserSaveParams): Promise<number> {
  const wdb = this.getPuri("w");
  wdb.ubRegister("users", data);
  const [id] = await wdb.ubUpsert("users");
  return id;
}

// Pattern 2: 수동 transaction (복잡한 경우)
async createUserComplex(data: UserSaveParams): Promise<number> {
  const wdb = this.getPuri("w");
  
  return wdb.transaction(async (trx) => {
    // 복잡한 트랜잭션 로직
    const [id] = await trx.table("users").insert(data).returning("id");
    
    // 조건부 롤백
    if (someCondition) {
      await trx.rollback();
    }
    
    return id;
  });
}

주의사항

반드시 지켜야 할 사항:
  1. 메서드는 async 함수여야 함
  2. this.getPuri()로 DB 접근
  3. 에러는 throw로 전파 (자동 롤백)
  4. 중첩 트랜잭션은 같은 컨텍스트 공유

흔한 실수

class UserModel extends BaseModelClass {
  // ❌ 잘못된 사용
  @transactional()
  async wrongUsage1(data: UserSaveParams): Promise<number> {
    // getPuri를 다른 변수에 저장하지 말 것
    const db = this.getPuri("w");
    
    // 에러를 catch로 숨기면 롤백 안 됨
    try {
      db.ubRegister("users", data);
      const [id] = await db.ubUpsert("users");
      return id;
    } catch (error) {
      console.error(error);
      return 0; // ❌ 에러를 숨김 → 롤백 안 됨
    }
  }
  
  // ✅ 올바른 사용
  @transactional()
  async correctUsage(data: UserSaveParams): Promise<number> {
    const wdb = this.getPuri("w");
    
    // 에러는 throw로 전파
    if (!data.email) {
      throw new Error("Email is required");
    }
    
    wdb.ubRegister("users", data);
    const [id] = await wdb.ubUpsert("users");
    return id;
  }
}

다음 단계