낙관적 동시 실행 제어 (OCC)는 여러 사용자 또는 프로세스가 동시에 동일한 리소스를 수정하려고 할 때 공유 리소스를 관리하고 '업데이트 손실' 또는 경합 상태를 방지하는 데 사용되는 전략입니다.
예를 들어 공유 리소스가 리소스
(예: 프로젝트, 버킷 또는 서비스)에 적용되는 IAM 정책인 Google Cloud IAM과 같은 시스템을 생각해 보세요. OCC를 구현하기 위해 시스템은 일반적으로 리소스 객체에 버전 번호 또는 etag (엔티티 태그) 필드를 사용합니다.
OCC 소개
두 프로세스 A와 B가 동시에 공유 리소스를 업데이트하려고 한다고 가정해 보겠습니다.
프로세스 A 가 리소스의 현재 상태를 읽습니다.
프로세스 B 가 동일한 현재 상태를 읽습니다.
프로세스 A 가 사본을 수정하고 서버에 다시 씁니다.
프로세스 B 가 사본을 수정하고 서버에 다시 씁니다.
프로세스 B가 프로세스 A가 이미 변경했다는 것을 모른 채 리소스를 덮어쓰기 때문에 프로세스 A의 업데이트가 손실됩니다.
OCC는 엔티티가 수정될 때마다 변경되는 고유한 지문을 도입하여 이 문제를 해결합니다. 많은 시스템 (예: IAM)에서 etag를 사용하여 이 작업을 실행합니다. 서버는 모든 쓰기에서 이 태그를 확인합니다.
리소스를 읽으면 서버에서
etag(고유한 지문)를 반환합니다.수정된 리소스를 다시 전송할 때는 원래
etag를 포함해야 합니다.서버에서 저장된
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;
}
}