Skip to content

트랜잭션 컨트롤러

Fox Transactions 에서 트랜잭션 옵션에 따라 새로운 트랜잭션을 시작하거나 기존 트랜잭션 참여하며(enlist) 트랜잭션 메서드들의 수행 결과에 따라 트랜잭션을 커밋 혹은 롤백하는 등 트랜잭션을 관리하는 주체를 트랜잭션 컨트롤러라고 부릅니다. Fox Transactions 는 기본적으로 3종류의 트랜잭션 컨트롤러를 제공합니다. 이 문서에서는 이들 트랜잭션 컨트롤러의 특징과 사용법 등을 설명합니다.

이 문서와 관련된 예제 코드는 다음을 참조 하십시요.

Overview

Fox Transactions 에서 트랜잭션 컨트롤러는 트랜잭션 메서드 호출 전 수행하는 전처리와 호출 후에 수행하는 후처리의 한 단계로서 트랜잭션을 처리합니다. 트랜잭션 컨트롤러는 IFoxTransactionControl 인터페이스를 구현하는 객체로서 Fox Transactions 가 트랜잭션 처리를 위해 전처리/후처리 과정에서 이 인터페이스의 BeginTransaction, CommitTransaction, RollbackTransaction 등의 메서드들을 호출합니다.

IFoxTransactionControl 인터페이스의 메서드들을 구현할 때 어떤 트랜잭션 라이브러리를 사용하는가에 따라 몇몇 종류로 나누어 집니다. FoxFastTransactinoControllerFoxTransactionScopeControllerSystem.Transactions 네임스페이스에서 제공하는 CommitableTransaction, DependentTransaction, TrasactionScope 등의 클래스를 사용하여 구현되었으며 FoxLocalTransactionController 는 ADO.NET 에서 제공하는 트랜잭션 API 만을 사용하여 구현되었습니다. 필요하다면 IFoxTransactionControl 인터페이스를 구현하는 커스텀 트랜잭션 컨트롤러를 구현하여 사용하는 것도 가능합니다.

FoxTransactionController 특성

트랜잭션 컨트롤러는 트랜잭션 클래스/메서드 수준에서 FoxTransactionControllerAttribute 특성을 사용하여 선택이 가능합니다. FoxTransactionAttributeFoxAutoCompleteAttribute 특성과 마찬가지로 FoxTransactionControllerAttribute 특성도 파생 클래스에 상속될 수 있으며 클래스 수준의 특성은 메서드 수준의 특성에 오버라이드 됩니다.

[FoxTransactionController(FoxTransactionControllerKind.LocalTransaction)]
public class BizClass : FoxBizBase, IBizClass
{
    public void Method1()
    {
        // 로컬 트랜잭션 컨트롤러 적용됨
    }

    [FoxTransactionController(FoxTransactionControllerKind.FastTransaction)]]
    public void Method2()
    {
        // Fast 트랜잭션 컨트롤러 적용됨
    }

}

트랜잭션 컨트롤러의 종류는 FoxTransactionControllerKind 열거 타입으로 다음과 같습니다.

  • Default

    디폴트 트랜잭션 컨트롤러를 사용하도록 합니다. 디폴트 트랜잭션 컨트롤러는 FastTransaction 입니다.

  • FastTransaction

    System.Transactions 네임스페이스의 CommitableTransactionDependentTransaction 클래스를 직접 사용하여 트랜잭션을 제어하는 컨트롤러입니다. Fox Transactions 에서 FoxTransactionControllerAttribute 특성이 명시되지 않았을 때 기본적으로 사용되는 디폴트 트랜잭션 컨트롤러 입니다.

  • TransactionScope

    System.Transactions 네임스페이스의 TransactionScope 클래스를 사용하여 트랜잭션을 제어하는 컨트롤러입니다.

  • LocalTransaction

    ADO.NET 의 트랜잭션 제어 API 를 사용하여 로컬 트랜잭션을 사용하는 트랜잭션 컨트롤러 입니다.

  • Custom

    IFoxTransactionControl 인터페이스를 구현하는 커스텀 트랜잭션 컨트롤러 타입을 사용하는 트랜잭션 컨트롤러 입니다.

  • RootContext

    트랜잭션 루트의 트랜잭션 컨트롤러를 사용합니다.

트랜잭션 컨트롤러 호환성

트랜잭션 전파 상황에서 두 트랜잭션 메서드의 트랜잭션 컨트롤러는 동일해야 합니다. 예를 들어, 비즈니스 로직 클래스는 FastTransaction 을 사용하지만 DAC 클래스는 LocalTransaction 을 사용하는 경우 비즈니스 로직 클래스는 DAC 클래스의 메서드를 호출할 수 없습니다. 다음 코드에서 InsertData 메서드 호출은 트랜잭션 컨트롤러가 호환되지 않는다는 메시지와 함께 InvalidOperationException 예외가 발생합니다.

[FoxTransactionController(FoxTransactionControllerKind.FastTransaction)]
public class BizClass : FoxBizBase, IBizClass
{
    public void SomeBizLogic()
    {
        using DacClass dac = new();
        IDacClass itf = dac.CreateExecution<IDacClass>();
        itf.InsertData();   // 컨트롤러 호환이 안 되므로 예외 발생
        ......
    }
}

[FoxTransactinoController(FoxTransactionControllerKind.LocalTransaction)]
Public class DacClass : FoxDacBase, IDacClass
{
    public void InsertData(...)
    {
        ......
    }
}

이러한 이유에서 FoxDacBase 클래스는 호출자, 즉 트랜잭션 루트의 트랜잭션 컨트롤러를 사용하도록 RootContext 값을 사용하여 다음과 같이 선언되어 있습니다.

1
2
3
4
5
6
[FoxTransaction(FoxTransactionOption.Supported)]
[FoxTransactionController(FoxTransactionControllerKind.RootContext)]
public abstract class FoxDacBase : FoxComponentBase
{
    ......
}

Fast Transaction Controller

Fast 트랜잭션 컨트롤러는 Fox Transactions 에서 제공하는 디폴트 트랜잭션 컨트롤러 입니다. FoxTransactionControllerAttribute 특성에 FoxTransactionControllerKind.Default 값이 사용되었거나 심지어 FoxTransactionControllerAttribute 특성이 명시되지 않았더라도 트랜잭션 컨트롤러로 Fast 트랜잭션 컨트롤러가 사용됩니다. Fast 트랜잭션 컨트롤러는 실제로 FoxFastTransactionController 클래스에 의해 구현되며 내부적으로 사용됩니다.

FoxFastTransactionController 클래스는 System.Transcations 네임스페이스의 CommitableTransactionDependentTransaction 클래스를 사용하여 트랜잭션을 제어합니다. System.Transactions 네임스페이스의 API 를 사용하기 때문에 System.Transactions 네임스페이스가 제공하는 트랜잭션의 특성을 그대로 갖습니다. 즉, 로컬 트랜잭션으로 트랜잭션을 시작해서 필요하다면 분산 트랜잭션으로 승급을 수행합니다.

Info

Fast 트랜잭션 컨트롤러의 이름에 Fast 가 붙은 이유는 TransactionScope 클래스가 내부적으로 사용하는 CommittableTransaction, DependentTransaction 클래스를 직접적으로 사용하여 TransactionScope 트랜잭션 컨트롤러에 비해 성능이 우수하기 때문입니다.

Transaction Scope Controller

TransactionScope 트랜잭션 컨트롤러는 FoxTransactionScopeController 클래스에 의해 구현되는 트랜잭션 컨트롤러 입니다. 이 컨트롤러는 FoxFastTransactionController 클래스와 유사하게 System.Transactions 네임스페이스의 트랜잭션 API 를 사용하여 트랜잭션을 제어합니다. 하지만 FoxTransactionScopeController 클래스는 저수준 API 대신 TransactionScope 클래스를 직접 사용합니다. FoxFastTransactionController 보다 오버헤드가 조금 더 높지만 TransactionScope 클래스를 사용하는 다른 코드들과 상호운영성은 조금 더 좋습니다.

System.Transactions.TransactionScope 클래스와 상호 운영성을 높이기 위한 목적이 아니라면 FoxFastTransactionController 를 사용하기를 권장합니다.

Local Transaction Controller

로컬 트랜잭션 컨트롤러는 FoxLocalTransactionController 클래스에 의해 구현되는 트랜잭션 컨트롤러로서 Fox Transactions 에서 제공하는 강력한 기능 중 하나입니다. 분산 트랜잭션은 트랜잭션 처리에서 요구되는 복잡한 시나리오를 매우 간단하게 만들어 줍니다. 하지만 Windows의 MSDTC와 같은 트랜잭션 관리자에 의존하므로 설정이 복잡하고 Linux나 대부분의 클라우드 환경에서는 사용이 불가능합니다. 한편 로컬 트랜잭션은 성능이 우수하고 대부분의 환경에서 사용가능하지만 개발자가 복잡한 코드를 작성해야 한다는 단점이 있습니다.

FoxLocalTransactionController 클래스를 사용하면 분산 트랜잭션을 사용하는 것처럼 간단하게 코드를 작성할 수 있지만 내부적으로 로컬 트랜잭션이 사용됩니다. 분산 트랜잭션의 장점과 로컬 트랜잭션의 장점을 모두 취할 수 있지만 몇 가지 제약 사항을 갖습니다.

작동 원리

로컬 트랜잭션 컨트롤러의 작동 원리를 이해하면 사용 방법과 이 컨트롤러의 제약 사항을 쉽게 이해할 수 있습니다. 로컬 트랜잭션의 한계점에서 언급한 대로 여러 메서드들이 참여하는 트랜잭션을 로컬 트랜잭션으로 처리하려면 트랜잭션에 참여하는 메서드들이 하나의 Connection 객체 혹은 FoxDbAccess 객체를 공유해야 합니다. 로컬 트랜잭션 컨트롤러의 주요 기능이 바로 트랜잭션에 참여하는 메서드들이 하나의 FoxDbAccess 객체를 사용하도록 조정하는 것입니다.

로컬 트랜잭션 컨트롤러가 작동하기 위해서는 클래스는 FoxComponentBase 에서 파생되어야 할 뿐만 아니라 IFoxLocalTransactionControl 인터페이스를 구현해야 합니다. FoxLocalTransactionController 클래스는 IFoxLocalTransactionControl 인터페이스를 통해 FoxDbAccess 객체를 생성하고 트랜잭션에 참여한 메서드들이 생성된 하나의 FoxDbAccess 객체가 공유되도록 제어합니다.

FoxDacBase 클래스는 IFoxLocalTransactionControl 인터페이스를 구현하여 로컬 트랜잭션 컨트롤러가 제공하는 단일 FoxDbAccess 객체가 DbAccess 속성으로 설정되도록 제어합니다. 즉 FoxDacBase 클래스는 CreateDbInstance 메서드를 호출하지 않고 로컬 트랜잭션 컨트롤러가 제공하는 FoxDbAccess 객체를 DbAccess 속성값으로 설정합니다. FoxDacBase 에서 파생된 클래스가 DbAccess 속성을 사용하여 데이터베이스를 액세스한다면 로컬 트랜잭션 컨트롤러가 생성한 로컬 트랜잭션을 사용하여 트랜잭션을 처리하게 되는 것입니다.

로컬 트랜잭션 컨트롤러 작동 원리

트랜잭션 컨트롤러의 예제 코드 에서 비즈니스 로직 클래스(BizClass)의 InsertMany 메서드가 반복적으로 DAC 클래스(DacClass)의 InsertMemo 를 호출합니다.

public class BizClass : FoxBizBase, IBizClass
{
    public List<int> InsertMany(Memo[] memos)
    {
        Console.WriteLine($"Transaction controller: {this.Context.TransactionControllerType}");
        using DacClass dac = new();
        IDacClass itf = dac.CreateExecution<IDacClass>();
        List<int> newIds = [];
        foreach (Memo memo in memos)
        {
            int id = itf.InsertMemo(memo);
            newIds.Add(id);
        }
        return newIds;
    }
}

public class DacClass : FoxDacBase, IDacClass
{
    public int InsertMemo(Memo memo)
    {
        Console.WriteLine($"  DbAccess instance id = {this.DbAccess.GetHashCode()}");
        object? result = this.DbAccess.ExecuteQueryScalar("memo.insert", memo)
            ?? throw new InvalidOperationException("InsertMemo failed: result is null.");
        int newId = Convert.ToInt32(result);
        return newId;
    }
}

위 코드를 수행하면 다음과 같은 결과가 출력됩니다. DAC 클래스를 호출할 때마다 DbAccess 속성이 새로운 FoxDbAccess 인스턴스로 초기화 됨에 주목하십시요.

1
2
3
4
Transaction controller: NeoDEEX.Transactions.Common.FoxFastTransactionController
  DbAccess instance id = 20234383
  DbAccess instance id = 44115416
  DbAccess instance id = 16578980

한편, BizClass 가 로컬 트랜잭션 컨트롤러를 사용하도록 FoxTransactionControllerAttribute 특성을 다음과 같이 적용하면,

1
2
3
4
5
[FoxTransactionController(FoxTransactionControllerKind.LocalTransaction)]
public class BizClass : FoxBizBase, IBizClass
{
    ......
}

수행 결과는 다음과 같이 출력됩니다. 여러 호출에도 불구하고 하나의 FoxDbAccess 인스턴스가 공유되어 DbAccess 속성값으로 사용됨에 주목하십시요.

1
2
3
4
Transaction controller: NeoDEEX.Transactions.Common.FoxLocalTransactionController
  DbAccess instance id = 58328727
  DbAccess instance id = 58328727
  DbAccess instance id = 58328727

제약 사항

로컬 트랜잭션 컨트롤러가 Fox Transactions 의 선언적/자동 트랜잭션 기능을 정상적으로 작동하도록 하려면 다른 트랜잭션 컨트롤러에 비해 다음 제약 사항을 고려해야 합니다.

  • FoxDacBase 클래스의 DbAccess 속성을 사용하여 수행한 데이터베이스 액세스만이 트랜잭션에 포함됩니다.

    로컬 트랜잭션 컨트롤러의 작동 원리에서 언급한 대로 하나의 FoxDbAccess 객체를 통해 데이터베이스에 접근함으로써 로컬 트랜잭션이 수행되기 때문에 FoxDacBase.DbAccess 속성이 아닌 새로운 FoxDbAccess 객체나 Connection 객체를 통해 수행되는 데이터 액세스는 트랜잭션에 포함되지 않습니다. 따라서 다음 예제 코드에서 두번째 INSERT 는 트랜잭션이 실패하더라도 롤백되지 않습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void BadDbAccess()
    {
        Memo memo_will_be_rollbacked = new() { ...... };
        this.DbAccess.ExecuteQueryNonQuery("memo.insert", memo_will_be_rollbacked);
    
        Memo memo_will_not_be_rollbacked = new() { ...... };
        FoxDbAccess NonTransactionalDbAccess = FoxDbAccess.CreateDbAccess();
        NonTransactionalDbAccess.ExecuteQueryNonQuery("memo.insert", memo_will_not_be_rollbacked);
    }
    

    Note

    FoxDacBase 클래스가 아닌 IFoxLocalTransactionObject 인터페이스를 구현하는 다른 클래스를 사용하는 경우, IFoxLocalTransactionObject.Enlist 메서드가 호출될 때 전달되는 FoxDbAccess 객체를 사용해야 로컬 트랜잭션을 사용할 수 있습니다.

  • 트랜잭션에 사용되는 연결 문자열은 하나 뿐이며 트랜잭션에 참여하는 첫번째 DAC 클래스가 사용하는 연결 문자열입니다.

    로컬 트랜잭션 컨트롤러가 사용될 때 FoxDacBase 에서 파생된 클래스가 CreateDbInstance 메서드를 전혀 호출하지 않는 것은 아닙니다. 로컬 트랜잭션 컨트롤러는 트랜잭션이 시작되면 IFoxLocalTransactionObject 인터페이스의 CreateDbInstance 메서드를 호출합니다. 비즈니스 로직 클래스는 이 메서드에서 null 을 반환하여 FoxDbAccess 객체 생성을 지연(deferred) 시킵니다. 그리고 트랜잭션 내에서 최초로 호출되는 FoxDacBase 클래스는 이 메서드가 호출되면 자신의 가상 메서드인 CreateDbInstance 메서드를 다시 호출하여 FoxDbAccess 객체를 생성합니다.

    이 이후 호출되는 IFoxLocalTransactionObject 인터페이스의 Enlist 메서드는 CreateDbInstance 메서드가 반환했던 FoxDbAccess 객체를 매개변수로 전달하므로 이 객체를 DbAccess 속성값으로 사용합니다.

  • FoxDacBase.DbAccess 속성에 대해 Open/Close/BeginTrans/CommitTrans/RollbackTrans 메서드 호출은 오류를 유발할 수 있습니다.

    로컬 트랜잭션은 단일 데이터베이스 연결 내에서 트랜잭션 API 를 사용하여 트랜잭션을 관리합니다. 따라서 데이터베이스 연결을 열고(open) 트랜잭션을 시작하고 커밋 혹은 롤백하는 작업은 로컬 트랜잭션 컨트롤러가 수행해야 합니다. 개별 메서드에서 이러한 작업을 수행해서는 안됩니다.

    로컬 트랜잭션 컨트롤러는 FoxDbAccess 객체가 이러한 작업들을 수행하지 않도록 잠금(내부 IsLocked 속성) 설정을 수행합니다. 잠긴 FoxDbAccess 객체는 Open/Close/BeginTrans/CommitTrans/RollbackTrans 메서드가 호출되면 InvalidOperationException 예외를 발생합니다.

    잠긴 FoxDbAccessInvalidOperationException 예외를 발생시키지 않고 단순히 호출을 무시하도록 설정할 수도 있습니다. FoxTransactionControllerAttribute 특성에서 ThrowExceptionOnLocked 속성을 false 로 지정하면 됩니다. ThrowExceptionOnLocked 속성을 false 로 설정하면 로컬 트랜잭션 컨트롤러와 다른 컨트롤러를 모두 지원하는 DAC 클래스를 만들때 유용하게 사용할 수 있습니다.

    1
    2
    3
    4
    5
    6
    [FoxTransactionController(FoxTransactionControllerKind.LocalTransaction, 
            ThrowExceptionOnLocked = false)]
    public class BizClass : FoxBizBase, IBizClass
    {
        .......
    }
    

    Warning

    ThrowExceptionOnLocked 속성 설정은 트랜잭션 루트에서 트랜잭션 컨트롤러를 지정하는 것과 함께 이루어져야 합니다.

Custom Transaction Controller

커스텀 트랜잭션 컨트롤러는 IFoxTransactionControl 인터페이스를 구현하는 추가적인 트랜잭션 컨트롤러를 작성하고 사용할 수 있도록 해줍니다.

1
2
3
4
public CustomController : IFoxTransactionControl
{
    ......
}

작성된 커스텀 트랜잭션 컨트롤러는 다음과 같이 FoxTransactionControllerAttribute 특성에 Custom 값과 타입을 명시하여 트랜잭션에 적용시킬 수 있습니다.

1
2
3
4
5
6
[FoxTransactionController(FoxTransactionControllerKind.Custom,
                CustomControllerType = typeof(CustomController))]
public class BizClass : FoxBizBase, IBizClass
{
    .......
}

Info

써드 파티 트랜잭션 API 사용과 같은 특별한 경우가 아니라면 커스텀 트랜잭션 컨트롤러 구현이 필요한 상황은 발생하지 않습니다. 커스텀 트랜잭션 컨트롤러의 작성은 트랜잭션 처리와 관련되기 때문에 매우 복잡하며 많은 테스트를 요구하는 조심스러운 구현이 필요하므로 기술 지원을 받는 것이 좋습니다.

Summary

Fox Transactions 는 상황에 따라 선택 가능한 몇 가지 트랜잭션 컨트롤러를 제공합니다. Fast 트랜잭션 컨트롤러는 System.Transactions 네임스페이스가 제공하는 API 를 활용하는 디폴트 트랜잭션 컨트롤러입니다. 환경 트랜잭션(ambient transaction) 및 트랜잭션 승급, 분산 트랜잭션 기능을 통해 매우 간단하고 편리한 트랜잭션 환경을 제공하기 때문에 대부분의 상황에서 Fast 트랜잭션 컨트롤러를 사용하길 권장합니다.

한편 Linux 환경이나 클라우드 환경에서는 분산 트랜잭션을 사용할 수 없기 때문에 System.Transactions 네임스페이스 기반의 트랜잭션을 사용하기 어렵습니다. 이러한 경우, 일부 제약 사항을 준수하면 로컬 트랜잭션 컨트롤러를 사용하여 편리하고 생산성 높은 선언적 자동 트랜잭션을 그대로 사용할 수 있습니다.