์ ์ฐฉ์ ์ด๋ฒคํธ ์๊ตฌ์ฌํญ
- ์ ์ฐฉ์์ผ๋ก n๋ช ๊น์ง๋ง ๊ธฐํํฐ์ฝ์ ๋ฐ์ ์ ์๋ค.
- ๊ธฐํํฐ์ฝ์ 1์ธ๋น ํ๋ฒ, ์ฆ ์ค๋ณต์ผ๋ก ๋ฐ์ ์๋ ์๋ค.
์ ๋ ๋ถ์ฐ๋ฝ์ ํตํด ๋์์ฑ ์ด์๋ฅผ ํด๊ฒฐํ๊ณ , ์ ์๊ตฌ์ฌํญ์ ์ถฉ์กฑํ๊ธฐ ์ํด Redis
๋ฅผ ํ์ฉํ๊ธฐ๋ก ํ์ต๋๋ค. ๊ทธ ์ด์ ๋, Redis
๋ In-memory DB
๋ก์จ ์๋๊ฐ ๋น ๋ฅด๋ฉฐ, RDB
์์ ๋ฝ์ ์ฌ์ฉํ๊ธฐ ์ํด์๋ ๋ณ๋์ ์ปค๋ฅ์
ํ์ ๊ด๋ฆฌํด์ผ ํ๊ณ , ๋ฝ์ ๊ด๋ จ๋ ๋ถํ๋ฅผ RDB
์์ ๋ฐ๋๋ค๋ ์ ์ด ํจ์จ์ ์ด์ง ์๋ค๊ณ ์๊ฐํ๊ธฐ ๋๋ฌธ์
๋๋ค.
๋ถ์ฐ๋ฝ์ ์ ํํ ์ด์
Redis๋ "์ฑ๊ธ์ค๋ ๋๋ก ๋ช ๋ น์ ์ํํ๋ฏ๋ก, ์ ์ด์ ๋์์ฑ์ด ๋ฐ์ํ ์๊ฐ ์๋ ๊ฑฐ ์๋์ผ?"๋ผ๊ณ ์๊ฐํ ์ ์์ต๋๋ค.
์ผ๋จ ์ง๋ฌธ์ ๋ํ ๋ต์ ๋ง์ต๋๋ค. Redis์ ๋ช
๋ น ์์ฒด๋ ์์์ (atomic)์
๋๋ค. ํ์ง๋ง ์๋์ ๊ฐ์ด ์ฌ๋ฌ ๊ฐ์ ์ฐ์๋ Redis
๋ช
๋ น์ด๋ฅผ ์คํํ ๋ ๊ทธ ์ฌ์ด์ ๋ค๋ฅธ ํด๋ผ์ด์ธํธ์ ๋ช
๋ น์ด ๋ค์ด์ฌ ๊ฒฝ์ฐ๋ฅผ ๊ณ ๋ คํด์ผ ํฉ๋๋ค.
if (redis.get(key) < 100) {
redis.incr(key)
}
์ด ์ฝ๋ ๊ฐ์ ๊ฒฝ์ฐ๋ ๋์์ฑ ๋ฌธ์ ์์ ์์ ํ์ง ์์ต๋๋ค. ๊ฐ์๊ฐ 100๊ฐ ๋ฏธ๋ง์ผ ๋๋ง ์นด์ดํ ๋๋๋ก ์ค๊ณํ์ง๋ง, ๋ง์ ์ค๋ ๋๋ค์ด ๋์์ if๋ฌธ์ ๊ฒ์ฆ ์กฐ๊ฑด์ ํต๊ณผํด์ Redis์ ๊ฐ์ ์ฐ๋ ค๊ณ ํ๋ฉด, 100๊ฐ ์ด์์ ์ด๊ณผ ์ผ์ด์ค๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.
Redis์์๋ ์ด๋ฐ ์ผ์ด์ค๋ฅผ ์์ฝ๊ฒ ํด๊ฒฐํ ์ ์๋๋ก Transaction(WATCH, MULTI, EXEC ๋ฑ)์ ์ ๊ณตํ๋ฉฐ, ์ด๋ฅผ ํตํด ์ฌ๋ฌ ๋จ๊ณ์ ๊ฑธ์น Redis ๋ช ๋ น์ด ์งํ ์ ์ฒด๋ฅผ ํ๋์ ํธ๋์ญ์ ์ผ๋ก ์ฒ๋ฆฌํ์ฌ ์์์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์์ต๋๋ค. ๋ํ ์ด ๋ฐฉ์์ Redis ๋ด๋ถ์์ ์ ๊ณตํ๋ ๊ธฐ๋ฅ์ด๋ฏ๋ก, ์ถ๊ฐ์ ์ธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋๊ตฌ๊ฐ ํ์ํ์ง ์๋ค๋ ์ฅ์ ์ด ์์ต๋๋ค.
๊ทธ๋ฌ๋ ์ด ๋ฐฉ์์ ์ ํํ์ง ์์ ๋ค ๊ฐ์ง ์ด์ ๊ฐ ์๋๋ฐ, ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- Redis ํธ๋์ญ์ ์ Redis ๋ช ๋ น์ด๋ค๋ง ์์์ ์ผ๋ก ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํ๊ธฐ์, ํธ๋์ญ์ ์ค๊ฐ์ Redis ๋ช ๋ น์ด๋ฅผ ์ ์ธํ ์ฝ๋๋ฅผ ์ฝ์ ํ๋ ๊ฒ์ ๋ถ๊ฐ๋ฅํ๋ค. ์ฆ, Redis์ ์ฝ๊ณ ์ฐ๋ ์์ ๋ค๋ง ์์์ ์ผ๋ก ๊ตฌ์ฑํ ์ ์๋ค.
- Cluster Redis ํ๊ฒฝ์์๋ ํธ๋์ญ์ ์ฒ๋ฆฌ๊ฐ ๋ณต์กํด์ง๋ค.
- ์ด ๋ฐฉ์์ Optimistic Locking์ ์ฌ์ฉํ๋ฏ๋ก, ๋ฐ์ดํฐ ๋ณ๊ฒฝ ์ ์ถฉ๋์ด ๋ฐ์ํ๋ฉด ์ฌ์๋ํด์ผ ํ๋ ๋ถ๋ด์ด ์กด์ฌํ๋ค.(๋ฌผ๋ก ๊ตญ๋ด ๋๋ถ๋ถ์ ์๋น์ค ํธ๋ํฝ์ Optimistic Locking ๋ฐฉ์์ผ๋ก ์ถฉ๋ถํ ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํ๋ค๋ ์๊ฒฌ์ด ๋ง๋ค.)
- Redis Transaction ๊ด๋ จํ ์ง์์ด ๋น์ ์์ด, ์คํ๋ฆฐํธ ๊ธฐ๊ฐ ๋ด์ ๊ฐ๋ฐ๊ณผ ๊ฒ์ฆ๊น์ง ์งํํ๊ธฐ ์ํด ํ์ต ๊ฒฝํ์ด ์๋ ๋ถ์ฐ๋ฝ์ ์ ์ฉํ๋ ๊ฒ ๋ ๋์ ์ ํ์ผ๋ก ๋ณด์๋ค.
์ ๋ ๋จ์ํ ์ด๋ฒ ์ ์ฐฉ์ ์ด๋ฒคํธ๋ง์ ์ํ ๋์์ฑ ์ด์๋ฅผ ํด๊ฒฐํ๊ธฐ๋ณด๋ค๋, ๋ค์ํ ๊ธฐํ์์๋ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ ํ ๋๋ฅผ ๋ง๋ จํ๊ณ ์ ํ๊ณ , ์ด์ ์ ํ์ตํ๋ ๊ฒฝํ์ ํตํด ์คํ๋ฆฐํธ ๊ธฐ๊ฐ์ผ์ ์ ๋ง์ถ๊ธฐ ์ํด ๋ค์ค ์ธ์คํด์ค ํ๊ฒฝ์์๋ ๊ณตํต๋ ๋ฝ์ ์ฌ์ฉํ ์ ์๋ ๋ถ์ฐ ๋ฝ์ ๊ตฌํํ๊ธฐ๋ก ํ์ต๋๋ค.
Redis์ Redisson ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ ์ ํ ์ด์
Java์ Redis ํด๋ผ์ด์ธํธ
์๋ Jedis, Lettuce, Redisson ์ด๋ ๊ฒ ํฌ๊ฒ ์ธ ๊ฐ์ง์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์กด์ฌํฉ๋๋ค. ์ธ ๊ฐ์ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ๋ถ ๋ถ์ฐ๋ฝ ๊ตฌํ์ด ๊ฐ๋ฅํ์ง๋ง, ์ ํฌ๋ Redisson์ ์ ์ ํ๋๋ฐ ์ด์ ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- Jedis๋ Lettuce์ ๋นํด ์ฑ๋ฅ์ด ๋งค์ฐ ๋จ์ด์ง๊ธฐ ๋๋ฌธ์ ๊ณ ๋ ค ๋์์ด ์๋์๋ค.
- Lettuce๋ ์คํ๋ฝ ๊ธฐ๋ฐ์ผ๋ก, ์์ฒญ์ด ๋ง์์๋ก Redis์ ๋ถํ๊ฐ ์ปค์ง๊ณ ๊ฐ๋ฐ์๊ฐ retry ๋ก์ง์ ์ง์ ์์ฑํด์ผ ํ๋ค.
- Redisson์ Pub/Sub ๊ธฐ๋ฐ์ผ๋ก Lock ๊ตฌํ์ด ๊ฐ๋ฅํ๊ณ ํ์์์ ๊ธฐ๋ฅ์ด ๊ตฌํ๋์ด ์๋ค.
๋ ์์ธํ ๋ด์ฉ์ด ๊ถ๊ธํ์๋ค๋ฉด ์๋ ๋ด์ฉ์ ์ฐธ๊ณ ํด ๋ณด์๊ธธ ์ถ์ฒ๋๋ฆฝ๋๋ค.
- https://thediscreetprogrammer.com/comparing
- https://hyperconnect.github.io/2019/11/15/redis-distributed-lock-1.html
๋ถ์ฐ ๋ฝ ๊ตฌํ
AOP๋ฅผ ์ฌ์ฉํ์ฌ ๊ฐ๋จํ๊ฒ ์ด๋
ธํ
์ด์
๋ง ๋ถ์ด๋ฉด, ๋ฉ์๋ ์คํ ์ ํ์ ๋ถ์ฐ ๋ฝ
์ด ์ฒ๋ฆฌ๋๋๋ก ํ๋ ๋ก์ง์ ๊ตฌํํ์ต๋๋ค.
DistributedLock.java
/**
* ๋ถ์ฐ ์ ๊ธ์ ์ํ ์ด๋
ธํ
์ด์
. ํด๋น ์ด๋
ธํ
์ด์
์ ๋ฉ์๋์ ์ ์ฉํ๋ฉด
* ํด๋น ๋ฉ์๋ ์คํ ์ ๋ถ์ฐ ์ ๊ธ์ด ์ ์ฉ๋๋ค.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DistributedLock {
/**
* ์ ๊ธ ํค๋ฅผ ์ง์ ํ๋ค. Spring EL ํํ์์ ์ฌ์ฉํ์ฌ ๋์ ์ธ ๊ฐ์ ์ง์ ํ๋ค.
* ์: "#paramName"
*
* @return ์ ๊ธ ํค ๋ฌธ์์ด
*/
String key() default "";
/**
* ์ ๊ธ์ ํ๋ํ๊ธฐ ์ํด ๋๊ธฐํ ์ต๋ ์๊ฐ(์ด)์ ์ง์ .
*
* @return ๋๊ธฐ ์๊ฐ (๋จ์: ์ด)
*/
long waitTime() default 5;
/**
* ์ ๊ธ์ ๋ณด์ ํ ์ต๋ ์๊ฐ(์ด)์ ์ง์ .
*
* @return ๋ณด์ ์๊ฐ (๋จ์: ์ด)
*/
long leaseTime() default 1;
}
@DistributedLock
์ด๋
ธํ
์ด์
์ ์ ์ํ์ฌ, ์ด๋
ธํ
์ด์
๋ง ๋ถ์ด๋ฉด ๋ฉ์๋์ ๋ถ์ฐ๋ฝ์ ์ ์ฉํ ์ ์๋๋ก ๋ง๋ค์์ต๋๋ค.
DistributedLockAop.java
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class DistributedLockAop {
private static final String REDISSON_LOCK_PREFIX = "LOCK:";
private final RedissonClient redissonClient;
/**
* @DistributedLock ์ด๋
ธํ
์ด์
์ด ๋ถ์ ๋ฉ์๋๋ฅผ ๋์์ผ๋ก ํ๋ ์ด๋๋ฐ์ด์ค.
* ํด๋น ๋ฉ์๋๋ฅผ ์คํํ๊ธฐ ์ ์ ๋ถ์ฐ ์ ๊ธ์ ์๋ํ๊ณ , ์คํ ํ ์ ๊ธ์ ํด์ ํ๋ค.
*/
@Around("@annotation(distributedLock)")
public Object lock(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// ์ ๊ธ ํค ์์ฑ: ๊ณ ์ ๋ ์ ๋์ฌ + ๋์ EL ํํ์ ๊ฐ
String key = REDISSON_LOCK_PREFIX + CustomSpringELParser.getDynamicValue(signature.getParameterNames(), joinPoint.getArgs(), distributedLock.key());
RLock lock = redissonClient.getLock(key);
try {
// ์ ๊ธ ์๋: ์ง์ ๋ ๋๊ธฐ ์๊ฐ ๋์ ์ ๊ธ์ ํ๋ํ๋ ค๊ณ ์๋
if (!lock.tryLock(distributedLock.waitTime(), distributedLock.leaseTime(), TimeUnit.SECONDS)) {
log.info("Failed to acquire lock.");
return null;
}
// ์ ๊ธ ํ๋ ์ฑ๊ณต ์, ๋์ ๋ฉ์๋ ์คํ
return joinPoint.proceed();
} finally {
// ํ์ฌ ์ค๋ ๋๊ฐ ์ ๊ธ์ ๋ณด์ ํ๊ณ ์๋์ง ํ์ธ ํ ์ ๊ธ ํด์
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
DistributedLockAop
ํด๋์ค๋ @DistrubutedLock
์ด๋
ธํ
์ด์
์ ํ์ฉํ์ฌ ํน์ ๋ฉ์๋์ ๋ถ์ฐ ๋ฝ์ ์ ์ฉํ๋ AOP ๋ก์ง์ ์ ๊ณตํฉ๋๋ค. @DistrubutedLock
์ด๋
ธํ
์ด์
์ด ๋ถ์ ๋ฉ์๋๊ฐ ํธ์ถ๋ ๋, ์ด AOP ๋ก์ง์ด ์๋ํ์ฌ Redisson
์ ์ฌ์ฉํ Redis ๋ถ์ฐ ๋ฝ
๋ฉ์ปค๋์ฆ์ด ์ ์ฉ๋ฉ๋๋ค.
- @DistributedLock ์ด๋ ธํ ์ด์ ์ด ๋ถ์ ๋ฉ์๋๊ฐ ํธ์ถ๋๋ฉด DistributedLockAop์ lock ๋ฉ์๋๊ฐ ์คํ๋๋ค.
- ํด๋น ๋ฉ์๋๋ ๋จผ์ ๋์ ์ ๊ธ ํค๋ฅผ ์์ฑํ๋ฉฐ, ์ด๋ @DistributedLock์ Key ๊ฐ์ SpEL๋ก ํ๊ฐํ์ฌ ์ฌ์ฉํ๋ค.
- ์ด ๋์ ํค๋ก Redisson์ RLock ๊ฐ์ฒด๋ฅผ ์ป์ด์จ๋ค.
- ์ง์ ๋ ์๊ฐ(waitTime) ๋์ ์ ๊ธ์ ์๋ํ๋ค. ์ฑ๊ณตํ๋ฉด ๋ฉ์๋๋ฅผ ์คํํ๊ณ , ์คํจํ๋ฉด null์ ๋ฐํํ๋ค.
- ๋ฉ์๋ ์คํ ํ, ํ์ฌ ์ค๋ ๋๊ฐ ์ ๊ธ์ ๋ณด์ ํ๊ณ ์์ผ๋ฉด ์ ๊ธ์ ํด์ ํ๋ค.
๋ํ RedissonClient ์ธํฐํ์ด์ค์๋ ๋ค์ํ ์ข ๋ฅ์ ๋ถ์ฐ ์ ๊ธ์ ์ ๊ณตํ๋๋ฐ, Redisson์์ ์ฌ์ฉํ ์ ์๋ ์ ๊ธ ๋งค์ปค๋์ฆ์ ์ข ๋ฅ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค. - ๊ณต์ ๋ฌธ์
- ๊ธฐ๋ณธ ์ ๊ธ(getLock): ์ฌ์ง์ ์ด ๊ฐ๋ฅํ ๊ธฐ๋ณธ ์ ๊ธ์ด๋ค. ์ฌ์ฉ์ด ๊ฐ๋จํ์ง๋ง ๊ณต์ ์ฑ์ ๋ณด์ฅํ์ง ์์, ๊ธฐ์ ์ํ๊ฐ ๋ฐ์ํ ์ ์๋ค.
- ๊ณต์ ์ ๊ธ(getFairLock): ๋๊ธฐ ์์์ ๋ฐ๋ผ ์ ๊ธ์ ํ๋ํ๋ ๊ณต์ ํ ๋ฐฉ์์ด๋ค. ๊ณต์ ์ฑ์ ์ ๊ณตํ์ง๋ง ์ฑ๋ฅ ์ค๋ฒํค๋๊ฐ ๋ฐ์ํ ์ ์๋ค.
- ์ฝ๊ธฐ-์ฐ๊ธฐ ์ ๊ธ(getReadWriteLock): ์ฝ๊ธฐ์ ์ฐ๊ธฐ ์์ ์ ๋ํด ๋ค๋ฅธ ์ ๊ธ์ ์ ๊ณตํ๋ค. ์ฝ๊ธฐ ์์ ์ ๋ณ๋ ฌ์ฑ์ ํ์ฉํ์ง๋ง ์ฐ๊ธฐ ์ ๊ธ์ ๋ํ ๊ฒฝ์์ด ์ฑ๋ฅ ์ ํ๋ฅผ ์ผ์ผํฌ ์ ์๋ค.
- ๋ค์ค ์ ๊ธ(getMultiLock): ์ฌ๋ฌ ์ ๊ธ์ ํ๋๋ก ๋ฌถ์ด์ ๊ด๋ฆฌํ๋ค. ์ฌ๋ฌ ๋ฆฌ์์ค์ ๋ํด ๋์ ์ ๊ทผ์ด ๊ฐ๋ฅํ์ง๋ง, ํ๋๋ผ๋ ํ๋ ์คํจ ์ ์ ์ฒด ์์ ์ด ์คํจํ๋ค.
- ๋ ๋๋ฝ(getRedLock): ์ฌ๋ฌ ๋ ๋์ค ์ธ์คํด์ค์ ๊ฑธ์ณ์ ์ ๊ธ์ ๊ด๋ฆฌํ๋ค. ๋์ ๊ฐ์ฉ์ฑ๊ณผ ์์ ์ฑ์ ์ ๊ณตํ์ง๋ง, ๊ทธ์ ๋ฐ๋ฅธ ๊ตฌํ ๋ณต์ก์ฑ๊ณผ ๋คํธ์ํฌ ์ง์ฐ์ ์ํฅ์ ๋ฐ์ ์ ์๋ค.
getLock()์ ๋ชจ๋ Redisson ์ธ์คํด์ค์ ๋๊ธฐ ์ค์ธ ๋ค๋ฅธ ์ค๋ ๋์ ์๋ฆผ์ ๋ณด๋ด๋ ๋ฐฉ์์ ์ฌ์ฉํ์ฌ ์ ๊ธ์ ํ๋ํฉ๋๋ค. ์ด๋ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํจ๊ณผ์ ์ผ๋ก ํด๊ฒฐํ์ง๋ง, ์๋ฆผ์ด ๋์์ ์ฌ๋ฌ ์ค๋ ๋์ ์ ์ก๋๋ฏ๋ก ์ ์ฐฉ์์ ์๋ฒฝํ๊ฒ ๋ณด์ฅํ์ง๋ ๋ชปํฉ๋๋ค. ๋ฐ๋ผ์ ์์ฒญ ์์์ ๋ฐ๋ฅธ ๋์์ด ์ค์ํ๋ค๋ฉด ๋๊ธฐ ์์์ ๋ฐ๋ผ ์ ๊ธ์ ํ๋ํ๋ ๊ณต์ ์ ๊ธ(getFairLock)์ ์ฌ์ฉํ๋ ๊ฒ์ด ๋ ์ ํฉํ ์ ์์ต๋๋ค. ๋ค๋ง getFairLock์ getLock์ ๋นํด ์ฑ๋ฅ์ ์ผ๋ก ๋๋ฆฌ๋ฏ๋ก ์ ํ์ ์ํฉ์ ์๋ง๋ ๊ธฐ๋ฅ์ ์ ํํ๋ ๊ฒ์ ๊ถ์ฅ๋๋ฆฝ๋๋ค.
CustomSpringELParser.java
/**
* Spring Expression Language (SpEL) ๋ฌธ์์ด์ ํ์ฑํ๊ธฐ ์ํ ์ ํธ๋ฆฌํฐ ํด๋์ค.
*/
public class CustomSpringELParser {
private CustomSpringELParser() {
}
/**
* ์ฃผ์ด์ง SpEL ๋ฌธ์์ด์ ํ์ฑํ๊ณ ํ๊ฐํ๋ค. ์ ๊ณต๋ ๋งค๊ฐ ๋ณ์ ์ด๋ฆ๊ณผ ์ธ์๋ฅผ ์ฌ์ฉํ๋ค.
*
* @param parameterNames SpEL ๋ฌธ์์ด ๋ด์ ๋งค๊ฐ ๋ณ์ ์ด๋ฆ๋ค.
* @param args ๋งค๊ฐ ๋ณ์์ ๊ฐ๋ค.
* @param key ํ๊ฐ๋ SpEL ๋ฌธ์์ด.
* @return SpEL ๋ฌธ์์ด ํ๊ฐ์ ๊ฒฐ๊ณผ.
*/
public static Object getDynamicValue(String[] parameterNames, Object[] args, String key) {
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
// ์ ๊ณต๋ ์ธ์๋ฅผ ํด๋น ๋งค๊ฐ ๋ณ์ ์ด๋ฆ์ ํ ๋น
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
// SpEL ๋ฌธ์์ด ํ์ฑ ๋ฐ ํ๊ฐ
return parser.parseExpression(key).getValue(context, Object.class);
}
}
DistrubutedLockAop
ํด๋์ค ๋ด์ lock ๋ฉ์๋
์์๋ @DistributedLock
์ด๋
ธํ
์ด์
์ด ์ ์ฉ๋ ๋ฉ์๋๊ฐ ํธ์ถ๋ ๋ ๋ถ์ฐ ๋ฝ์ ์ ์ฉํ๋๋ฐ, ์ด๋ ์ค์ํ ์ ์ "๋ฝ์ ํค๋ฅผ ์ด๋ป๊ฒ ์์ฑํ ๊ฒ์ธ๊ฐ?"์
๋๋ค.
์์์ ์์ฑํ @DistributedLock
์ด๋
ธํ
์ด์
์๋ key๋ผ๋ ์์ฑ์ด ์์ต๋๋ค. ์ด key๋ Spring EL ํํ์(SpEL)
์ผ๋ก ์์ฑ๋ ์ ์์ด์, ๋ฉ์๋ ์ธ์ ๋ฑ์ ๊ธฐ๋ฐ์ผ๋ก ๋์ ์ธ Lock Key
๋ฅผ ์์ฑํ ์ ์์ต๋๋ค.
CustomSpringELParser์ ์ญํ
์ด SpEL ํํ์์ ์ค์ ๊ฐ์ผ๋ก ๋ณํํ๊ธฐ ์ํด CustomSpringELParser.getDynamicValue()
๋ฉ์๋๊ฐ ํธ์ถ๋ฉ๋๋ค.
String key = REDISSON_LOCK_PREFIX
+ CustomSpringELParser.getDynamicValue(
signature.getParameterNames(),
joinPoint.getArgs(),
distributedLock.key()
);
- signature.getParameterNames(): ๋ฉ์๋์ ํ๋ผ๋ฏธํฐ ์ด๋ฆ๋ค์ ๊ฐ์ ธ์จ๋ค.
- joinPoint.getArgs(): ํธ์ถ๋ ๋ฉ์๋์ ์ค์ ์ธ์ ๊ฐ์ ๊ฐ์ ธ์จ๋ค.
- distributedLock.key(): @DistributedLock ์ด๋ ธํ ์ด์ ์ด ์ง์ ํ SpEL ๋ฌธ์์ด์ ๊ฐ์ ธ์จ๋ค.
์ด ์ ๋ณด๋ค์ CustomSpringELParser.getDynamicValue
์ ์ ๋ฌํ๋ฉด, ํด๋น ๋ฉ์๋๋ SpEL ๋ฌธ์์ด๋ค์ ๋ณด๊ณ ์ค์ ํค ๊ฐ์ ๋ฐํํฉ๋๋ค.
@DistributedLock(key = "#id")
public void someMethod(String id) {
// ...
}
์๋ฅผ ๋ค์ด ์ ๋ฉ์๋๋ฅผ ํธ์ถํ ๋ id ๊ฐ์ผ๋ก "JOBDA"
๋ฅผ ์ ๋ฌํ๋ค๋ฉด, ์ ๊ธํค๋ "LOCK:JOBDA"
๊ฐ ๋ฉ๋๋ค. ์ด๋ CustomSpringELParser
๊ฐ "#id"
๋ผ๋ SpEL ํํ์์ "JOBDA"
๋ผ๋ ๊ฐ์ผ๋ก ํ๊ฐํ๊ธฐ ๋๋ฌธ์
๋๋ค. ๋ฐ๋ผ์ ์ด๋ ๊ฒ ๋์ ์ผ๋ก ์ ๊ธ ํค๋ฅผ ์์ฑํ์ฌ, ์ฌ๋ฌ ๊ณณ์์ ๋์ผํ ๋ฉ์๋๋ฅผ ํธ์ถํ ๋ ๋ค๋ฅธ ์ธ์ ๊ฐ์ ๋ฐ๋ผ ๋ค๋ฅธ ์ ๊ธ์ ์ ์ฉํ ์ ์์ต๋๋ค.
์ฌ๊ธฐ๊น์ง๊ฐ ๋ถ์ฐ ๋ฝ์ ๊ตฌํ์ด์๊ณ , ์๋๋ ์ด๋ฅผ ์ด์ฉํ "์ด๋ฒคํธ ๋น์ฆ๋์ค ๋ก์ง"์ ํ ํ๋ฆฟ ์ฝ๋์ ๋๋ค.
๋น์ฆ๋์ค ๋ก์ง ํ ํ๋ฆฟ
EventService.java
@Service
@Slf4j
@RequiredArgsConstructor
public class EventService {
private static final Long TRUE = 1L; // Redis ์๋น์ค๊ฐ TRUE๋ฅผ ๋ฐํํ ๋์ ๊ฐ
private final EventRedisRepository eventRedisRepository;
private final KafkaProducerService kafkaProducerService;
/**
* ์ฌ์ฉ์๊ฐ ์ด๋ฒคํธ์ ์๋ชจํ๋ ๋ฉ์๋.
* @DistributedLock ์ด๋
ธํ
์ด์
์ ํตํด ๋ฉ์๋ ์คํ ์ ๋ถ์ฐ ์ ๊ธ์ ์๋.
*
* @param eventUserDto ์ฌ์ฉ์ ์ ๋ณด
* @param eventType ์ด๋ฒคํธ ํ์
* @param count ํ์ฌ ์ด๋ฒคํธ์์ ์๋ชจ ๊ฐ๋ฅํ ์ธ์ ์
*/
@DistributedLock(key = "#eventType.name()")
public void enterUserIntoEvent(EventUserDto eventUserDto, EventType eventType, int count) {
if (hasEventReachedCapacity(eventType, count)) {
throw new EventCapacityExceededException(eventType + " ์ด๋ฒคํธ์ ์ธ์์ด ๋ง๊ฐ๋์์ต๋๋ค.");
}
enterUserIfNotAlreadyEntered(eventUserDto, eventType);
}
/**
* ํด๋น ์ด๋ฒคํธ์ ์ธ์์ด ๋ง๊ฐ๋์๋์ง ํ์ธ.
*
* @param eventType ์ด๋ฒคํธ ํ์
* @param count ํ์ฌ ์ด๋ฒคํธ์์ ์๋ชจ ๊ฐ๋ฅํ ์ธ์ ์
* @return ์ธ์์ด ๋ง๊ฐ๋ ๊ฒฝ์ฐ true, ์๋๋ฉด false
*/
private boolean hasEventReachedCapacity(EventType eventType, int count) {
return eventRedisRepository.getCurrentEventCount(eventType) >= count;
}
/**
* ์ฌ์ฉ์๊ฐ ์ด๋ฏธ ์ด๋ฒคํธ์ ์๋ชจํ๋์ง ํ์ธ ํ, ์์ง ์๋ชจํ์ง ์์๋ค๋ฉด ์๋ชจ ๋ก์ง์ ์คํ.
*
* @param eventUserDto ์ฌ์ฉ์ ์ ๋ณด
* @param eventType ์ด๋ฒคํธ ํ์
*/
private void enterUserIfNotAlreadyEntered(EventUserDto eventUserDto, EventType eventType) {
if (hasUserAlreadyEntered(eventUserDto, eventType)) {
throw new EventAlreadyEnteredException(eventUserDto.userId() + "์(๋) ์ด๋ฏธ " + eventType + " ์ด๋ฒคํธ์ ์๋ชจํ์์ต๋๋ค.");
}
sendEventEntryMessage(eventUserDto, eventType);
}
/**
* ์ฌ์ฉ์๊ฐ ์ด๋ฏธ ์ด๋ฒคํธ์ ์๋ชจ๋์๋์ง ํ์ธ.
*
* @param eventUserDto ์ฌ์ฉ์ ์ ๋ณด
* @param eventType ์ด๋ฒคํธ ํ์
* @return ์ด๋ฏธ ์๋ชจ๋์์ผ๋ฉด true, ์๋๋ฉด false
*/
private boolean hasUserAlreadyEntered(EventUserDto eventUserDto, EventType eventType) {
return eventRedisRepository.addUserToEventSet(eventType, eventUserDto.userId()) != TRUE;
}
/**
* Kafka๋ฅผ ํตํด DB์ ์ด๋ฒคํธ ์๋ชจ ๋ฉ์์ง๋ฅผ ์ ์ฅํ ์ ์๋๋ก ๋ฉ์ธ์ง ๋ฐํ.
*
* @param eventUserDto ์ฌ์ฉ์ ์ ๋ณด
* @param eventType ์ด๋ฒคํธ ํ์
*/
private void sendEventEntryMessage(EventUserDto eventUserDto, EventType eventType) {
String topic = KafkaProducerTopics.EVENT_ENTRY.getTopic();
KafkaEventEntryDto payload = new KafkaEventEntryDto(eventUserDto, eventType);
kafkaProducerService.sendAsyncJsonMessage(topic, payload);
}
}
์ฌ์ฉ์๊ฐ ์ด๋ฒคํธ์ ์ฑ๊ณต์ ์ผ๋ก ์๋ชจํ๋ฉด, Kafka
๋ฅผ ํตํด Topic
์ ๋ฐํํฉ๋๋ค. ์ด Topic
์ Consume
์์ ์ฒ๋ฆฌํ์ฌ ํ์์ผ๋ก DB
์ ๋ก๊น
ํ๋ ๊ตฌ์กฐ์
๋๋ค. ์ด๋ฌํ ๋ฐฉ์์ผ๋ก 1. ์ฑ
์๊ณผ ์ญํ ์ ๋ถ๋ฆฌํ๊ณ 2. ์๋น์ค ๊ฐ ์์กด๋๋ฅผ ๋ฎ์ถ ์ ์์ต๋๋ค.
๋ง์ฝ ํ๋ก์ ํธ์ Kafka์ ๊ฐ์ ๋ฉ์ธ์ง ์๋น์ค๋ฅผ ์ ์ฉํ๊ธฐ๊ฐ ์ด๋ ต๋ค๋ฉด ์คํ๋ง์์ ์ ๊ณตํด ์ฃผ๋ ApplicationEventPublisher๋ฅผ ์ฌ์ฉํ๋ ๊ฒ๋ ์ข์ ์ ํ์ผ๋ก ๋ณด์ ๋๋ค.
EventRedisRepository.java
@Repository
@RequiredArgsConstructor
public class EventRedisRepository {
// Redis์ ์ ์ฅ๋ ๋ ์ฌ์ฉ๋๋ ์ ๋ฏธ์ฌ
private static final String EVENT_SUFFIX = "event:";
// Spring Data Redis๋ฅผ ํ์ฉํ RedisTemplate ๊ฐ์ฒด
private final RedisTemplate<String, Object> redisTemplate;
/**
* ์ฌ์ฉ์๋ฅผ ํด๋น ์ด๋ฒคํธ ๋ฑ๋ก ๋ช
๋จ์ set์ ์ถ๊ฐ.
*
* @param eventType ์ด๋ฒคํธ ํ์
* @param userId ์ฌ์ฉ์ ๊ณ ์ ๊ฐ
* @return ์ฑ๊ณต์ ์ผ๋ก ์ถ๊ฐ๋๋ฉด 1, ์ด๋ฏธ ์กด์ฌํ๋ฉด 0
*/
public Long addUserToEventSet(EventType eventType, String userId) {
return redisTemplate.opsForSet().add(generateEventSetKey(eventType), userId);
}
/**
* ํด๋น ์ด๋ฒคํธ์์ ํ์ฌ๊น์ง ๋ฑ๋ก๋ ์ฌ์ฉ์ ์๋ฅผ ๋ฐํ.
*
* @param eventType ์ด๋ฒคํธ ํ์
* @return ๋ฑ๋ก๋ ์ฌ์ฉ์ ์
*/
public Long getCurrentEventCount(EventType eventType) {
return redisTemplate.opsForSet().size(generateEventSetKey(eventType));
}
/**
* ์ด๋ฒคํธ ํ์
์ ๊ธฐ๋ฐ์ผ๋ก Redis ์ ์ฅ์ฉ ํค๋ฅผ ์์ฑ.
*
* @param eventType ์ด๋ฒคํธ ํ์
* @return Redis์์ ์ฌ์ฉ๋๋ set์ ํค
*/
public String generateEventSetKey(EventType eventType) {
return EVENT_SUFFIX + eventType.name().toLowerCase();
}
}
๊ทธ๋ฆฌ๊ณ , ๋์ผํ ์ ์ ๊ฐ ์ค๋ณต์ผ๋ก ์ด๋ฒคํธ์ ์๋ชจ๋๋ ๊ฒ์ ๋ฐฉ์งํ๊ธฐ ์ํด Redis
์ Set ์๋ฃํ(SADD, SCARD)
์ ์ฌ์ฉํ์์ต๋๋ค.
- SADD(Set Add): Set์ ์๋ฃ๋ฅผ ์ถ๊ฐํ๋ ์ฐ์ฐ์ผ๋ก, ๋ง์ฝ ์ฃผ์ด์ง ๋ฐ์ดํฐ๊ฐ Set์ ์ด๋ฏธ ์กด์ฌํ๋ ๊ฒฝ์ฐ ๋ค์ ์ถ๊ฐ๋์ง ์์ต๋๋ค. ๋ํ ๊ฐ์ด ์ด๋ฏธ Set์ ์กด์ฌํ๋ ๊ฒฝ์ฐ 0์ ๋ฐํํ๊ณ , ์ ๊ท๋ก ์ถ๊ฐ๋ ๊ฒฝ์ฐ ์ถ๊ฐ๋ ๊ฐ์ ๊ฐ์๋ฅผ ๋ฐํํฉ๋๋ค.
- SCARD(Set Cardinallity): Set์ ์๋ ๋ฐ์ดํฐ์ ์, ์ฆ Set์ ํฌ๊ธฐ๋ฅผ ๋ฐํํฉ๋๋ค.
์ด๋ฒคํธ ์ฒ๋ฆฌ ๊ตฌ์ฑ๋
Redis
๋ฅผ ํ์ฉํ์ฌ ์ค์๊ฐ ์ด๋ฒคํธ ์ฒ๋ฆฌ ์๋๋ฅผ ํฅ์ ์์ผฐ์ต๋๋ค. ์ค์ DB
์ ์ง์ ์ ๊ทผํ๋ ๋์ , Redis
์์ ์ฌ์ฉ์ ์๋ชจ ์ํ์ ํ์ฌ ์ด๋ฒคํธ ์ฐธ์ฌ ์ธ์์ ๊ด๋ฆฌํจ์ผ๋ก์จ ๋น ๋ฅธ ์๋ต ์๊ฐ์ ๋ณด์ฅํ์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ Kafka
๋ฅผ ํ์ฉํ์ฌ Consume
์์ ๋ก๊น
์ด๋, ๊ธฐํํฐ์ฝ ๋ฐํ๊ณผ ๊ฐ์ ํ์ ๋ก์ง์ ๋น๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌํ๋๋ก ํ์ฌ ์๋น์ค ๊ฐ์ ์์กด๋๋ฅผ ๋ฎ์ถ์ด, ์์คํ
์ ๋ ์ ์ฐํ๊ณ ํ์ฅ ๊ฐ๋ฅํ๊ฒ ์ค๊ณํ์ต๋๋ค.
ํ ์คํธ
์ ํํ ์๋งํผ๋ง ์ด๋ฒคํธ์ ๋ฑ๋ก๋๋์ง ํ ์คํธ
@Test
@DisplayName("๋์์ ์ฌ๋ฌ ์ฌ์ฉ์๊ฐ ์ด๋ฒคํธ์ ์๋ชจํ ๋, ์๋ชจ ๊ฐ๋ฅํ ์ต๋ ์ธ์ ์๋ฅผ ์ด๊ณผํ์ง ์์์ผ ํ๋ค.")
void ๋์_์๋ชจ์_์ธ์์_์ด๊ณผ_์ํจ() throws InterruptedException {
// given
int ๋์_์์ฒญ_๊ฐ์ = 1000;
int ์ต๋_์ฐธ๊ฐ_์ธ์ = 100;
CountDownLatch latch = new CountDownLatch(๋์_์์ฒญ_๊ฐ์);
ExecutorService executorService = Executors.newFixedThreadPool(32);
// when
for (int i = 0; i < ๋์_์์ฒญ_๊ฐ์; i++) {
executorService.submit(() -> {
try {
String ์ ์ ์์ด๋ = UUID.randomUUID().toString(); // ๊ณ ์ ํ ์ฌ์ฉ์ ID ์์ฑ
eventService.enterUserIntoEvent(new EventUserDto(์ ์ ์์ด๋), EventType.TEST, ์ต๋_์ฐธ๊ฐ_์ธ์);
} finally {
latch.countDown();
}
});
}
latch.await();
executorService.shutdown();
// then
assertThat(eventRedisRepository.getCurrentEventCount(EventType.TEST)).isEqualTo(์ต๋_์ฐธ๊ฐ_์ธ์);
}
๋์ผ ์ ์ ๊ฐ ์ฌ๋ฌ ๋ฒ ์๋ชจํ ๊ฒฝ์ฐ ํ ๋ฒ๋ง ์๋ชจ๋๋์ง ํ
์คํธ
@Test
@DisplayName("์ค๋ณต๋ ์ฌ์ฉ์ ID๋ก ์ฌ๋ฌ ๋ฒ ์๋ชจํ ๊ฒฝ์ฐ, ํ ๋ฒ๋ง ์๋ชจ๊ฐ ๋์ด์ผ ํ๋ค.")
void ์ค๋ณต_์ฌ์ฉ์ID_์๋ชจ_๋ถ๊ฐ() throws InterruptedException {
// given
int ๋์_์์ฒญ_๊ฐ์ = 1000;
int ์ต๋_์ฐธ๊ฐ_์ธ์ = 100;
CountDownLatch latch = new CountDownLatch(๋์_์์ฒญ_๊ฐ์);
ExecutorService executorService = Executors.newFixedThreadPool(32);
String ์ค๋ณต_์ ์ ์์ด๋ = "์ค๋ณต์์ด๋";
// when
for (int i = 0; i < ๋์_์์ฒญ_๊ฐ์; i++) {
executorService.submit(() -> {
try {
eventService.enterUserIntoEvent(new EventUserDto(new EventUserDto(์ค๋ณต_์ ์ ์์ด๋), ์ต๋_์ฐธ๊ฐ_์ธ์);
} finally {
latch.countDown();
}
});
}
latch.await();
executorService.shutdown();
// then
assertThat(eventRedisRepository.getCurrentEventCount(EventType.TEST)).isEqualTo(1L);
}
๋ถ์ฐ ๋ฝ ๋ฏธ์ ์ฉ ํ
์คํธ: ๋ฉ์๋์ @DistributedLock(key = "#eventType.name()") ์ ๊ฑฐ
์ฐธ์กฐ
'BackEnd๐ฑ > Spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
ArchUnit์ผ๋ก ์ํคํ ์ฒ ๊ฒ์ฌํ๊ธฐ (0) | 2023.10.28 |
---|---|
LocalStack์ ํ์ฉํ AWS S3 ํ ์คํธ ํ๊ฒฝ ๊ตฌ์ถํ๊ธฐ (2) | 2023.10.22 |
TestContainer๋ก ํตํฉ ํ ์คํธ ํ๊ฒฝ ๊ตฌ์ถํ๊ธฐ (3) | 2023.10.15 |
Spring Data Redis์ @Indexed ์ฌ์ฉ ์ ์ฃผ์์ (0) | 2023.08.07 |
WebClient์์ ์๋ฌ ์ฒ๋ฆฌ์ ์ฌ์๋ํ๋ ๋ฐฉ๋ฒ (0) | 2023.08.03 |
Spring Batch๋? ๊ฐ๋จํ ๊ฐ๋ ๊ณผ ์ฝ๋ ์ดํด๋ณด๊ธฐ (0) | 2023.07.29 |
๋๊ธ