

# DynamoDB 잠금 클라이언트를 사용한 분산 잠금
<a name="BestPractices_DistributedLocking"></a>

기존 잠금-획득-릴리스 의미 체계가 필요한 애플리케이션의 경우 DynamoDB 잠금 클라이언트는 DynamoDB 테이블을 잠금 저장소로 사용하여 분산 잠금을 구현하는 오픈 소스 라이브러리입니다. 이 접근 방식은 여러 애플리케이션 인스턴스에서 외부 리소스(예: S3 객체 또는 공유 구성)에 대한 액세스를 조정해야 할 때 유용합니다.

잠금 클라이언트는 오픈 소스 [Java 라이브러리](https://github.com/awslabs/amazon-dynamodb-lock-client)로 사용할 수 있습니다.

## 작동 방식
<a name="BestPractices_DistributedLocking_HowItWorks"></a>

잠금 클라이언트는 전용 DynamoDB 테이블을 사용하여 잠금을 추적합니다. 각 잠금은 다음과 같은 키 속성을 가진 항목으로 표시됩니다.
+ 잠기는 리소스를 식별하는 파티션 키입니다.
+ 잠금의 유효 기간을 지정하는 리스 기간입니다. 잠금 홀더가 충돌하거나 응답하지 않을 경우 리스 기간이 지나면 잠금이 자동으로 만료됩니다.
+ 잠금 소유자가 리스를 연장하기 위해 주기적으로 보내는 하트비트입니다. 이렇게 하면 소유자가 아직 능동적으로 처리 중인 동안 잠금이 만료되지 않습니다.

잠금 클라이언트는 조건부 쓰기를 사용하여 한 번에 하나의 프로세스만 잠금을 획득할 수 있도록 합니다. 잠금이 이미 보류된 경우 호출자는 대기했다가 다시 시도하거나 즉시 실패하도록 선택할 수 있습니다.

## 잠금 클라이언트를 사용해야 하는 경우
<a name="BestPractices_DistributedLocking_WhenToUse"></a>

잠금 클라이언트는 다음과 같은 경우에 적합합니다.
+ 여러 애플리케이션 인스턴스 또는 마이크로서비스에서 공유 리소스에 대한 액세스를 조정해야 합니다.
+ 중요한 섹션은 장기 실행(초\~분)이며 충돌 시 전체 작업을 재시도하는 데 비용이 많이 듭니다.
+ 프로세스 실패를 정상적으로 처리하려면 자동 잠금 만료가 필요합니다.

일반적인 예로는 분산 워크플로 오케스트레이션, 여러 인스턴스에서 CRON 작업 조정, 공유 외부 리소스에 대한 액세스 관리 등이 있습니다.

## 단점
<a name="BestPractices_DistributedLocking_Tradeoffs"></a>

**추가 인프라**  
잠금 작업을 위한 추가 읽기 및 쓰기 용량과 함께 잠금 관리를 위한 전용 DynamoDB 테이블이 필요합니다.

**클럭 종속성**  
잠금 만료는 타임스탬프에 의존합니다. 클라이언트 간의 상당한 클럭 스큐로 인해 특히 짧은 리스 기간 동안 예기치 않은 동작이 발생할 수 있습니다.

**교착 상태 위험**  
애플리케이션이 여러 리소스에 대한 잠금을 획득하는 경우 교착 상태를 방지하려면 일관된 순서로 획득해야 합니다. 리스 기간은 응답하지 않는 소유자로부터 잠금을 자동으로 해제하여 안전망을 제공합니다.

## 구현
<a name="BestPractices_DistributedLocking_Implementation"></a>

다음 예제에서는 DynamoDB 잠금 클라이언트를 사용하여 잠금을 획득하고 해제하는 방법을 보여줍니다.

```
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;

final DynamoDbClient dynamoDB = DynamoDbClient.builder()
    .region(Region.US_WEST_2)
    .build();

final AmazonDynamoDBLockClient lockClient = new AmazonDynamoDBLockClient(
    AmazonDynamoDBLockClientOptions.builder(dynamoDB, "Locks")
        .withTimeUnit(TimeUnit.SECONDS)
        .withLeaseDuration(10L)
        .withHeartbeatPeriod(3L)
        .withCreateHeartbeatBackgroundThread(true)
        .build());

// Try to acquire a lock on a resource
final Optional<LockItem> lock =
    lockClient.tryAcquireLock(AcquireLockOptions.builder("my-shared-resource").build());

if (lock.isPresent()) {
    try {
        // Perform operations that require exclusive access
        processSharedResource();
    } finally {
        // Always release the lock when done
        lockClient.releaseLock(lock.get());
    }
} else {
    System.out.println("Failed to acquire lock.");
}

lockClient.close();
```

**중요**  
처리 로직에서 예외가 발생하더라도 잠금이 해제되도록 항상 `finally` 블록에서 잠금을 해제합니다. 릴리스되지 않은 잠금은 리스가 만료될 때까지 다른 프로세스를 차단합니다.

조건부 쓰기를 직접 사용하여 잠금 클라이언트 라이브러리 없이 간단한 잠금 메커니즘을 구현할 수도 있습니다. 다음 예제에서는 `UpdateItem`을 조건 표현식과 함께 사용하여 잠금을 획득하고 `DeleteItem`으로 해제합니다.

```
from datetime import datetime, timedelta
from boto3.dynamodb.conditions import Attr

def acquire_lock(table, resource_name, owner_id, ttl_seconds):
    """Attempt to acquire a lock. Returns True if successful."""
    expiry = (datetime.now() + timedelta(seconds=ttl_seconds)).isoformat()
    now = datetime.now().isoformat()
    try:
        table.update_item(
            Key={'LockID': resource_name},
            UpdateExpression='SET #owner = :owner, #expiry = :expiry',
            ConditionExpression=Attr('LockID').not_exists() | Attr('ExpiresAt').lt(now),
            ExpressionAttributeNames={'#owner': 'OwnerID', '#expiry': 'ExpiresAt'},
            ExpressionAttributeValues={':owner': owner_id, ':expiry': expiry}
        )
        return True
    except table.meta.client.exceptions.ConditionalCheckFailedException:
        return False

def release_lock(table, resource_name, owner_id):
    """Release a lock. Only succeeds if the caller is the lock owner."""
    try:
        table.delete_item(
            Key={'LockID': resource_name},
            ConditionExpression=Attr('OwnerID').eq(owner_id)
        )
        return True
    except table.meta.client.exceptions.ConditionalCheckFailedException:
        return False
```

이 접근 방식은 조건 표현식을 사용하여 잠금이 존재하지 않거나 만료된 경우에만 잠금을 획득할 수 있고, 잠금을 획득한 프로세스에서만 잠금을 해제할 수 있도록 합니다. 잠금 테이블에서 [TTL(Time to Live)](TTL.md)을 활성화하여 만료된 잠금 항목을 자동으로 정리하는 것이 좋습니다.