kimyu0218
  • [spring] ๋ถ„์‚ฐ๋ฝ์„ ๊ฑธ์–ด๋„ ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค?! (feat. ํŠธ๋žœ์žญ์…˜ AOP)
    2024๋…„ 12์›” 12์ผ 21์‹œ 50๋ถ„ 19์ดˆ์— ์—…๋กœ๋“œ ๋œ ๊ธ€์ž…๋‹ˆ๋‹ค.
    ์ž‘์„ฑ์ž: @kimyu0218
    ๐Ÿ”— [redis] redisson์œผ๋กœ ๋ถ„์‚ฐ๋ฝ ๊ฑธ์–ด์„œ ๋™์‹œ์„ฑ ๋ฌธ์ œ ํ•ด๊ฒฐํ•˜๊ธฐ (๊ทผ๋ฐ ํ•ด๊ฒฐ ์•ˆ๋จ)

    ์ •๋ณด : ์Šฌ๋ž™ ์›นํ›… ์•Œ๋ฆผ์ด ๋งŽ์ด ๊ฐ€๋ฉด ๋ธ”๋ฝ๋œ๋‹ค

    ๋•Œ๋Š” ๋ถ„๋‹น์œผ๋กœ ๊ต์œก ๊ฐ€๋˜ ๊ธธ.. ๊ฐˆ ๊ธธ๋„ ๋ฉ€๊ณ  ๊ฐ€๋ฐฉ์ด ๋ฌด๊ฑฐ์›Œ ๋…ธํŠธ๋ถ์„ ๊ฐ€์ ธ๊ฐ€์ง€ ์•Š์•˜๋‹ค. ๊ทผ๋ฐ ์•„์นจ 7์‹œ๋ถ€ํ„ฐ ์Šฌ๋ž™ ์•Œ๋ฆผ์ด 100๊ฐœ ๋„˜๊ฒŒ ์™”๋‹ค. (๐Ÿ˜ˆ : ์–ด? ๋…ธํŠธ๋ถ ๋‘๊ณ  ๊ฐ„๋‹ค ์žฅ์• ๊ฐ์ด๋‹ค) ์•ฑ ์ง„์ž… ์‹œ์ ์— ์‚ฌ์šฉํ•˜๋Š” API + ์‚ฌ์šฉ์ž์˜ ํฌ๊ธฐํ•˜์ง€ ์•Š๋Š” ์ธ๋‚ด์‹ฌ์ด ํ•ฉ์ณ์ ธ ํŒ€์›๋“คํ•œํ…Œ ์Šฌ๋ž™ ์•Œ๋ฆผ ํ…Œ๋Ÿฌ๋ฅผ ์‹œ์ž‘ํ–ˆ๋‹ค.

    ํ•ด๋‹น ํฌ์ŠคํŒ…์—์„œ ์ฒ˜๋ฆฌํ•œ ์ค„๋กœ๋งŒ ์•Œ์•˜๋˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋™์‹œ์„ฑ์ด ์›์ธ์ด์—ˆ๋‹ค. ์šฐ๋ฆฌ ์„œ๋น„์Šค๋Š” ํ•˜๋ฃจ์— ํ•œ ๋ฒˆ๋งŒ ๋ฏธ์…˜ ์ธ์ฆ์„ ํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ Repeatable Read ๊ฒฉ๋ฆฌ ์ˆ˜์ค€์ด INSERT ์ž‘์—…์„ ์˜ˆ๋ฐฉํ•  ์ˆ˜ ์—†์–ด API๋ฅผ ๋น ๋ฅด๊ฒŒ ์—ฐ๋‹ฌ์•„ ํ˜ธ์ถœํ•˜๋ฉด ์—ฌ๋Ÿฌ ๋ฒˆ ์ธ์ฆํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅํ–ˆ๋‹ค.

    ์ด๋Ÿฌํ•œ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ๋ง‰๊ณ ์ž ๋ถ„์‚ฐ๋ฝ์„ ์ ์šฉํ–ˆ๊ณ  ์ž˜ ํ•ด๊ฒฐ๋œ ์ค„ ์•Œ์•˜์œผ๋‚˜ ์Šฌ๋ž™ ํ…Œ๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒƒ์ด๋‹ค...!

    ๋‹นํ™ฉํ•˜์‹  ๋ถ€์ง€๋Ÿฐํ•œ pm๋‹˜
    ๋ชจ๋“  ๊ฒฝ์šฐ์˜ ์ˆ˜๋กœ ์—๋Ÿฌ๋ฅผ ๋‚ด๋Š”, ๋‚˜๋ผ๋Š” ์—ฃ์ง€์ผ€์ด์Šค ๐Ÿ˜‡

     

    ๋ฝ ํš๋“/๋ฐ˜ํ™˜์€ ์ž˜ ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค๋งŒ

    ํฌ์ŠคํŒ…์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋“ฏ์ด ๋ถ„์‚ฐ๋ฝ์„ ์œ„ํ•œ ์–ด๋…ธํ…Œ์ด์…˜์„ ๋งŒ๋“ค๊ณ  AOP๋ฅผ ํ†ตํ•ด ๋ฝ์„ ํš๋“/๋ฐ˜ํ™˜ํ•˜๋Š” ๋กœ์ง์„ ์ž‘์„ฑํ–ˆ๋‹ค.

    @Around("@annotation(com.nexters.goalpanzi.common.annotation.RedissonLock)")
    public Object lockMissionVerification(final ProceedingJoinPoint joinPoint) throws Throwable {
        ...
        var args = joinPoint.getArgs();
        LockKey lockKey = LockKey.of(lockTarget, args);
        RLock lock = redissonClient.getLock(lockKey.toString());
            
        // joinPoint ์ˆ˜ํ–‰ ์ „์— ๋ฝ ํš๋“ํ•˜๊ธฐ
        boolean lockable = lock.tryLock(annotation.waitTime(), annotation.timeUnit());
        ...
        log.info("{} acquired {} lock with key: {}.", methodName, lockTarget, lockKey);
            
        // joinPoint ์ˆ˜ํ–‰
        joinPoint.proceed();
            
        // joinPoint ์ˆ˜ํ–‰ ํ›„ ๋ฝ ๋ฐ˜ํ™˜ํ•˜๊ธฐ
        lock.unlock();
        log.info("{} released {} lock with key: {}.", methodName, lockTarget, lockKey);
        ...
     }

    `docker logs [DOCKER_CONTAINER_ID] | grep "lock"`์œผ๋กœ ๋กœ๊ทธ๋ฅผ ์ถœ๋ ฅํ•˜๋ฉด ๋ฏธ์…˜ ์ธ์ฆ ์š”์ฒญ ์‹œ ๋ฝ์ด ์ž˜ ๊ฑธ๋ฆฌ๊ณ  ์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ์‹ฌ์ง€์–ด ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ํ•œ ๋ฏธ์…˜์— ๋Œ€ํ•ด ์—ฌ๋Ÿฌ ๋ฒˆ ์š”์ฒญ์„ ๋ณด๋‚ด๋„ ์ˆœ์ฐจ์ ์œผ๋กœ ์‹คํ–‰๋˜๊ณ  ์žˆ์—ˆ๋‹ค.

    2024-12-11 21:18:45 [http-nio-8080-exec-6]  INFO c.n.g.common.aop.RedissonLockAspect - createVerification acquired MissionVerification lock with key: LOCK:MissionVerification:[CreateMissionVerificationCommand{memberId=107, missionId=183}].
    2024-12-11 21:18:46 [http-nio-8080-exec-6]  INFO c.n.g.common.aop.RedissonLockAspect - createVerification released MissionVerification lock with key: LOCK:MissionVerification:[CreateMissionVerificationCommand{memberId=107, missionId=183}].
    2024-12-11 21:22:53 [http-nio-8080-exec-4]  INFO c.n.g.common.aop.RedissonLockAspect - createVerification acquired MissionVerification lock with key: LOCK:MissionVerification:[CreateMissionVerificationCommand{memberId=110, missionId=183}].
    2024-12-11 21:22:53 [http-nio-8080-exec-4]  INFO c.n.g.common.aop.RedissonLockAspect - createVerification released MissionVerification lock with key: LOCK:MissionVerification:[CreateMissionVerificationCommand{memberId=110, missionId=183}].


     

    ๋ฝ ํš๋“ ์ „์— ํŠธ๋žœ์žญ์…˜์ด ์‹œ์ž‘๋œ๋‹ค๊ณ ?!

    ๋™์‹œ์„ฑ ๋ฌธ์ œ๋Š” ํŠธ๋žœ์žญ์…˜๊ณผ ๊ด€๋ จ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ํŠธ๋žœ์žญ์…˜์ด ์–ธ์ œ ์‹œ์ž‘๋˜๊ณ  ์–ธ์ œ ๋๋‚˜๋Š”์ง€ ๋ถ„์„ํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค. `application.yml` ํŒŒ์ผ์— ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ํŠธ๋žœ์žญ์…˜์˜ ์‹œ์ž‘๊ณผ ์ข…๋ฃŒ ๋กœ๊ทธ๋ฅผ ์ฐ๋Š”๋‹ค.

    logging:
      level:
        org:
          springframework:
            transaction:
              interceptor: TRACE

    ์‹ค์ œ ์„œ๋น„์Šค๋Š” ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐ ์˜ค๋žœ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์— ์ด์ „์— ์ž‘์„ฑํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์— `@Transactional` ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์ด๊ณ  ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ–ˆ๋‹ค.

    @Transactional
    public class RedissonLockTestBean {
     
        @RedissonLock(waitTime = 1L)
        void serializeFunction(final Object args) throws InterruptedException {
            ...
        }
    }
    2024-12-11T23:12:22.488+09:00 TRACE 11616 --- [pool-2-thread-2] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.nexters.goalpanzi.config.redisson.RedissonLockTestBean.serializeFunction]
    2024-12-11T23:12:22.488+09:00 TRACE 11616 --- [pool-2-thread-1] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.nexters.goalpanzi.config.redisson.RedissonLockTestBean.serializeFunction]
    2024-12-11T23:12:22.645+09:00  INFO 11616 --- [pool-2-thread-2] c.n.g.common.aop.RedissonLockAspect      : serializeFunction acquired Unknown lock with key: LOCK:Unknown:[Resource1].
    2024-12-11T23:12:22.645+09:00  INFO 11616 --- [pool-2-thread-1] c.n.g.common.aop.RedissonLockAspect      : serializeFunction acquired Unknown lock with key: LOCK:Unknown:[Resource0].
    2024-12-11T23:12:24.707+09:00  INFO 11616 --- [pool-2-thread-2] c.n.g.common.aop.RedissonLockAspect      : serializeFunction released Unknown lock with key: LOCK:Unknown:[Resource1].
    2024-12-11T23:12:24.708+09:00  INFO 11616 --- [pool-2-thread-1] c.n.g.common.aop.RedissonLockAspect      : serializeFunction released Unknown lock with key: LOCK:Unknown:[Resource0].
    2024-12-11T23:12:24.708+09:00 TRACE 11616 --- [pool-2-thread-2] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.nexters.goalpanzi.config.redisson.RedissonLockTestBean.serializeFunction]
    2024-12-11T23:12:24.708+09:00 TRACE 11616 --- [pool-2-thread-1] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.nexters.goalpanzi.config.redisson.RedissonLockTestBean.serializeFunction]

     
    ๋กœ๊ทธ์—์„œ ์•Œ ์ˆ˜ ์žˆ๋“ฏ์ด ํŠธ๋žœ์žญ์…˜์„ ์‹œ์ž‘ํ•œ ํ›„์— ๋ถ„์‚ฐ๋ฝ์„ ๊ฑด๋‹ค. ์šฐ๋ฆฌ ์„œ๋น„์Šค๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์„ค๋ช…ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ผ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

    ์œ„์™€ ๊ฐ™์€ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘ ์ „์— ๋ถ„์‚ฐ๋ฝ์„ ๊ฑธ์–ด์•ผ ํ•œ๋‹ค.

     

    Repeatable Read ๊ฒฉ๋ฆฌ ์ˆ˜์ค€์—์„œ INSERT๋กœ ์ธํ•œ ๋™์‹œ์„ฑ ๋ฌธ์ œ ์˜ˆ๋ฐฉํ•˜๊ธฐ

    ์„œ๋น„์Šค์—์„œ ๋ฐœ์ƒํ•œ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ๋‹ค์‹œ ์ •๋ฆฌํ•ด๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

    • ํ•˜๋ฃจ์— ํ•œ ๋ฒˆ๋งŒ ๋ฏธ์…˜์„ ์ธ์ฆ(INSERT)ํ•  ์ˆ˜ ์žˆ๋Š” ์„œ๋น„์Šค์—์„œ
    • Repeatable Read ๊ฒฉ๋ฆฌ ์ˆ˜์ค€์œผ๋กœ ์ธํ•ด ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๊ณ ,
    • ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด redis ๋ถ„์‚ฐ๋ฝ์„ ์ ์šฉํ–ˆ์ง€๋งŒ
    • ๋จผ์ € ์‹คํ–‰๋œ ํŠธ๋žœ์žญ์…˜์ด ์ปค๋ฐ‹๋˜๊ธฐ ์ „์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์œผ๋ฉด์„œ ์ธ์ฆ์ด ์—ฌ๋Ÿฌ ๋ฒˆ ์ˆ˜ํ–‰๋˜์—ˆ๋‹ค.

    ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘ ์ „ํ›„์— ๋ถ„์‚ฐ๋ฝ์„ ํš๋“/๋ฐ˜ํ™˜ํ•ด์•ผ ํ•œ๋‹ค. ๋™์ผํ•œ ์กฐ์ธ ํฌ์ธํŠธ์—์„œ `@Transactional`์— ์˜ํ•œ AOP, `@RedissonLock`์— ์˜ํ•œ AOP๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— ๊ทธ ์ˆœ์„œ๋ฅผ ์กฐ์ ˆํ•ด์•ผ ํ•œ๋‹ค. ์ฆ‰, ์กฐ์ธ ํฌ์ธํŠธ์— ์–ด๋“œ๋ฐ”์ด์Šค๋“ค์„ ์ ์ ˆํ•˜๊ฒŒ ๋ผ์›Œ๋„ฃ์–ด์•ผ ํ•œ๋‹ค.

    ๐Ÿ”— ์กฐ์ธ ํฌ์ธํŠธ, ์–ด๋“œ๋ฐ”์ด์Šค, AOP ๊ฐœ๋… - [spring] ์Šคํ”„๋ง AOP

     

    @Order๋กœ AOP ์ˆœ์„œ ์„ค์ •ํ•˜๊ธฐ

    `@Order`๋Š” ์ปดํฌ๋„ŒํŠธ์˜ ์ˆœ์„œ๋ฅผ ์ง€์ •ํ•œ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ `Ordered.LOWEST_PRECEDENCE`๋กœ ๊ฐ€์žฅ ๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„๋ฅผ ์˜๋ฏธํ•œ๋‹ค.

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
    @Documented
    public @interface Order {
    	int value() default Ordered.LOWEST_PRECEDENCE;
    }
    public interface Ordered {
    	int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
    	int LOWEST_PRECEDENCE = Integer.MAX_VALUE;
    	int getOrder();
    }

    `@Transactional`์€ `Ordered.LOWEST_PRECEDENCE`๋ฅผ, ์ˆœ์„œ๋ฅผ ์ง€์ •ํ•ด์ฃผ์ง€ ์•Š์€ `@RedissonLock` ๋˜ํ•œ `Ordered.LOWEST_PRECEDENCE`๋ฅผ ๊ฐ–๋Š”๋‹ค. ๋ถ„์‚ฐ๋ฝ์ด ํŠธ๋žœ์žญ์…˜๋ณด๋‹ค ๋จผ์ € ์‹คํ–‰๋˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ํŠธ๋žœ์žญ์…˜๋ณด๋‹ค ๋†’์€ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ฐ€์ ธ์•ผ ํ•œ๋‹ค. ๋”ฐ๋ผ์„œ `Ordered.LOWEST_PRECEDENCE`๋ณด๋‹ค ์ž‘์€ ์ •์ˆ˜๋ฅผ `value`๋กœ ๊ฐ–๋Š” `@Order` ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์—ฌ์ค€๋‹ค.

    @Order(Ordered.HIGHEST_PRECEDENCE) // ๋ถ„์‚ฐ๋ฝ์„ ๊ฐ€์žฅ ๋จผ์ € ์‹คํ–‰ํ•œ๋‹ค!!
    @RequiredArgsConstructor
    @Slf4j
    @Aspect
    @Component
    public class RedissonLockAspect {
        ...
    }

    ์œ„์™€ ๊ฐ™์ด ์ˆœ์„œ๋ฅผ ์กฐ์ •ํ•œ ํ›„ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์žฌ์‹คํ–‰ํ•˜๋ฉด ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘ ์ „์— ๋ถ„์‚ฐ๋ฝ์„ ๋จผ์ € ํš๋“ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

    2024-12-11T23:22:24.979+09:00  INFO 16148 --- [pool-2-thread-2] c.n.g.common.aop.RedissonLockAspect      : serializeFunction acquired Unknown lock with key: LOCK:Unknown:[Resource1].
    2024-12-11T23:22:24.979+09:00  INFO 16148 --- [pool-2-thread-1] c.n.g.common.aop.RedissonLockAspect      : serializeFunction acquired Unknown lock with key: LOCK:Unknown:[Resource0].
    2024-12-11T23:22:25.010+09:00 TRACE 16148 --- [pool-2-thread-1] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.nexters.goalpanzi.config.redisson.RedissonLockTestBean.serializeFunction]
    2024-12-11T23:22:25.010+09:00 TRACE 16148 --- [pool-2-thread-2] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.nexters.goalpanzi.config.redisson.RedissonLockTestBean.serializeFunction]
    2024-12-11T23:22:27.012+09:00 TRACE 16148 --- [pool-2-thread-1] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.nexters.goalpanzi.config.redisson.RedissonLockTestBean.serializeFunction]
    2024-12-11T23:22:27.012+09:00 TRACE 16148 --- [pool-2-thread-2] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.nexters.goalpanzi.config.redisson.RedissonLockTestBean.serializeFunction]
    2024-12-11T23:22:27.074+09:00  INFO 16148 --- [pool-2-thread-2] c.n.g.common.aop.RedissonLockAspect      : serializeFunction released Unknown lock with key: LOCK:Unknown:[Resource1].
    2024-12-11T23:22:27.074+09:00  INFO 16148 --- [pool-2-thread-1] c.n.g.common.aop.RedissonLockAspect      : serializeFunction released Unknown lock with key: LOCK:Unknown:[Resource0].

     
    ๋” ์ด์ƒ ๊ฐ™์€ ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๊ธธ ๋ฐ”๋ผ๋ฉฐ... (์ด๊ฒŒ ์›์ธ์ด ์•„๋‹ˆ๋ผ๋ฉด 2ํƒ„์œผ๋กœ ๋Œ์•„์˜ต๋‹ˆ๋‹ค ๐Ÿ˜‡)


    ์ฐธ๊ณ ์ž๋ฃŒ

    ๋Œ“๊ธ€