BackEnd๐ŸŒฑ/Spring

๋ถ„์‚ฐ๋ฝ์œผ๋กœ ์„ ์ฐฉ์ˆœ ์ด๋ฒคํŠธ ๊ตฌํ˜„ํ•˜๊ธฐ

dkswnkk 2023. 8. 31. 17:33

์„ ์ฐฉ์ˆœ ์ด๋ฒคํŠธ ์š”๊ตฌ์‚ฌํ•ญ

  1. ์„ ์ฐฉ์ˆœ์œผ๋กœ n๋ช…๊นŒ์ง€๋งŒ ๊ธฐํ”„ํ‹ฐ์ฝ˜์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค.
  2. ๊ธฐํ”„ํ‹ฐ์ฝ˜์€ 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 ๋‚ด๋ถ€์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ์ด๋ฏ€๋กœ, ์ถ”๊ฐ€์ ์ธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‚˜ ๋„๊ตฌ๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ์ด ๋ฐฉ์‹์„ ์„ ํƒํ•˜์ง€ ์•Š์€ ๋„ค ๊ฐ€์ง€ ์ด์œ ๊ฐ€ ์žˆ๋Š”๋ฐ, ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. Redis ํŠธ๋žœ์žญ์…˜์€ Redis ๋ช…๋ น์–ด๋“ค๋งŒ ์›์ž์ ์œผ๋กœ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ธฐ์—, ํŠธ๋žœ์žญ์…˜ ์ค‘๊ฐ„์— Redis ๋ช…๋ น์–ด๋ฅผ ์ œ์™ธํ•œ ์ฝ”๋“œ๋ฅผ ์‚ฝ์ž…ํ•˜๋Š” ๊ฒƒ์€ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค. ์ฆ‰, Redis์— ์ฝ๊ณ  ์“ฐ๋Š” ์ž‘์—…๋“ค๋งŒ ์›์ž์ ์œผ๋กœ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.
  2. Cluster Redis ํ™˜๊ฒฝ์—์„œ๋Š” ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ๊ฐ€ ๋ณต์žกํ•ด์ง„๋‹ค.
  3. ์ด ๋ฐฉ์‹์€ Optimistic Locking์„ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•˜๋ฉด ์žฌ์‹œ๋„ํ•ด์•ผ ํ•˜๋Š” ๋ถ€๋‹ด์ด ์กด์žฌํ•œ๋‹ค.(๋ฌผ๋ก  ๊ตญ๋‚ด ๋Œ€๋ถ€๋ถ„์˜ ์„œ๋น„์Šค ํŠธ๋ž˜ํ”ฝ์€ Optimistic Locking ๋ฐฉ์‹์œผ๋กœ ์ถฉ๋ถ„ํžˆ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ์˜๊ฒฌ์ด ๋งŽ๋‹ค.)
  4. Redis Transaction ๊ด€๋ จํ•œ ์ง€์‹์ด ๋‹น์‹œ ์—†์–ด, ์Šคํ”„๋ฆฐํŠธ ๊ธฐ๊ฐ„ ๋‚ด์— ๊ฐœ๋ฐœ๊ณผ ๊ฒ€์ฆ๊นŒ์ง€ ์ง„ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด ํ•™์Šต ๊ฒฝํ—˜์ด ์žˆ๋Š” ๋ถ„์‚ฐ๋ฝ์„ ์ ์šฉํ•˜๋Š” ๊ฒŒ ๋” ๋‚˜์€ ์„ ํƒ์œผ๋กœ ๋ณด์˜€๋‹ค.

์ €๋Š” ๋‹จ์ˆœํžˆ ์ด๋ฒˆ ์„ ์ฐฉ์ˆœ ์ด๋ฒคํŠธ๋งŒ์„ ์œ„ํ•œ ๋™์‹œ์„ฑ ์ด์Šˆ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ๋ณด๋‹ค๋Š”, ๋‹ค์–‘ํ•œ ๊ธฐํš์—์„œ๋„ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ํ† ๋Œ€๋ฅผ ๋งˆ๋ จํ•˜๊ณ ์ž ํ–ˆ๊ณ , ์ด์ „์— ํ•™์Šตํ–ˆ๋˜ ๊ฒฝํ—˜์„ ํ†ตํ•ด ์Šคํ”„๋ฆฐํŠธ ๊ธฐ๊ฐ„์ผ์ •์„ ๋งž์ถ”๊ธฐ ์œ„ํ•ด ๋‹ค์ค‘ ์ธ์Šคํ„ด์Šค ํ™˜๊ฒฝ์—์„œ๋„ ๊ณตํ†ต๋œ ๋ฝ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ถ„์‚ฐ ๋ฝ์„ ๊ตฌํ˜„ํ•˜๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

 

Redis์˜ Redisson ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ ์ •ํ•œ ์ด์œ 

Java์˜ Redis ํด๋ผ์ด์–ธํŠธ์—๋Š” Jedis, Lettuce, Redisson ์ด๋ ‡๊ฒŒ ํฌ๊ฒŒ ์„ธ ๊ฐ€์ง€์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ์„ธ ๊ฐ€์ง€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ „๋ถ€ ๋ถ„์‚ฐ๋ฝ ๊ตฌํ˜„์ด ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ์ €ํฌ๋Š” Redisson์„ ์„ ์ •ํ–ˆ๋Š”๋ฐ ์ด์œ ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • Jedis๋Š” Lettuce์— ๋น„ํ•ด ์„ฑ๋Šฅ์ด ๋งค์šฐ ๋–จ์–ด์ง€๊ธฐ ๋•Œ๋ฌธ์— ๊ณ ๋ ค ๋Œ€์ƒ์ด ์•„๋‹ˆ์—ˆ๋‹ค.
  • Lettuce๋Š” ์Šคํ•€๋ฝ ๊ธฐ๋ฐ˜์œผ๋กœ, ์š”์ฒญ์ด ๋งŽ์„์ˆ˜๋ก Redis์˜ ๋ถ€ํ•˜๊ฐ€ ์ปค์ง€๊ณ  ๊ฐœ๋ฐœ์ž๊ฐ€ retry ๋กœ์ง์„ ์ง์ ‘ ์ž‘์„ฑํ•ด์•ผ ํ•œ๋‹ค.
  • Redisson์€ Pub/Sub ๊ธฐ๋ฐ˜์œผ๋กœ Lock ๊ตฌํ˜„์ด ๊ฐ€๋Šฅํ•˜๊ณ  ํƒ€์ž„์•„์›ƒ ๊ธฐ๋Šฅ์ด ๊ตฌํ˜„๋˜์–ด ์žˆ๋‹ค.

๋” ์ž์„ธํ•œ ๋‚ด์šฉ์ด ๊ถ๊ธˆํ•˜์‹œ๋‹ค๋ฉด ์•„๋ž˜ ๋‚ด์šฉ์„ ์ฐธ๊ณ ํ•ด ๋ณด์‹œ๊ธธ ์ถ”์ฒœ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

 

 

๋ถ„์‚ฐ ๋ฝ ๊ตฌํ˜„

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 ๋ถ„์‚ฐ ๋ฝ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.

  1. @DistributedLock ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด DistributedLockAop์˜ lock ๋ฉ”์„œ๋“œ๊ฐ€ ์‹คํ–‰๋œ๋‹ค.
  2. ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋Š” ๋จผ์ € ๋™์  ์ž ๊ธˆ ํ‚ค๋ฅผ ์ƒ์„ฑํ•˜๋ฉฐ, ์ด๋•Œ @DistributedLock์˜ Key ๊ฐ’์„ SpEL๋กœ ํ‰๊ฐ€ํ•˜์—ฌ ์‚ฌ์šฉํ•œ๋‹ค.
  3. ์ด ๋™์  ํ‚ค๋กœ Redisson์˜ RLock ๊ฐ์ฒด๋ฅผ ์–ป์–ด์˜จ๋‹ค.
  4. ์ง€์ •๋œ ์‹œ๊ฐ„(waitTime) ๋™์•ˆ ์ž ๊ธˆ์„ ์‹œ๋„ํ•œ๋‹ค. ์„ฑ๊ณตํ•˜๋ฉด ๋ฉ”์„œ๋“œ๋ฅผ ์‹คํ–‰ํ•˜๊ณ , ์‹คํŒจํ•˜๋ฉด null์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  5. ๋ฉ”์„œ๋“œ ์‹คํ–‰ ํ›„, ํ˜„์žฌ ์Šค๋ ˆ๋“œ๊ฐ€ ์ž ๊ธˆ์„ ๋ณด์œ ํ•˜๊ณ  ์žˆ์œผ๋ฉด ์ž ๊ธˆ์„ ํ•ด์ œํ•œ๋‹ค.

๋˜ํ•œ 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()
    );
  1. signature.getParameterNames(): ๋ฉ”์„œ๋“œ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ ์ด๋ฆ„๋“ค์„ ๊ฐ€์ ธ์˜จ๋‹ค.
  2. joinPoint.getArgs(): ํ˜ธ์ถœ๋œ ๋ฉ”์„œ๋“œ์˜ ์‹ค์ œ ์ธ์ˆ˜ ๊ฐ’์„ ๊ฐ€์ ธ์˜จ๋‹ค.
  3. 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()") ์ œ๊ฑฐ

 

 

์ฐธ์กฐ