예를 들어 초콜릿을 아껴 먹으려고 10개를 서랍에 넣어두었습니다. 하지만 내가 모르는 사이에 동료들이 몰래 훔쳐먹었습니다. 나는 10개가 남은 것을 기대하고 서랍을 열었지만 빈 상자만 남았습니다.
이와 같은 현상은 DB에서 더욱 자주 발생합니다.데이터를 수정하여 저장했지만 다시 조회했을 때 다른 값이 반환되는 경우입니다. 이때 데이터의 무결성이 깨지고, 의도하지 않은 결과가 반환됩니다. 특히 값이 자주 바뀌는 경우엔 매우 심각한 문제로 다가옵니다. 쇼핑몰에서 상품의 재고수량이나 경매 시스템에서 실시간 호가를 하나의 row의 column값으로 사용한다면 동시성 문제는 매우 중요한 문제입니다. 상품이 더 팔리거나 항상 커져야할 경매호가가 낮아질 수도 있기 때문입니다.
동시성 문제 해결방법 🤔
비관적인 방법
현재 수정하려는 데이터가 언제든 다른 요청에 의해 수정될 가능성을 고려하여 해당 데이터에 Lock을 거는 방식입니다.
장점은 데이터의 무결성을 완벽히 지킬 수 있습니다. 수정할 데이터에 row level lock을 걸기 때문에 다른 요청에서 수정하는 것은 불가능합니다.
단점은 lock으로 인하여 이후의 다른 요청은 대기상태로 빠집니다. 기존의 lock의 transaction이 commit 또는 rollback으로 끝나면 대기하고 있던 요청을 비로소 실행합니다.
해당 방법은 서버의 성능에 따라 처리량이 결정되기 때문에 도입하기전 서비스의 최소 처리 tps에 부합하는지 확인해야합니다.
이 방법은 세가지 방식으로 적용할 수 있습니다.
첫 번째는 DB에서 제공하는 데이터 Lock 수준을 높이는 것 입니다. 일반적인 기본수준값인 Repeatable Read
를 Serializer
이상의 수준으로 올립니다. 강도 높은 락으로 완전한 일관성을 유지할 수 있습니다. 하지만, 이로인한 다른 요청들이 모두 취소됩니다. 취소된 작업들은 재시도를 위한 처리가 반드시 필요하기 때문에 서비스에 적용하는것엔 현실성이 부족합니다
set transaction isolation level read serializable;
두 번째는 select for update 활용하여 명시적으로 lock을 잡을 수 있습니다. 하지만 lock을 잡는 구간이 길어져 성능에 심각한 영향을 끼칩니다. 극장예매와 같은 서비스가 아니라면 사용하지 않는것이 좋습니다.
마지막은 Data의 transaction의 write lock을 활용하는 것입니다. 일반적으로 데이터를 수정할 때 write lock이 걸리고 transaction이 끝나야 lock이 풀리는 것을 이용합니다. 데이터의 일관성을 유지할 수 있지만 DB와 서버 성능에 따라서 서비스 속도가 좌우됩니다.
쇼핑몰 서비스에서 재고수량을 다음의 native query 또는 프레임워크에서 제공하는 형태의 ORM으로 적용할 수 있습니다.
UPDATE item_quantity
SET quantity = quantity - ${order count}
WHERE id=${id}
AND quantity >= ${order count}
Update 쿼리는 영향받은 data row수 를 반환하기 때문에 affected row count가 0인 경우에 재고부족에 대한 예외처리를 하면 됩니다.
현직 회사 재고 시스템도 동시성문제를 위와 동일한 방식으로 해결하였습니다.👍
낙관적인 방법
수정하려는 데이터는 나만 수정할 것이라는 낙관적인 생각의 방법입니다. 테이블에 version이라는 숫자컬럼 또는 updated_at 이라는 시간컬럼을 만들어서 수정될 때마다 1씩 증가하거나, 현재시간으로 갱신하게 해줍니다. 값을 수정할 때 Versino이 동일하면 수정이 가능해지고, 동일하지 않으면 수정에 실패합니다.
장점으로는 모델에 컬럼을 하나 추가하면 구현이 비교적 쉽습니다.
단점은 두개의 DB세션이 동일한 버전으로 수정하려고 하면 한 개의 세션에선 version conflict이 발생하여 affected row count가 0이 됩니다. 따라서 이 경우 요청을 재시도하도록 구현이 필요합니다.
UPDATE item_quantity
SET quantity = quantity - ${order count}
WHERE id=${id}
AND version=${version}
각 언어와 프레임워크마다 versioning을 자동으로 관리해주는 라이브러리들이 있기 때문에 찾아보는 추천합니다.
동시성 문제를 해결하는 방법은 다양하지만 적용하려는 서비스에 프로토타입으로 비교후 알맞는 방식을 택하여 문제를 해결하길 바랍니다. 🤗
감사합니다.