Spring에서 동시성 이슈 해결 방법(MySQL, Redis 이용하기)

애플리케이션에서 여러 사용자가 동시에 같은 데이터를 수정하려 할 때, 동시성 문제(Race Condition)가 발생할 수 있습니다. 이런 문제를 해결하기 위한 방법으로 여러 접근 방식이 있습니다. 이번 글에서는 Spring에서 동시성 문제를 해결하기 위한 몇 가지 대표적인 방법을 살펴보겠습니다.

 

1. synchronized 키워드를 이용한 해결

Java에서 가장 기본적인 동시성 처리 방법 중 하나는 synchronized 키워드를 사용하는 것입니다. 이를 통해 하나의 프로세스 내에서 동시에 여러 스레드가 동일한 리소스에 접근하지 못하도록 제어할 수 있습니다. 아래는 synchronized를 사용한 예시입니다.

public synchronized void decreaseQuantity(Long id, Long quantity) {
    stockService.decreaseQuantity(id, quantity);
}

위 코드는 간단히 동시성을 제어할 수 있지만, 단일 프로세스 에서만 동작합니다. 즉, 서버가 여러 대일 경우 서로 다른 서버에서 같은 데이터를 동시에 수정하려는 상황을 막을 수 없습니다. 이는 대부분의 시스템이 여러 서버로 구성되어 있기 때문에 실무에서 사용하기에 적합하지 않습니다.

 

2. 데이터베이스 락을 활용한 해결

MySQL과 같은 데이터베이스에서 락(Lock)을 활용해 동시성을 제어할 수 있습니다. 데이터베이스 락은 동시 수정이 발생할 가능성이 있는 상황에서 주로 사용됩니다. 대표적인 데이터베이스 락 방법으로는 아래와 같은 종류가 있습니다.

 

2.1. 비관적 락(Pessimistic Lock)

비관적 락은 데이터를 수정할 때 미리 락을 걸고, 다른 트랜잭션이 해당 데이터를 수정할 수 없도록 막습니다. 이는 충돌이 자주 발생하는 환경에서 효과적이지만, 성능에 영향을 미칠 수 있습니다.

@Lock(LockModeType.PESSIMISTIC_WRITE)
public Stock findById(Long id);

이는 SQL 쿼리에서 FOR UPDATE 구문을 사용하여 락을 거는 방식으로 동작합니다. 비관적 락은 충돌이 자주 발생하는 경우에 적합하지만, 성능 저하를 유발할 수 있습니다.

 

2.2. 낙관적 락(Optimistic Lock)

낙관적 락은 데이터를 수정하기 전에 먼저 데이터를 읽고, 나중에 데이터를 업데이트할 때 다른 트랜잭션이 해당 데이터를 수정했는지 여부를 확인합니다. 버전 번호를 사용하여 데이터의 변경 여부를 확인하는 방식입니다.

@Version
private Long version;

이 방식은 락을 걸지 않기 때문에 성능에 이점이 있지만, 업데이트가 실패했을 때 재시도 로직을 개발자가 직접 작성해야 합니다.

 

2.3. 네임드 락(Named Lock)

네임드 락은 데이터베이스의 메타데이터에 락을 걸어, 해당 락을 명시적으로 해제할 때까지 다른 트랜잭션이 그 락을 획득할 수 없도록 합니다. 트랜잭션이 종료될 때 자동으로 해제되지 않으므로, release_lock() 명령어로 락을 해제해야 합니다. 네임드 락은 주로 분산 환경에서 락을 관리할 때 유용합니다.

SELECT GET_LOCK('lock_name', 10);
-- GET_LOCK 함수는 'lock_name'이라는 이름의 네임드 락을 시도합니다.
-- 이 락은 분산 시스템에서 동시에 여러 스레드나 프로세스가 동일한 리소스에 접근하지 못하도록 제어할 때 사용됩니다.
-- 두 번째 인자인 10은 락을 획득하기 위해 최대 10초 동안 대기한다는 의미입니다.
-- 반환값:
--   1: 락을 성공적으로 획득한 경우
--   0: 락 획득에 실패한 경우 (10초 내에 다른 프로세스가 락을 해제하지 않음)
--  -1: 에러 발생 시 (권한 부족 등)

네임드 락은 주로 JPA에서 네이티브 쿼리를 사용하여 구현되며, 락을 걸고 해제할 때는 커넥션 풀이 부족해질 수 있기 때문에 락 전용 데이터 소스를 사용하는 것이 좋습니다. 동일한 데이터 소스를 사용하면 락이 오래 걸리거나 대기 상태가 길어져 다른 트랜잭션에 영향을 줄 수 있습니다.

 

 

3. Redis를 활용한 분산 락

분산 환경에서 동시성 문제를 해결하는 또 다른 방법으로 Redis를 이용한 분산 락이 있습니다. Redis는 빠른 속도와 함께 분산 시스템에서 락을 관리하기에 적합한 도구입니다. 이를 사용하여 여러 서버 간에 동시성 문제를 해결할 수 있습니다.

 

3.1. Lettuce 기반 락

Lettuce는 setnx 명령어를 사용하여 분산 락을 구현합니다. setnx는 해당 키가 존재하지 않을 때만 값을 설정하는 명령어로, 락을 관리할 수 있습니다. 다만, 이 방식은 Spin Lock 방식으로 락을 획득하려는 스레드가 지속적으로 락을 요청하기 때문에 Redis에 부하를 줄 수 있습니다.

redisTemplate.opsForValue().setIfAbsent(key, value, timeout);

 

3.2. Redisson 기반 락

Redisson은 Redis의 Pub/Sub 기능을 활용하여 락을 구현합니다. Pub/Sub은 채널을 구독한 스레드가 락을 해제할 때 메시지를 받아 락을 획득하는 방식으로 동작합니다. 이를 통해 Redis에 과도한 부하를 줄이고 효율적으로 락을 관리할 수 있습니다.

RLock lock = redissonClient.getLock("lock_key");
lock.lock();

Redisson은 재시도 로직을 자동으로 관리해주기 때문에 Lettuce보다 사용하기 편리합니다.

 

4. 실무에서의 선택

  • 충돌이 빈번하지 않은 경우: 낙관적 락(Optimistic Lock)이 성능상 이점이 있습니다.
  • 충돌이 빈번한 경우: 비관적 락(Pessimistic Lock)이 데이터 일관성을 보장하기에 적합합니다.
  • 분산 환경에서의 락: Redis 기반의 분산 락(Redis Lock)을 추천합니다. Lettuce와 Redisson 중 Redis 부하를 줄이고 싶다면 Redisson을 사용하는 것이 좋습니다.

 


참고

인프런 '최상용'님의 '재고시스템으로 알아보는 동시성이슈 해결방법'을 수강하고 정리한 글입니다.