티스토리 뷰


생각을 정리하는 의식적인 연습을 위해 지난번 글부터 하나를 작성하는 데 1시간만 투자해보고 있습니다. 아직 세밀한 디테일을 챙기거나 충분한 부연 설명을 덧붙이는 것은 어렵게 느껴지지만 기존의 생각을 빠르게 정리하는 데에는 큰 도움이 된다고 느껴 당분간은 이 방식을 유지해보고자 합니다.

 

 

Optimistic Locking

Optimistic Locking(낙관적 잠금, 또는 Optimistic Concurrency Control)은 비관적 잠금(Pessimistic Locking)과 달리 잠금을 사용하지 않는 동시성 제어 방법으로 트랜잭션 간의 충돌이 자주 발생하지 않을 것이라는 가정하에 고안되었습니다. 이 기법이 적용된 대표적인 예로는 NoSQL인 MongoDB의 WiredTiger 스토리지 엔진이 있습니다.

 

WiredTiger는 RDBMS처럼 트랜잭션 기능을 제공하는데 락 경합으로 발생하는 병목 현상을 줄이기 위해 Optimistic Locking을 활용하고 있습니다. (그 외에도 Lock-Free 알고리즘을 많이 활용하고 있으니 살펴보시길 바랍니다) WriedTiger는 기본적으로 Repeatable-read 격리 수준을 유지하면서 업데이트 충돌이 발생하면 이를 감지할 수 있도록 구현되어 있고, MongoDB 4.2부터는 Callback API를 통해 Retry를 알아서 수행하도록 통합되어 사용자가 직접 Callback Handler를 구현하지 않아도 되게끔 개선되었습니다.

 

 

MongoDB로 보는 Optimistic Locking의 한계

이때 락 경합에 의한 병목 현상을 해소하고자 한 것은 좋았으나 서비스에 따라 앞서 이야기했던 "트랜잭션 간의 충돌이 자주 발생하지 않을 것이라는 가정"이 부합되지 않는 경우가 발생하게 되었습니다. Optimistic Locking을 사용하면서 성공할 때까지 매번 Retry를 수행한다는 것은 매번 실패한 Transaction이 수행되기 전으로 Resorce를 Rollback 해야 한다는 의미인데, Resorce 경합이 과도하게 발생할 경우 수 많은 Transaction이 계속 재시도되고 복구되는 상황에 빠지기 때문입니다. (WiredTiger는 Transaction에서 다루는 데이터를 Shared Cache에 담아놓고 Checkpoint라는 기법을 통해 Commit 또는 Rollback을 수행합니다)

 

매번 Shared Cache의 메모리 영역의 데이터를 이전 시점으로 복구시키는 동작의 성능 부하와 완료되지 못한 Transaction이 계속 늘어남에 따라 할당된 Shared Cache의 여유 공간이 없어지는 것 또한 또 다른 장애로 번질 수 있는 상황까지 야기하게 되었습니다. (이 경우 수행되던 Transaction이 갑작스래 중단되는 경우도 있습니다.)

결국 MongoDB 6.2부터는 transactionTooLargeForCacheThreshold라는 임계값을 지정하고, 지정된 임계값을 넘었을 때 TransactionTooLargeForCache를 발생시켜 더 이상 Retry를 수행하지 않도록 변경되었습니다.

 

MongoDB의 사례를 돌아보면 Optimistic Locking 기법은 특정 리소스에 대해 데이터 경합이 낮은 경우에만 고려되어야 하는 기법임을 명확히 알 수 있고 현재 서비스에서 대량의 트래픽이 발생하고 특정 리소스에 경합이 발생할 때 Optimistic Locking과 Retry 전략을 고민하고 있다면 적절하지 못한 고민이라 볼 수 있습니다..

 

 

그럼 언제 사용할 수 있을까요?

위와 같은 단점에도 Optimistic Locking을 활용할 수 있는 사례는 여러가지 있겠죠 (최종 값이 반영되는 것이 아닌 지속적으로 증감되는 것, 경합이 발생할 여지는 적지만 갱신 유실이 생겨서는 안되는 것 등). 제가 실제로 제품에 도입한 사례는 원장 테이블에 대한 Distributed Lock과의 혼용 사례입니다. "왜 Pessimistic Locking의 구현체를 사용하고 있으면서 OptimisticLocking까지 사용하는 거지?"라고 의아하게 생각하실 수도 있는데 Distributed Lock에는 필연적으로 신뢰할 수 없는 영역이 존재하기 때문입니다.

Distributed Lock에 접근하는 다수의 서버 인스턴스가 있고, 경합이 자주 발생하는 Resource 이며, 각 서버 인스턴스의 GC Cycle이 다르고 (e.g. Lock을 점유한 뒤 STW가 발생하여 Commit 전에 Lock의 유지 시간을 초과하는 등의 상황), Distributed Lock을 관리하는 저장소의 장애 및 데이터 유실 등이 발생하였을 때 등 다양한 요인으로 인해 아무리 Distributed Lock이 걸려있다고 할지라도 동일 Resorce에 대한 중복 요청 및 갱신 유실은 충분히 발생할 수 있습니다.

 

이때 각 Resource에 Version을 부여하고 갱신 작업을 충돌시킨 뒤 Retry를 수행하지 않는다면 최소한 데이터 정합성은 보장할 수 있게 됩니다. (유사하게 Lock에 대한 Version을 부여하여 과거의 요청을 거부, 무시하는 fencing token 기법도 있습니다) 이렇게 부여된 Version은 추후에 Domain Event, Event Store와 통합하여 활용하는 Event Sourcing 구조로 개선할 때에 동일 리소스에 대한 변경을 추적 또한 손쉽게 달성할 수 있는 장점도 있습니다.

 

 

Reference

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/10   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함