재고 관리에서의 동시성 이슈 해결

크리링·2025년 5월 8일
0

실무 트러블 슈팅

목록 보기
9/10
post-thumbnail

현재 매장의 재고 관리 서비스를 하고있는 스타트업에 서버 개발자로 일하고 있다. 새로운 프로젝트인 식자재 쇼핑몰을 시작하고, 개선 중인 중에 고민이 생겼다.

기존에 매장 재고 관리에서는 한 매장에서 주문이 동시에 들어오거나 동시에 입고되는 등의 일이 드물어 동시성을 딱히 고민하지 않았다.

하지만 이번 식자재 쇼핑몰 프로젝트에서는 나중에 이벤트를 통한 쿠폰 발급 같은 것도 신경을 써달라는 말을 듣기는 했다. (다소 의역)

그럼 어떻게 동시성 이슈를 해결할 수 있을지 먼저 알아보고 선택하자.






상황

100개의 재고 차감 요청이 동시에 들어온다.

테스트코드는 이렇게 작성했다.

간단하게 코드를 설명하면

  • ExecutorService
    • 비동기로 실행하는 작업을 단순하게 사용하게 도와주는 자바 API
  • CountDownLatch
    • 다른 스레드에서 수행중인 요청이 끝날때까지 기다려주는 메소드

기존의 단순한 차감 형태의 코드에서는 잔여 수량 검증 로직이 맞지 않는다.

그 이유는
RaceCondition

  • 둘 이상의 엑세스가 공유 데이터에 동시 변경하고 조회할때 생기는 문제
  • 하나의 쓰레드가 완료되고 다음 쓰레드 접근

어떻게 해결할 수 있을까?






Synchronized

잘 알고있는 해당 차감 메소드에 동기화 선언을 해준다.

메소드에 선언하고 다시 테스트를 실행하면

테스트는 실패한다.
이유는 스프링의 @Transactional 동작 방식 때문인데
트랜잭션 종료 전에 메서드 호출이 가능해서 문제가 발생한다.

그러기에 @Transactional을 주석 처리하면

테스트 케이스는 성공한다.

그러나
동기화는 하나의 프로세스 안에서만 보장된다.
서버가 여러대일 때는 실패할 수 밖에 없다.



MySQL

그러면 MySQL의 락으로 해결해보자

Pessimistic Lock

  • 실제로 데이터에 락을 걸어서 정합성을 맞추는 방법.
  • exclusive lock 을 걸게되면 다른 트랜잭션에서는 lock이 해제되기 전에 데이터를 가져갈 수 없음
  • 로우나 테이블 단위 락
  • 데드락이 걸릴 수 있으므로 주의

장단점

  • 충돌이 빈번하다면 Optimistic Lock 보다 성능이 좋다.

  • 락을 통해 업데이트 제어해 데이터 정합성을 지킬 수 있다.

  • 별도의 락을 잡기 때문에 성능 감소가 있을 수 있다.



Optimistic Lock

  • 실제로 락을 이용하지 않고 버전을 이용함으로써 정합성을 맞추는 방법.
  • 데이터를 읽은 후에 update를 수행할 때 현재 내가 읽은 버전이 맞는지 확인하며 업데이트
  • 내가 읽은 버전에서 수정사항이 생겼을 경우 application에서 다시 읽은 후에 작업을 수행

엔티티에 version 어노테이션 추가


재시도 기능 분리를 위한 퍼사드 구현 및 실행


(CPU 99.9%까지 도달해서 테스트 검증은 중지)

장단점

  • 재시도 로직으로 이전보다 오래 걸림
  • 별도의 락을 가지지 않아 성능 상 Pessimistic 락보다 이점
  • 재시도 로직 개발자가 직접 작성



Named Lock

  • 이름을 가진 metadata locking
  • 이름을 가진 락을 획득후 해제할 때까지 다른 세션은 이 lock을 획득할 수 없도록 합니다.
  • transaction이 종료될 때 lock이 자동으로 해제되지 않습니다.
    -> 별도의 명령어로 해제를 수행하거나 선점시간이 끝났을 경우 해제
  • 메타데이터 단위 락

Perssimistic LockNamed Lock은 비슷해보이지만
Perssimistic Lock은 로우나 테이블 단위 락이고 Named Lock은 메타데이터 단위 락


퍼사드 생성


부모(퍼사드)의 트랜잭션과 별도로 실행되기 위해 propagation 실행

같은 데이터 소스 사용으로 커넥션 풀 최대 설정 수정

장단점

  • Named락주로 분산락 구현시 사용

  • Pessimistic 락은 타임 아웃 구현이 힘들지만 타임 아웃 설정 가능

  • 락 해제를 잘 해야돼서 구현이 어려움






Redis

설치방법

도커를 통한 레디스 설치

docker pull redis

# redis 6379 기본 포트로 사용
docker run --name myredis -d -p 6379:6379 redis

# 
docker ps

레디스 실행

gradle에 redis 의존성 추가

dockr ps 명령어의
redis 컨테이너 아이디 복사
redis-cli 실행

Lettuce

  • setnx 명령어를 활영하여 분산락 구현
  • spin 락 방식 (Named Lock과 유사)
  • 락 획득 못 했을때 별도의 재시도 필요
  • 레디스 이용한다는 점과 세션 관리 신경 안 써도 됨


Lock 관리를 위한 redisRepository 생성


퍼사드 생성

  1. NamedLock 방식과 동일해서 NamedLock 서비스 구현체 의존성 주입
  2. 반복된 접근은 레디스에 부하를 주기에 실패시 0.1초 슬립으로 락 획득에 텀 추가



Redisson

  • pub-sub 기반으로 락 구현 제공


1. mvnrepository.com 접속
2. redisson 검색
3. Redisson/Spring Boot Starter 접속
4. 의존성 복사

RedissonLockFacade 추가

  • pub-sub 구현이기에 레디스의 부하를 줄여줌
  • 구현 복잡
  • 별도 라이브러리 사용해야함






결론

어떤걸 쓰는게 맞는걸까?
내가 내린 결론은 서비스마다 다르다이다.

하지만 서비스 특성상 B2B이고, 많은 유저가 동시에 트래픽이 몰리는 걱정을 할 정도는 아니라고 생각이 들어 Pessimistic Lock 을 사용하였고, 후에 더 많은 트래픽이나 이슈가 발생하면 Optimistic Lock을 적용하고 더 나아가 레디스 사용을 고려하려고 한다.
(레디스를 너무 써보고 싶다.)






코드

재고 관리 동시성 처리 예시

0개의 댓글