CheerUp_Cheers

분산락 적용기 (feat. 락(Lock) 종류와 장단점, 동시성) 본문

스프링

분산락 적용기 (feat. 락(Lock) 종류와 장단점, 동시성)

meorimori 2024. 8. 15. 17:48

 

 

목차

     

     

     

    개요


    동시성을 제어하는 방법은 여러가지가 있고, 필자의 회사에서는 주로 @Transactional와 낙관적 락을 사용하여 처리를 하는 편이다.

     

    하지만, 프로젝트를 꽤나 진행이 된 이후에 위의 구조를 사용하는 로직이 많은 오류를 뱉게 되었다.

    기존에 설계 목적과 다르게 하나의 컬럼에 대해서 지독하게 UPDATE를 진행하게 되었고, 낙관적 락의 특성상 기존의 상태가 변경되면 업데이트 처리를 하지 않고 오류를 발생시키기 때문이었다.

     

    '이런 상황에서는 어떻게 개선을 할 수 있을까?'

    '개선을 할 수 있는 시간적, 기술적 여건이 충분할까?'

    를 시작으로 해당 이슈를 어떻게 처리했는지에 대해서 작성하고 싶어 글을 작성하였다.

     

    '어? 이거 이렇게 사용해도 괜찮나요?' 라고 의견이 나올 수 있고, 더 나은 방법도 있을 것이라고 생각한다.

     

    Lock의 종류 (비관적, 낙관적)


    일단 Lock 종류에 대해서 알 필요가 있다.

    웹상에서 잘 정리된 글들이 많아서, 아래의 참조로 남긴 내용을 참고 하면 될 것 같고 간단하게 표로 정리해보았다.

     

    앞에 서술했던 것 처럼, 사내에서는 @Transactional과 낙관락을 사용하는 방법으로 동시성을 처리하는 편이고 표로 정리된 비교 내용처럼 성능과 어플리케이션에서의 처리가 필요하지만 동시성은 우수한 편이다.

    낙관적 락 vs 비관적 락

      비관적 락 (Pessimistic Lock) 낙관적 락 (Optimistic Lock)
    락 설정 여부 트랜잭션 시작 시 Shared Lock 또는 Exclusive Lock을 설정 락을 설정하지 않음.
    성능 동시성 낮음, 성능 저하 가능, 데드락 위험 존재 동시성 높음, 성능 우수
    데이터 무결성 데이터 무결성 보장 데이터 무결성 보장 ( 충돌 시, 추가 처리 필요 )
    충돌 처리 DB단에서 처리 어플리케이션 단에서 처리
    적합한 환경 - 데이터 무결성이 중요할 때
    - 데이터 충돌이 자주 발생할 때 (UPDATE )
    - 데이터 충돌이 드물 때 (UPDATE ↓)
    - 조회 작업이 많은 경우 (SELECT )

     

     

    하지만, 데이터 충돌이 많은 경우에 어플리케이션에서 Retry등으로 처리한다고 하여도 낙관적 락은 계속 실패할 가능성이 꽤나 있다.

     

    '이럴 땐 어떻게 해야할까?', '알림으로만 받으면 될까?', '그렇다면 알림이 너무 많이 오지 않을까?' 

    라는 생각이 들었고, 개선여지는 충분해 보였다.

     

    동시성 처리


    동시성을 처리할 수 있는 방법은 여러가지가 있다.

     

    낙관적 락, 비관적 락도 동시성 처리에 한 부분이다.

    하지만 여기서는 Redisson의 분산락을 사용하여 동시성을 개선을 진행하였다.

     

    Redisson을 사용한 이유는 아래와 같다.

    • 하나의 서버에서만 동작하는 synchronized와 다르게 분산환경에서 동시성 제어가 가능
    • 비관락은 해당 레코드에 락을 걸게 되어 다른 로직들에게 대기를 걸게 된다. (데드락, 성능 이슈)

     

    분산환경에서의 동시성을 제어할 방법은 찾았고, 문제는 실제 적용하는 것이 남았다.

     

    앞에 서술했던 것 처럼, 시간적, 기술적 여유가 있지 않았다.

    문제가 되는 로직들은 여러 모듈에 퍼져있고, 해당 로직 또한 실행되는 시점이 제각각이였다.

     

    이게 왜 문제가 되는지는 Redisson 분산락 적용에 유의점을 보면 알 수 있다.

     

    Redisson의 사용 유의점

    레디슨의 경우, 메소드 블록을 Lock으로 잡아 줌으로써 편하게 동시성을 지킬 수 있다.

    @Transactional와 같이 사용할 경우, 아래의 유의점을 지켜줄 때, 데이터 정합성을 만족할 수 있다

    락 획득 → 트랜잭션 시작 → 트랜잭션 종료 → 락 해제

     

    여러 모듈에서 사용하고 있으며 상위 트랜잭션이 적용이 되어있어, 데이터 정합성을 지켜줄 수 가 없어 보였다.

    그래서, 개선을 위해 아래의 선택이 필요했다.

    1. 해당 로직을 사용하는 전체 기능을 수정
    2. 데이터 정합성을 위해 다른 방법 강구

    기존에 바이블처럼 퍼져 있던 위의 유의점을 지킨 사례 말고, 데이터 정합성을 지킬 수 있는 방법을 강구했다.

     

     

    데이터 정합성을 위한 방법

    @Transactional과 분산락을 같이 썼을 경우 데이터 정합성이 틀어지는 이유를 @Transactional가 끝이 났을 경우 커밋이 되는 점에서 착안하여 버전1, 버전2, 버전3으로 진행하였다.

     

    버전 1 - TransactionSynchronizationManager [원복 처리]

    처음에는 트랜잭션 범위를 분리하고, 개선로직 이후의 로직에서 예외 발생 시

    원복처리하는 방법을 생각하였다.

    간략화 된 시퀀스 다이어그램 V1

     

     

     

    하지만 이 경우에도 문제점이 보였다.

    • 원복마저 실패한다면? 등..

    버전 2 - @TransactionalEventListener + BEFORE_COMMIT + @Transactional(propagation = Propagation.REQUIRES_NEW)

    다음은 원복을 하지 않고, 다른 트랜잭션이지만 예외 전파로 같이 처리하는 방법이다.

    간략화 된 시퀀스 다이어그램 V2

    테스트 코드에서 문제가 없이 돌아가며, 정합성이 완벽하게 지켜졌다.

    하지만, 테스트를 위해 스레드를 늘려가면 큰 문제가 생긴다.

     

    기본적으로 스프링에서는 DB 커넥션 풀이 10개이다.

    그래서 테스트 중, 스레드를 9개부터는 처리가 거의 되지 않았다. (8개는 처리가 됨)

     

    @Transactional(propagation = Propagation.REQUIRES_NEW) 를 사용하지 않을 때는, 순차적으로 스레드가 30개든 50개든 처리가 된다.

    하지만, REQUIRES_NEW를 사용하게 되면 커넥션풀을 획득을 위해 가지려고 하는 데드락을 발생하게 되고, 기능이 마비되는 경우가 발생할 수 있다.

     

    커넥션 풀을 늘리면 되지 않나요?

    DB커넥션을 잡고 있는 상황에서, REQUIRES_NEW를 가지는 메소드를 남발하게 되면 커넥션풀을 늘린다고 발생하지 않는다는 보장이 없어 위험해 보였다.

    그래서 이 방법도 사용하지 않았다. 

     

    버전 3 (최종)  - @TransactionalEventListener + BEFORE_COMMIT + RETRY

    이 방법은 위의 방법에서, REQUIRES_NEW를 사용하지 않은 방법이다.

    Redisson의 분산락 특성상 데이터 정합성이 떨어질 수 있는데, 작은 확률로 정합성이 틀어지는 부분을 Retry처리를 통해 잡았다.

     

    또한 Retry를 시도 후에도, 발생할 수 있는 부분은 예외 + 롤백을 통해서 원복이 처리되는 방식이다.

    간략화 된 시퀀스 다이어그램 V3

     

     

     


    참조

    더보기
    • 낙관락 비관락 비교
     

    [Database] 낙관적 락 / 비관적 락

    이름 그대로 비관적 락은 자원 경쟁을 비관적으로, 낙관적 락은 낙관적으로 본다. 비관적 락은 Repeatable Read 또는 Serializable 정도의 격리성 수준을 제공한다.트랜잭션이 시작될 때 Shared Lock 또는 Ex

    velog.io

    • 오라클 SELECT FOR UPDATE 
     

    [ORACLE] SELECT ~ FOR UPDATE 기능 및 용도

    SELECT ~ FOR UPDATE 정의 선택된 행들에 대해 배타적인 LOCK을 설정하는 기능이다. SELECT FOR UPDATE 문을 통해 커서 결과 집합의 레코드를 잠글 수 있다. 이 문을 사용하기 위해 레코드를 변경할 필요는

    tyrionlife.tistory.com

    • Mysql SELECT FOR UPDATE
     

    MYSQL __SELECT FOR UPDATE__

    목차SELECT FOR UPDATESELECT FOR UPDATE 의 옵션1. SELECT FOR UPDATEselect for update는 MySQL에서 사용되는 특별한 select 문의 한 종류이다.특정 레코드 또는 행을 select하면서 해당 레코드를 다른 트랜잭션에서 변

    systemdata.tistory.com

    • 동시성 처리
     

    [Spring] 스프링 동시성 처리 방법(feat. 비관적 락, 낙관적 락, 네임드 락)

    0. 들어가기 전 이전에는 DB 단의 동시성 처리 방법인 Lock에 대해서 알아봤습니다. https://ksh-coding.tistory.com/121 [DB] DB Lock이란? (feat. Lock 종류, 블로킹, 데드락) 0. 락(Lock)이란? 여러 커넥션에서 동시

    ksh-coding.tistory.com

    • @Transactional REQUIRES_NEW 이슈
     

    Spring Transaction REQUIRES_NEW Propagation 지옥 (with Mybatis Local session cache)

    무분별하게 설정 된 REQUIRES_NEW Propagation으로 인한 Connection Deadlock 현상

    medium.com

     

    '스프링' 카테고리의 다른 글

    AOP  (0) 2021.01.28