[재고 관리 #1] 트랜잭션, 락의 부하 최적화(feat. 실버 블릿은 없다.)

트랜잭션과 락을 어떻게 하면 성능을 최적화할 수 있을지 고민합니다.

🟤 프로젝트 개요

신뢰할 수 있는 데이터를 가공 및 제공하는 것은 서버 개발자의 필요한 역량입니다. 때문에 데이터의 정합성을 다룰 수 있는 트랜잭션 & 락은 필요한 기술입니다.

예전 경험을 살려 많은 근로자들이 동시에 주문을 처리해서 재고 히스토리를 업데이트 해야하는 프로젝트를 시작하게 되었습니다.


🌱 이슈 1 : 락을 통해 주문을 처리하기

매우 단순하게 Flow를 구성해보았습니다.

  1. 주문 데이터는 해당 주문이 처리되었는지의 상태(status)를 변경합니다.
  2. 1번의 데이터 수정과 함께 해당 주문 아이디에 포함되어 있는 아이탬 재고를 차감합니다.
  3. 2번이 완료된다면 모든 재고 처리는 완료되어 DB에 반영됩니다.

1 ~ 3까지의 Flow를 단순하게 쿼리만 요청한다면 중대한 이슈가 발생합니다.

stock-management1-001

트랜잭션과 락을 활용하지 않은 로직이라면 위와 같은 데이터 이슈가 발생합니다.

회원 A가 주문 1번을 처리하기 위해 상품 1의 재고가 10이라는 것을 확인합니다. 회원 B도 주문 2번을 처리하기 위해 상품 1의 재고가 10이라는 것을 확인합니다.

회원 A가 10이라는 재고에서 차감하여 9로 업데이트합니다. 회원 B가 재고를 차감하면 회원 A가 처리하여 반영된 재고가 아닌 이전에 조회한 10이라는 재고에서 차감한다. 즉, 회원 B가 재고를 차감하여 업데이트하면 회원 A가 처리한 재고가 반영되지 않으므로 데이터 정합성에 어긋나게 됩니다.

위의 데이터 정합성 문제를 해결하기 위해서는 재고처리할 상품 데이터에 락을 거는 방법이 있습니다.

Record 락은 Row 하나의 데이터를 update 처리할 때 락을 겁니다. 해당 update가 있는 트잰잭션이 완료되기 전까지는 다른 트랜잭션 쿼리들이 락이 걸린 Row 데이터를 접근, 수정할 수 없고 락이 풀릴 때까지 대기합니다(exclusive lock).

위의 Flow는 락으로 해결할 수 있습니다.

stock-management2

🟤 Lock을 적용시키는 방법

Lock을 적용하는 방법은 의외로 간단합니다. 해당 프로젝트에서는 JPA를 사용하지 않기 때문에 쿼리문에서 exclusive lock을 걸어야합니다.

select * from item where item_id = 1 for update; 

위의 쿼리를 실행하게 된다면 위의 쿼리를 포함하는 하나의 트랜잭션이 마쳐질 때까지 다른 트랜잭션의 쿼리에서 item_id = 1인 데이터에 접근하지 못하게 됩니다. 따라서 item_id의 재고를 순차적으로 차감하여 데이터 정합성을 지킬 수 있게 됩니다.


🌱 트랜잭션 & 락의 한계

상품 아이디에 배타적 락을 사용한 방법은 일핏 보기에는 문제를 해결한 것처럼 보입니다. 하지만 이 방법은 더 큰 문제를 야기합니다.

🟤 데드락

만약 주문 데이터에 상품이 하나씩 존재했다면 데드락이라는 문제는 발생하지 않았을 것 입니다. 하지만 하나의 주문에는 여러 상품들이 담겨 있고 여러 상품들을 처리할 때 각 상품마다 락을 걸게 된다면 문제가 발생합니다.

stock-management3

  1. 회원 A가 주문 1을 처리하기 위해 상품 1을 조회하여 락을 겁니다. 그 사이 회원 B가 주문 2를 처리하기 위해 상품 2를 조회하여 락을 겁니다.
  2. 회원 A는 상품 1을 처리 완료하고 상품 2를 처리하려고 했지만 회원 B가 이미 락을 걸어놓았기 때문에 대기합니다.
  3. 마찬가지로 회원 B가 상품 2를 처리하고 상품 1을 처리하려 했지만 회원 A가 상품 1에 락을 걸어놓았기 때문에 대기합니다.
  4. 회원 A와 회원 B의 주문 처리는 교착상태에 빠져 더이상의 처리가 어렵게 되었습니다.

🟤 해결 방법

현재 구조에서 데드락을 해결하기 위해서는 다음과 같은 방법들이 존재합니다.

  1. 예방 기법
    • 예방 기법은 트랜잭션을 실행하기 전에 필요한 모든 데이터를 미리 Lock을 거는 것입니다.
    • Set Lock Timeout을 설정하여 Lock이 걸려있는 데이터에 접근할 때 대기 시간을 정합니다. 일정시간 대기한 후 계속 락이 걸려있다면 해당 요청을 취소합니다.

=> 예방 기법은 데드락으로 인해 정지된 요청을 취소하여 다시 서버가 동작하도록 하지만, 근본적인 해결책이 아닙니다.

  1. 주문 처리 메소드를 synchronized로 동기적 호출하기
    • synchronized로 주문 처리 메소드를 동기적으로 호출하게 되면 주문을 동기적으로 처리하기 때문에 상품 간 데드락이 발생하지 않습니다.
    • 하지만 이 방법은 단일 서버에서만 적용 가능하며, Java/Spring의 멀티 스레드의 이점을 살리지 못한다는 단점을 가지고 있습니다. (성능 이슈)

성능을 고려하지 않고 처리한다면 synchronized로 요청을 하나씩 수행하는 것이 안전할 것입니다. 하지만 고성능, 대규모 트래픽이 발생하는 경우에는 Scale out이나 병렬적 처리 등이 필요하기 때문에 필연적으로 구조를 수정해야합니다.


🌱 정리

아직 찾지 못한 이슈들이 있을 수 있지만 위의 2문제 (특히 데드락)만으로도 개선의 여지는 매우 충분합니다.

이번 기회에 실버블릿(신의 은총)은 없다라는 것을 많이 느낍니다. 트랜잭션, 락 이라는 개념만 봤을 때는 매우 편리한 기능입니다. 구글링을 통해서도 정말 많은 자료들이 쏟아져나오는만큼 매우 좋은 기술이기 때문에 소개되는 것이 아닌가? 생각이 들었습니다만… 그건 단순한 생각에 불과했습니다. 🥲

규모가 있는 트래픽이 발생한다는 가정하에 현재 구조 및 코드를 개선해보고자 합니다.

보통 위와 같은 상황이 발생하면 모든 것을 기술로 풀어내고자 하는 마음이 들 것 같습니다. 하지만 기술보다 우선적으로 생각해볼 문제는 현재 구조나 사용하는 제품이 적당한가?를 먼저 고민해보아야 한다고 생각합니다.

RDBMS는 애초에 재고 관리에 적합한 데이터베이스는 아닙니다. 대표적인 예로 RDBMS는 Disk에 데이터를 저장하기 때문에 기본적으로 I/O가 발생하는데 데이터를 변경하는 I/O가 수시로 요청되면 변경에 대한 부하가 증가하게 되어 성능에 좋지 않습니다.

만약 다른 개발자, 기획자 등의 동료들과 협의하여 처리 프로세스를 변경할 수 있는 여유가 충분하다면 적극적으로 프로세스 개선 의견을 내비추는 것이 효율적일 것입니다. 프로세스 개선은 추후 제품 유지보수와 더 나은 아키텍쳐 등을 확보할 수 있기 때문입니다.

기술은 문제 해결을 위한 도구라는 수시로 되뇌여야 합니다. 개발자는 문제를 해결하는 사람이라는 명언?이 때문에 기술뿐 아니라 동료와의 소통 등을 통해 문제를 다방면으로 접근하여 해결하는 자세를 갖출 수 있게 도메인을 이해하는 등의 노력이 필요하다고 생각합니다.

  • 기술적으로 풀어내기

데드락이라는 이유로 인해 동기화 등의 방법을 사용하여 성능이 대폭 하락하게 되었습니다. 이 상황을 프로세스를 변경하는 것뿐 아니라 기술적으로는 어떻게 풀어낼 수 있을지 고민하게 되었습니다.

그 중 배의 민족, 쿠 등에서 도입한 다양한 방법들을 접하고 프로젝트에 도입한 사례를 소개합니다.


🌱 정리

모든 기술에는 트레이드 오프가 있으며 각 상황에 맞는 기술적, 구조적 선택을 하는 자세가 필요할 것 같습니다. 절대 기술이란건 존재하지 않기 때문에 유연하게 생각하고 문제를 해결할 수 있는 다양한 방법들을 접하는 것이 중요하다고 느끼게 되었습니다.


© 2021. All rights reserved.