최적의 동시 실행 제어 (OCC)

낙관적 동시 실행 제어 (OCC)는 여러 사용자 또는 프로세스가 동시에 동일한 리소스를 수정하려고 할 때 공유 리소스를 관리하고 '업데이트 손실' 또는 경합 상태를 방지하는 데 사용되는 전략입니다.

예를 들어 공유 리소스가 리소스 (예: 프로젝트, 버킷 또는 서비스)에 적용되는 IAM 정책인 Google Cloud IAM과 같은 시스템을 생각해 보세요. OCC를 구현하기 위해 시스템은 일반적으로 리소스 객체에 버전 번호 또는 etag (엔티티 태그) 필드를 사용합니다.

OCC 소개

두 프로세스 A와 B가 동시에 공유 리소스를 업데이트하려고 한다고 가정해 보겠습니다.

  1. 프로세스 A 가 리소스의 현재 상태를 읽습니다.

  2. 프로세스 B동일한 현재 상태를 읽습니다.

  3. 프로세스 A 가 사본을 수정하고 서버에 다시 씁니다.

  4. 프로세스 B 가 사본을 수정하고 서버에 다시 씁니다.

프로세스 B가 프로세스 A가 이미 변경했다는 것을 모른 채 리소스를 덮어쓰기 때문에 프로세스 A의 업데이트가 손실됩니다.

OCC는 엔티티가 수정될 때마다 변경되는 고유한 지문을 도입하여 이 문제를 해결합니다. 많은 시스템 (예: IAM)에서 etag를 사용하여 이 작업을 실행합니다. 서버는 모든 쓰기에서 이 태그를 확인합니다.

  1. 리소스를 읽으면 서버에서 etag (고유한 지문)를 반환합니다.

  2. 수정된 리소스를 다시 전송할 때는 원래 etag를 포함해야 합니다.

  3. 서버에서 저장된 etag가 전송한 etag와 일치하지 않는 경우 (즉, 읽은 후 다른 사용자가 리소스를 수정함) 쓰기 작업이 ABORTED 또는 FAILED_PRECONDITION 오류와 함께 실패합니다.

이 실패로 인해 클라이언트는 전체 프로세스를 재시도 해야 합니다. 즉, 상태를 다시 읽고, 변경사항을 다시 적용하고, 새 etag로 쓰기를 다시 시도합니다.

OCC 루프 구현

OCC 구현의 핵심은 재시도 로직을 처리하는 while 루프입니다. 경합이 심한 경우 무한 루프를 방지하기 위해 적절한 최대 재시도 횟수를 설정합니다.

루프 단계

단계 작업 구현 예
읽기 etag를 포함한 현재 리소스 상태를 가져옵니다. Policy policy = client.getIamPolicy(resourceName);
수정 변경사항을 로컬 객체에 적용합니다. policy = policy.toBuilder().addBinding(newBinding).build();
쓰기/확인 이전 etag를 사용하여 수정된 리소스를 저장하려고 시도합니다. 이 작업은 try 블록 내에 있어야 합니다. try { client.setIamPolicy(resourceName, policy); return policy; } catch (AbortedException e) { // retry loop }
성공/재시도 쓰기가 성공하면 루프를 종료합니다. 동시 실행 오류로 인해 실패하면 재시도 카운터를 늘리고 루프를 계속합니다 (읽기 단계로 돌아감).

다음 파일은 프로젝트 리소스의 IAM 정책을 타겟으로 사용하여 OCC 루프를 구현하는 방법을 보여주는 실행 가능한 예를 제공합니다.

설치

이 예를 사용하려면 pom.xml에 다음 종속 항목을 추가하세요.

<dependency>
  <groupId>com.google.cloud</groupId>
  <artifactId>google-cloud-resourcemanager</artifactId>
  <version>1.45.0</version>
</dependency>

import com.google.api.gax.rpc.AbortedException;
import com.google.api.gax.rpc.FailedPreconditionException;
import com.google.cloud.resourcemanager.v3.ProjectName;
import com.google.cloud.resourcemanager.v3.ProjectsClient;
import com.google.iam.v1.Binding;
import com.google.iam.v1.GetIamPolicyRequest;
import com.google.iam.v1.Policy;
import com.google.iam.v1.SetIamPolicyRequest;
import java.util.ArrayList;
import java.util.List;

public class IamOccExample {

    /**
     * Executes an Optimistic Concurrency Control (OCC) loop to safely update a resource.
     *
     * This method demonstrates the core Read-Modify-Write-Retry pattern.
     *
     * @param projectId  The Google Cloud Project ID (e.g., "my-project-123").
     * @param role       The IAM role to grant (e.g., "roles/storage.objectAdmin").
     * @param member     The member to add (e.g., "user:user@example.com").
     * @param maxRetries The maximum number of times to retry the update.
     * @return The successfully updated IAM policy (or null on failure).
     */
    public static Policy updateIamPolicyWithOcc(
            String projectId,
            String role,
            String member,
            int maxRetries
    ) throws Exception {
        // Setup Client
        try (ProjectsClient projectsClient = ProjectsClient.create()) {
            String projectName = ProjectName.of(projectId).toString();
            int retries = 0;

            // START OCC LOOP (Read-Modify-Write-Retry)
            while (retries < maxRetries) {
                try {
                    // READ: Get the current policy. This includes the current etag.
                    System.out.printf("Attempt %d: Reading current IAM policy for %s...%n", retries, projectName);
                    GetIamPolicyRequest getIamPolicyRequest = GetIamPolicyRequest.newBuilder()
                            .setResource(projectName)
                            .build();
                    Policy policy = projectsClient.getIamPolicy(getIamPolicyRequest);

                    // MODIFY: Apply the desired changes to the local Policy object.
                    List<Binding> bindings = new ArrayList<>(policy.getBindingsList());
                    Binding targetBinding = null;
                    int bindingIndex = -1;

                    for (int i = 0; i < bindings.size(); i++) {
                        if (bindings.get(i).getRole().equals(role)) {
                            targetBinding = bindings.get(i);
                            bindingIndex = i;
                            break;
                        }
                    }

                    if (targetBinding != null) {
                        if (targetBinding.getMembersList().contains(member)) {
                            System.out.printf("Policy for role %s and member %s exists already!%n", role, member);
                            return policy;
                        }
                        // Create a new binding based on existing one to add the member
                        Binding updatedBinding = targetBinding.toBuilder()
                                .addMembers(member)
                                .build();
                        bindings.set(bindingIndex, updatedBinding);
                    } else {
                        // Role not found, create a new binding
                        Binding newBinding = Binding.newBuilder()
                                .setRole(role)
                                .addMembers(member)
                                .build();
                        bindings.add(newBinding);
                    }

                    // The policy builder now contains the modified bindings AND the original etag.
                    Policy updatedPolicy = policy.toBuilder()
                            .clearBindings()
                            .addAllBindings(bindings)
                            .build();

                    // WRITE/CHECK: Attempt to write the modified policy.
                    System.out.printf("Attempt %d: Setting modified IAM policy...%n", retries);
                    SetIamPolicyRequest setIamPolicyRequest = SetIamPolicyRequest.newBuilder()
                            .setResource(projectName)
                            .setPolicy(updatedPolicy)
                            .build();
                    Policy resultPolicy = projectsClient.setIamPolicy(setIamPolicyRequest);

                    // SUCCESS: If the call succeeds, return the new policy and exit the loop.
                    System.out.printf("Successfully updated IAM policy in attempt %d.%n", retries);
                    return resultPolicy;

                } catch (AbortedException | FailedPreconditionException e) {
                    // If the etag is stale (concurrency conflict), this will throw a retryable exception.
                    retries++;
                    System.out.printf("Concurrency conflict detected (etag mismatch). Retrying... (%d/%d)%n",
                            retries, maxRetries);

                    // Exponential backoff (100ms * retry count)
                    Thread.sleep(100L * retries);
                }
            }
            // END OCC LOOP
        }

        System.out.printf("Failed to update IAM policy after %d attempts due to persistent concurrency conflicts.%n", maxRetries);
        return null;
    }
}