Skip to the content.

DistributedLockPort 가이드

Application Layer의 Cross-cutting 분산락 포트 인터페이스 가이드

1. 개요

1.1 위치 및 역할

application/
└── common/
    └── port/out/
        └── DistributedLockPort.java    # Cross-cutting concern

특징:

1.2 CommandPort/QueryPort와의 차이

구분 CommandPort/QueryPort DistributedLockPort
위치 {BC}/port/out/ common/port/out/
범위 Bounded Context 내부 전체 애플리케이션 공유
용도 도메인 데이터 접근 동시성 제어

2. 인터페이스 정의

package com.ryuqq.application.common.port.out;

import com.ryuqq.domain.common.vo.LockKey;
import java.util.concurrent.TimeUnit;

public interface DistributedLockPort {

    /**
     * 분산락 획득 시도
     *
     * @param key       Lock 키 (도메인 특화 LockKey 구현체)
     * @param waitTime  최대 대기 시간
     * @param leaseTime Lock 유지 시간
     * @param unit      시간 단위
     * @return Lock 획득 성공 여부
     */
    boolean tryLock(LockKey key, long waitTime, long leaseTime, TimeUnit unit);

    /**
     * 분산락 해제
     */
    void unlock(LockKey key);

    /**
     * 현재 스레드가 Lock을 보유 중인지 확인
     */
    boolean isHeldByCurrentThread(LockKey key);

    /**
     * Lock 상태 확인 (어떤 스레드든)
     */
    boolean isLocked(LockKey key);
}

3. LockKey VO 패턴

3.1 기반 인터페이스

// domain/common/vo/LockKey.java
public interface LockKey {

    /**
     * Redis Lock Key 값 반환
     * 형식: lock:{domain}:{entity}:{id}
     */
    String value();
}

3.2 도메인 특화 구현

각 Bounded Context에서 자신만의 LockKey를 구현합니다.

// domain/order/vo/OrderLockKey.java
public record OrderLockKey(Long orderId) implements LockKey {

    private static final String PREFIX = "lock:order:";

    public OrderLockKey {
        if (orderId == null || orderId <= 0) {
            throw new IllegalArgumentException("orderId must be positive");
        }
    }

    @Override
    public String value() {
        return PREFIX + orderId;
    }
}
// domain/stock/vo/StockLockKey.java
public record StockLockKey(Long productId) implements LockKey {

    private static final String PREFIX = "lock:stock:item:";

    public StockLockKey {
        if (productId == null || productId <= 0) {
            throw new IllegalArgumentException("productId must be positive");
        }
    }

    @Override
    public String value() {
        return PREFIX + productId;
    }
}
// 복합 키 예시
public record StockWarehouseLockKey(
    Long productId,
    Long warehouseId
) implements LockKey {

    @Override
    public String value() {
        return "lock:stock:" + productId + ":warehouse:" + warehouseId;
    }
}

4. UseCase에서 사용

4.1 기본 패턴

@Component
public class PlaceOrderUseCase {

    private final DistributedLockPort lockPort;
    private final OrderCommandPort orderCommandPort;

    public void execute(Long orderId) {
        OrderLockKey lockKey = new OrderLockKey(orderId);

        boolean acquired = lockPort.tryLock(lockKey, 10, 30, TimeUnit.SECONDS);

        if (!acquired) {
            throw new LockAcquisitionFailedException("주문 락 획득 실패: " + orderId);
        }

        try {
            // 비즈니스 로직
            orderCommandPort.save(order);
        } finally {
            lockPort.unlock(lockKey);
        }
    }
}

4.2 권장 파라미터

작업 유형 waitTime leaseTime 예시
빠른 작업 5초 10초 재고 차감
일반 작업 10초 30초 주문 생성
무거운 작업 30초 60초 대량 데이터 처리

5. 금지 사항

5.1 Adapter에서 금지

// ❌ 금지: @Transactional
@Component
@Transactional  // 금지!
public class DistributedLockAdapter implements DistributedLockPort {
    // Lock은 트랜잭션과 별개로 동작
}

// ❌ 금지: 비즈니스 로직
public boolean tryLock(LockKey key, ...) {
    if (isVipOrder(key)) {  // 금지! 비즈니스 로직
        // ...
    }
}

// ❌ 금지: DB 접근
public boolean tryLock(LockKey key, ...) {
    orderRepository.findById(...);  // 금지! DB 접근
}

5.2 UseCase에서 금지

// ❌ 금지: Lock 안에서 외부 API 호출
lockPort.tryLock(lockKey, ...);
try {
    externalApiClient.call();  // 위험! Lock 시간 초과 가능
} finally {
    lockPort.unlock(lockKey);
}

// ✅ 권장: 외부 호출 후 Lock
ExternalResult result = externalApiClient.call();  // Lock 밖에서
lockPort.tryLock(lockKey, ...);
try {
    processWithResult(result);
} finally {
    lockPort.unlock(lockKey);
}

6. 키 네이밍 컨벤션

6.1 형식

lock:{domain}:{id}
lock:{domain}:{entity}:{id}
lock:{domain}:{entity}:{id}:{sub-entity}:{sub-id}

6.2 예시

도메인 키 형식 예시
주문 lock:order:{orderId} lock:order:123
재고 lock:stock:item:{productId} lock:stock:item:456
결제 lock:payment:{paymentId} lock:payment:789
재고+창고 lock:stock:{productId}:warehouse:{warehouseId} lock:stock:456:warehouse:1

7. DomainEvent와의 유사성

항목 DomainEvent LockKey
기반 interface DomainEvent interface LockKey
위치 domain/common/event/ domain/common/vo/
구현체 OrderPlacedEvent OrderLockKey
구현체 위치 domain/{bc}/event/ domain/{bc}/vo/
패턴 상속 상속

8. 체크리스트

Port 구현 시

LockKey 구현 시

UseCase 사용 시