Projects/hub-eleven

[동시성 처리] 재고 감소 락 추상화 및 Redisson 락 구현 (아키텍처 구조 보완 필요)

annovation 2026. 6. 2. 23:00

아키텍처 구조

💡현재 프로젝트 구조

  •  

💡인터페이스 구현 구조

application service
    ↓
application port(interface)
    ↑
infrastructure adapter(implementation)
  • Redisson에 대한 직접 의존을 application service에서 분리하기 위해, application 계층에 port 인터페이스를 두고 infrastructure 계층에서 구현하도록 설계했다.
  • (왜 인터페이스는 application 계층에 두고, 구현체는 infrastructure 에 두는 걸까?)
  • 아 어렵넹

Lock 추상화 인터페이스 StockLockManager 구현

 

💡StockLockManager.java

package com.hubEleven.stock.application.port;

import java.util.function.Supplier;

public interface StockLockManager {

	<T> T executeWithLock(String lockKey, Supplier<T> supplier);
}
  • lockKey : 어떤 작업에 락을 걸지 구분하는 키
  • Supplier<T> : 락을 잡은 뒤 실행할 실제 작업
  • <T> : 작업 결과 타입을 유연하게 받기 위한 제네릭

💡코드 작동 예시

"이 lockKey로 락을 잡고,
락을 잡은 동안 supplier 안의 재고 감소 작업을 실행해줘"

 

💡RedissonStockLockManager.java

package com.hubEleven.stock.infrastructure.lock;

import static com.hubEleven.stock.domain.exception.StockErrorCode.STOCK_LOCK_TIMEOUT;

import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

import com.commonLib.common.exception.GlobalException;
import com.hubEleven.stock.application.port.StockLockManager;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class RedissonStockLockManager implements StockLockManager {

	private static final long WAIT_TIME_SECONDS = 3L;
	private static final long LEASE_TIME_SECONDS = 5L;

	private final RedissonClient redissonClient;

	@Override
	public <T> T executeWithLock(String lockKey, Supplier<T> supplier) {
		RLock lock = redissonClient.getLock(lockKey);
		boolean locked = false;

		try {
			locked = lock.tryLock(WAIT_TIME_SECONDS, LEASE_TIME_SECONDS, TimeUnit.SECONDS);
			if (!locked) {
				throw new GlobalException(STOCK_LOCK_TIMEOUT);
			}

			return supplier.get();
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
			throw new GlobalException(STOCK_LOCK_TIMEOUT);
		} finally {
			if (locked && lock.isHeldByCurrentThread()) {
				lock.unlock();
			}
		}
	}
}

 

이 코드를 하나 씩 살펴보면,

 

1. Redis에서 lockKey 이름의 자물쇠를 가져온다.

RLock lock = redissonClient.getLock(lockKey);
  • 해당 lockKey 는 서비스 코드에서 생성되며, 재고 감소하려는 상품 ID (productId) 를 조합하여 생성한다.
  • tryLock 시도 후 이 lockKey 가 겹쳤을 때, 동시성 제어를 하게된다.

2. 최대 3초 동안 자물쇠를 잡으려고 기다리고, 잡으면 5초 동안 락이 유지된다.

locked = lock.tryLock(3, 5, TimeUnit.SECONDS);

 

3. 락을 획득한 뒤, 실제 작업을 실행한다.

return supplier.get();

 

4. 작업 성공, 실패 여부와 상관 없이 마지막에는 락을 해체한다.

finally {
    if (locked && lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

 

💡Redisson 분산 락 기반 재고 감소 동시성 제어 흐름

 

💡RLock 이 뭐지?

 

💡그래서 supplier.get() 은 무슨 작업을 하는거지?

 


Questions

 

1️⃣ 왜 StockServiceImpl이 RedissonClient나 RLock을 직접 쓰지 않고, StockLockManager 같은 인터페이스를 거치게 만드는 게 좋을까?

  • StockServiceImpl이 RedissonClient나 RLock을 직접 쓰면 서비스 코드가 Redisson과의 의존성이 강해진다.
  • 예를 들어 서비스 코드에 아래와 같이 RLock 을 직접 사용하게 되면,
@Service
public class StockServiceImpl {

    private final RedissonClient redissonClient;

    public void decrease() {
        RLock lock = redissonClient.getLock("stock");

        lock.lock();

        try {
            // 재고 감소
        } finally {
            lock.unlock();
        }
    }
}
  • 락 방식을 변경하고자 할 때, 서비스 코드를 직접 수정해야한다.
  • 즉, 하위 기술 변경이 상위 비즈니스 코드까지 연쇄적으로 수정되게 되고 유지보수가 어려워진다.
  • 따라서 StockLockManager 와 같은 락 추상화 인터페이스를 활용해 해결할 수 있다.
public interface StockLockManager {
    <T> T executeWithLock(String lockKey, Supplier<T> supplier);
}
  • 인터페이스 구현체 예시
StockLockManager
├─ RedissonStockLockManager
├─ DatabaseStockLockManager
└─ InMemoryStockLockManager

 

2️⃣ 왜 executeWithLock(String lockKey, Supplier<T> supplier) 구조에서 락을 잡는 코드락 안에서 실행할 실제 작업을 분리해서 넘길까?


참고 자료

1. Java Docs : Supplier

https://docs.oracle.com/javase/8/docs/api/java/util/function/Supplier.html

 

Supplier (Java Platform SE 8 )

Represents a supplier of results. There is no requirement that a new or distinct result be returned each time the supplier is invoked. This is a functional interface whose functional method is get().

docs.oracle.com

2. Redisson Docs : Redisson Locks and Synchronizers

https://redisson.pro/docs/data-and-services/locks-and-synchronizers/index.html

 

Locks and synchronizers - Redisson Reference Guide

Locks and synchronizers Lock Valkey or Redis based distributed reentrant Lock object for Java and implements Lock interface. Uses pub/sub channel to notify other threads across all Redisson instances waiting to acquire a lock. If Redisson instance which ac

redisson.pro

반응형