본문 바로가기
👨‍🏫일문일답

Concurrency Issue and Thread-safety

by 캔 2024. 4. 6.

자원 공유 시 발생하는 문제인 동시성 문제(concurrency issue)에 대해 알아본다.

 

여러 스레드 및 애플리케이션에서 하나의 자원을 동시에 사용하려고 하면 경쟁 상태(race condition)가 발생한다. 이를 동시성 문제라고 하고 이런 상황이 발생되지 않는 성질을 스레드 안전성(thread-safety)라고 한다. 이를 달성하기 위한 방법에 대해 살펴보려고 한다.

 

Java API를 이용한 해결 방안

- synchronized - 메서드나 코드 블록에 synchronized 예약어를 사용하여 락을 걺

- 하나의 프로세스에서만 락이 가능하고 여러 프로세스 또는 앱에서 접근 시 사용 불가

- 따라서, 여러 애플리케이션 혹은 서버가 데이터에 접근해야 하는 실무 환경에서는 거의 사용되지 않음

 

Database(RDBMS)를 이용한 해결 방안

- pessimistic lock - 실제로 데이터에 락을 걸기

  - 락을 걸면 다른 트랜잭션에서는 락 해제 전까지 접근 불가.

  - 데드락 발생 가능성 있으므로 주의해서 사용해야 함

  - SQL의 'for update' 또는 스프링 데이터 @Lock 애너테이션(LockModeType.PESSIMISTIC_WRITE)

  - 충돌이 빈번하면 optimistic 보다 성능 좋음. 데이터 정합성 보장.

  - 별도 락 생성으로 성능 감소

- optimistic lock - 실제 락이 아닌 "버전" 사용

  - 버전이 다르면 수정 실패

  - 버전 칼럼 추가하고 javax.persistence.Version(혹은 jakarta.persistence.Version) 애너테이션 사용. 쿼리 메서드에 스프링 데이터 @Lock 애너테이션(LockModeType.OPTIMISTIC)

   - 별도의 락을 설정하지 않아 성능상 이점 있음

   - 수정 실패 시 별도의 재시도 로직 필요

   - 충돌이 빈번하게 일어나지 않을 것으로 예상되면 optimistic lock을 더 권장

- named lock - 이름을 가진 메타데이터 락

   - 트랜잭션 끝나도 락이 해제되지 않으므로 별도로 해제해줘야 함

   - 데이터가 아닌 별도의 공간에 락 생성 

   - MySql - get_lock() 명령어로 락 획득,  release_lock()로 해제

   - 락 생성 쿼리는 별도의 데이터 소스 사용해야 커넥션 풀 부족 현상 없음

   - 주로 분산 락 구현 시 사용

   - pessimistic lock보다 타임아웃 구현 용이

   - 구현 방법 복잡

 

Redis를 활용한 해결 방안

- 분산 락 활용

- Lettuce 라이브러리

  - setnx 명령어 - set if not exist. 기존 값이 없을 때만 set.

  - 락 가능한지 확인하면서 시도

  - spin lock 방식 -> 재시도 로직 작성 필요

  - 구현이 간단한 편

  - spin lock 방식이므로 Redis에 부하를 줄 수 있으므로 요청 간격 두어야 함

- Redisson 라이브러리

  - pub-sub 기반 락 구현 - 채널을 생성하고 락 점유 중인 스레드가 락 획득 대기 중인 스레드에게 해제 알림

  - Lettuce는 계속 락 획득 시도하지만, Redisson은 한 번 혹은 여러 번만 시도

  - pub-sub 기반이므로 Redis 부하 줄여줌

  - 구현이 복잡한 편

  - 별도 라이브러리 사용 필요(스프링은 기본적으로 Lettuce 라이브러리 사용)

- 재시도가 필요하지 않다면 Lettuce가 유리, 재시도가 필요하면 Redisson이 유리

 

RDBMS vs. Redis

- RDBMS

  - 이미 RDBMS 사용 중이라면 별도 비용 필요 없음

  - 일정 수준 트래픽까지는 ok

  - Redis 보다 성능이 좋지 않음

- Redis

  - Redis 구축 비용 있음

  - Redis 보다 성능 좋음