Changsoon's Note Backend Developer

동시성 이슈 해결방법 정리

동시성 이슈란?

동시성 이슈는 여러 스레드가 동시에 공유 자원에 접근하거나 수정할 때 발생할 수 있는 문제입니다.

데이터 불일치, 프로그램 충돌, 비정상적인 동작 등이 발생할 수 있기 때문에 유의하고 코드를 작성해야 한다.

해당 내용을 예제코드로 알아보자.

예제 코드 레포지토리 주소

동시성 이슈 발생

먼저 계좌에서 출금을 하는 동작으로 동시성 이슈가 발생하는 경우를 알아보자.

100개의 요청이 들어오는 경우 100개가 빠져서 0을 기대하지만 실제 잔고는 기대와는 다르게 출력된다.

이러한 동시성 이슈를 해결하는 방법을 알아보자.

synchronized

자바에서 synchronized 키워드는 멀티스레드 환경에서 동시성 문제를 해결하기 위해 사용한다.

이 키워드는 특정 코드 블록에 대해 한 번에 하나의 스레드만 접근하도록 보장하여, 여러 스레드가 동시에 해당 코드에 접근할 때 발생할 수 있는 데이터 충돌이나 일관성 문제를 방지한다.

synchronized 키워드를 메서드에 적용하면 해당 메서드를 호출할 때, 객체나 클래스(정적 메서드)에 대한 락을 걸어서 다른 스레드가 해당 메서드를 동시에 실행하지 못하게 한다.

코드 블록에 적용하면, 그 코드 블록에만 락을 걸어 다른 스레드가 동시에 접근하는 것을 방지한다.

내장 클래스

자바 내장 Lock 인터페이스와 스프링 내장 인터페이스 LockRegistry를 활용하는 방법으로도 동시성 이슈를 해결할 수 있다.

Lock은 java.util.concurrent.locks 패키지에 포함되어 있으며, ReentrantLock이 대표적인 구현 클래스입니다.

Lock 객체는 명시적으로 lock()과 unlock() 메서드를 호출하여 락을 얻고 해제할 수 있다.

tryLock() 메서드는 대기 시작을 제한하거나, 특정 조건에서 비차단식으로 락을 시도할 수 있다.

Lock은 interrupt()를 호출한 경우, 락을 얻지 못했을 때 스레드를 종료할 수 있다.

Spring에서는 LockRegistry를 제공한다. 가장 흔히 사용하는 구현체는 DefaultLockRegistry다.

이 클래스는 로컬 락이나 분산 락을 제공할 수 있다.

Lock과 마찬가지로 명시적으로 락을 획득하고 해제할 수 있고 executeLocked 메서드를 사용하면 지정된 자원에 대해 락을 획득하고, 락을 해제하는 과정까지 자동으로 관리한다.

낙관적 락

낙관적 락은 데이터 충돌이 자주 발생하지 않을 것이라고 가정하고, 락을 미리 걸지 않고 작업을 먼저 시도하고 나서 충돌이 발생했을 때 처리하는 방식이다.

즉, 작업을 수행하기 전에 락을 걸지 않고, 충돌이 발생할 때만 오류를 발생시켜 해결하는 방식이다.

낙관적 락의 장점은 락을 사용하지 않기 때문에 성능 저하가 적고, 높은 동시성 환경에서도 효율적으로 동작한다.

하지만 충돌이 발생할 경우 롤백이 필요하며, 데이터 충돌이 높을 경우 오히려 성능에 좋지 않다.

사용 방법은 데이터나 엔티티에 버전 정보를 추가하여, 사용자가 데이터를 수정할 때 버전 번호를 체크하는 방식으로 구현한다.

Spring Data JPA에서 @Lock(LockModeType.OPTIMISTIC_WRITE)로 설정할 수 있다.

비관적 락

비관적 락은 낙관적 락과 반대로 데이터 충돌이 발생할 가능성이 높다고 가정하고, 충돌이 발생하기 전에 데이터를 잠가 버리는 방식이다.

즉, 락을 먼저 걸고 작업을 수행해서 데이터 충돌을 완전히 방지하려는 접근이다.

비관적 락의 장점은 데이터 충돌을 완전히 방지한다는 점이다. 하지만 단점으로는 락을 걸고 해제하는 과정에서 대기 시간이 발생하여 성능이 저하될 수 있다.

또한 높은 동시성 환경에서는 병목 현상이 발생할 수 있다.

사용법은 DB에서 SELECT FOR UPDATE와 같은 SQL 명령을 사용하여 데이터베이스에서 레코드를 수정할 때 락을 걸어 다른 트랜잭션이 해당 데이터를 수정하지 못하도록 한다.

Spring Data JPA에서 @Lock(LockModeType.PESSIMISTICE_WRITE)로 설정할 수도 있다.

Named Lock

Named Lock은 데이터베이스에서 특정 리소스를 잠그는 데 사용된다.

데이터베이스의 SELECT FOR UPDATE와 유사한 기능을 제공하지만, 이름을 기반으로 잠금을 걸 수 있어, 더 유연한 방식으로 잠금을 관리할 수 있다.

@Query 애너테이션을 사용하여 명시적인 Named Lock을 설정할 수 있다.

MySQL의 GET_LOCK 함수를 이용하여 이름 기반 잠금을 설정할 수 있다.

Redis Lettuce 분산 락

Redis Lettuce 분산 락은 Redis를 사용해서 구현한다.

Redis는 기본적으로 단일 스레드로 동작하기 때문에, 분산 환경에서 여러 애플리케이션 인스턴스가 동시에 같은 자원에 접근할 때 Lettuce 클라이언트를 통해 분산 락을 구현할 수 있다.

Lettuce는 Java에서 Redis와의 연결을 제공하는 비동기 Redis 클라이언트이다.

Lettuce 클라이언트를 사용하여 분산락을 구현할 때는, 주로 SET 명령을 사용해 락을 설정하고 일정 시간 후 자동으로 해제되는 방식으로 동작한다.

SETNX(Set if Not Exists) 명령을 사용해 분산 락을 구현할 수 있다.

Redis Redisson 분산 락

Redisson은 Redis를 위한 고급 클라이언트로, 분산 시스템을 구축할 때 유용한 기능들을 제공한다.

Redis의 기본적인 SETNX 방식보다 더 높은 수준의 추상화를 제공하며, 분산 환경에서 락을 쉽게 구현할 수 있도록 도와준다.

Redisson은 ReentrantLock을 사용하여 락을 구현한다.

Redisson은 Redis의 잠금을 기본적으로 비동기 방식으로 처리하고, Java의 ReentrantLock과 유사한 방식으로 동작한다.

Redisson 분산 락은 하나의 락을 여러 노드에서 공유할 수 있게 해준다.

특징으로는 락을 설정할 때 타임아웃을 지정할 수 있으며, 동일한 스레드가 이미 획득한 락을 재진입할 수 있다.

또한 락의 유효성을 지속적으로 검사하고, 잘못된 락을 자동으로 해제한다.