Skip to main content

똑닥 멤버십과 MongoDB 트랜잭션 충돌 방지 방법

· 15 min read
허민지

안녕하세요, 백엔드팀 허민지입니다.

오늘은 똑닥에서 멤버십 서비스를 도입하게 된 배경과 함께, 멤버십 런칭 과정에서 새롭게 적용한 MongoDB Transaction, 그리고 그 과정에서 발생했던 에러 사례와 이를 예방하기 위한 방법에 대해 이야기해보려고 합니다.

비브로스에서 멤버십을 시작한 이유

비브로스는 2023년 8월 2일 멤버십 서비스를 정식 출시했습니다. 무료 서비스에서 유료 멤버십으로 전환하게 된 주요 이유는 '운영 자금' 확보가 절실했기 때문입니다. 글로벌 경제 위기와 침체된 투자 시장 속에서, 스타트업인 비브로스는 자금 조달에 큰 어려움을 겪었으며, 서비스를 지속하기 위해 유료 멤버십 전환이 불가피한 선택이었습니다. 유료 멤버십 도입이 확정된 이후, 백엔드팀은 멤버십 관련 업무를 담당할 인원 2명과 결제 시스템을 전담할 인원 1명으로 업무를 분담하여 작업을 시작했습니다.

백엔드팀의 주요 목표는 두 가지로 나뉘었습니다. 첫째, 사용자 요청일에 정확히 한 번씩 결제가 이루어질 수 있도록 안전한 정기 결제 시스템을 구축하는 것이었고, 둘째, 멤버십 조회성 서버 부하를 대비할 대안을 마련하는 것이었습니다. 특히 첫 번째 목표인 안전한 정기 결제 시스템 구축을 위해 MongoDB Transaction을 도입하여 데이터 일관성과 신뢰성을 확보하고자 했습니다.

MongoDB Transaction 소개

트랜잭션은 하나 이상의 읽기 또는 쓰기 작업을 포함하는 데이터베이스의 논리적 처리 단위입니다. 애플리케이션에서 여러 도큐먼트에 대한 읽기 및 쓰기를 논리적으로 묶어서 처리해야 하는 상황이 종종 발생합니다. 트랜잭션의 핵심은 작업이 성공하든 실패하든 부분적으로 완료되지 않는다는 점입니다. 즉, 트랜잭션 내 모든 작업이 완료되거나, 그렇지 않으면 모두 롤백됩니다.

트랜잭션이 진정한 트랜잭션으로 간주되기 위해서는 ACID(원자성, 일관성, 고립성, 지속성) 속성을 충족해야 합니다. ACID 트랜잭션은 오류가 발생하더라도 데이터 및 데이터베이스 상태의 유효성을 보장합니다.

MongoDB는 4.0 버전부터 멀티 문서 트랜잭션을 지원하며, 이를 통해 관계형 데이터베이스의 ACID 트랜잭션 특성을 제공합니다. 이 기능은 MongoDB를 활용하여 복잡한 비즈니스 로직을 처리하거나 다중 문서 작업을 단일 작업처럼 수행할 수 있도록 돕습니다. 이러한 특징은 데이터의 일관성을 유지하면서도 유연한 트랜잭션 설계를 가능하게 합니다.

트랜잭션의 주요 특성:

  • 원자성: 트랜잭션 내의 모든 작업은 성공적으로 완료되거나, 실패 시 모두 롤백됩니다.
  • 일관성: 트랜잭션 완료 후 데이터베이스는 항상 유효하고 일관된 상태를 유지합니다.
  • 고립성: 여러 트랜잭션이 동시에 실행되더라도 서로의 상태에 영향을 미치지 않습니다.
  • 지속성: 트랜잭션이 커밋되면 그 결과는 영구적으로 저장됩니다.

MongoDB 트랜잭션은 특히 복잡한 다중 문서 작업이나 데이터의 일관성이 필수적인 시나리오에서 유용합니다. 이러한 특성은 원자성이 중요한 멤버십 데이터 관리에 적합한 선택이었습니다.

비브로스에서는 멤버십 데이터를 멤버십, 멤버십 트랜잭션, 멤버십 트랜잭션 히스토리로 세분화하여 관리하며, 트랜잭션을 적극 활용했습니다. 이러한 구조는 QA 단계와 대규모 결제 테스트에서도 안정적으로 작동하여 문제없이 운영되었습니다.

그러나 실제 운영 서비스에서 MongoDB 트랜잭션을 실행하는 도중 Write Conflict가 발생하면서 이중 결제와 같은 이슈가 보고되기 시작했습니다. 이는 트랜잭션 설계 시 충돌 가능성을 충분히 고려하지 못한 데서 비롯되었으며, 문제 해결을 위해 Lock 메커니즘 등의 추가적인 대책이 필요하게 되었습니다.

Write Conflict란 무엇인가

Write Conflict는 두 개 이상의 쓰기 작업이 동일한 문서나 컬렉션에 동시에 접근하여 충돌이 일어날 때 발생합니다. MongoDB는 트랜잭션을 통해 격리성을 제공하지만, 완전한 병렬 처리를 지원하지 않기 때문에 Write Conflict가 발생할 가능성이 있습니다.

Write Conflict 사례 중 대표적인 예로 '동일한 문서를 동시에 수정하려는 경우'가 있었습니다. 이처럼 사용자 요청으로 멤버십 데이터를 업데이트하던 과정에서, 두 개의 트랜잭션이 동일한 문서를 동시에 수정하려 하여 Write Conflict가 발생했습니다. 이로 인해 이중 결제라는 심각한 문제가 발생하게 되었습니다.

이중 결제 이슈 Case #1

멤버십 케이스 #1

2023년 10월 6일, 이중 결제 문제가 발생한 상황이 있었습니다. 사용자는 기존 결제 수단을 다른 결제 수단으로 변경했는데 결제가 두 번 처리되었다는 이슈였습니다. 서버 로그를 확인해본 결과, 사용자가 멤버십 재결제 API 호출 후 결제 수단 변경 이벤트를 요청한 것이 파악되었습니다. 두 API 모두 멤버십 재결제 로직을 포함하고 있으며 동일한 콜렉션을 업데이트하고 있었습니다.

문제의 핵심은, 멤버십 재결제 API를 처리하기 위해 트랜잭션 #1이 진행 중이었고, 그 후에 결제 수단 변경 이벤트가 들어와 트랜잭션 #2가 시작된 점입니다. 하지만 트랜잭션 #1이 이미 진행 중이었기 때문에 Write Conflict가 발생했고, 트랜잭션 #1이 종료된 후, 트랜잭션 #2가 재시도되면서 결제가 두 번 이루어졌습니다.

Case #1 대책

이로 인하여 저희는 API 호출 순서를 Lock으로 제어하기로 하였습니다. 동시성을 제어하여 동일한 리소스에 대한 여러 트랜잭션이 동시에 실행되지 않도록 Lock 메커니즘을 도입한 것입니다. 멤버십 결제 재시도, 결제 수단 변경 이벤트 등 멤버십 업데이트 호출 시 멤버십 데이터에 동시 접근하지 못하도록 수정했습니다.

// 멤버십 정보 업데이트 요청 RedLock 데코레이터
@RedLock({
keyFactory: (userId: ObjectId) =>
`공통키:${body.userId}:update`,
duration: Duration.seconds(10),
})
// 결제 수단 변경 이벤트 요청 RedLock 데코레이터
@RedLock({
keyFactory: (body: CreatePaymentMethodRequest) =>
`공통키:${body.userId}:update`,
duration: Duration.seconds(10),
})

이중 결제 이슈 Case #2

놀토-1

2023년 10월 19일, 새로운 유형의 중복 결제 이슈가 발생했습니다. 사용자가 멤버십을 해지했음에도 두 번 결제가 이루어진 사례로, 원인을 조사한 결과 정기 결제 당일 사용자가 이미 결제 대기열에 들어간 상태에서 해지 신청을 했던 것이 문제의 시작이었습니다.

더 복잡한 점은 사용자가 해지 신청 후 해지 취소를 하였고, 이 과정이 정기 결제 당일에 이루어진 경우, 시스템 내에 즉시 결제를 실행하는 로직이 포함되어 있었던 것입니다. 이로 인해 사용자는 동일한 멤버십 이용권에 대해 두 번 결제되는 상황이 발생했습니다.

또한, 이 과정에서 멤버십 해지 요청 트랜잭션이 실행 중이었기 때문에, 정기 결제를 처리하려는 트랜잭션에서 Write Conflict가 발생했을 가능성도 높습니다. 이는 두 트랜잭션이 동일한 데이터에 동시에 접근하려 한 결과로 보입니다.

Case #2 대책

이 문제를 해결하기 위해 사용자 업데이트와 관련된 모든 API와 이벤트에 공통 키를 사용하여 Lock을 걸도록 로직을 변경하였습니다. 이를 통해 멤버십 결제 재시도, 결제 수단 변경 이벤트뿐만 아니라 멤버십 데이터가 변경되는 모든 API와 이벤트가 동시에 실행되지 않도록 했습니다. 이로써 동시 접근을 방지하고, 병렬적으로 실행되도록 개선되었습니다.

이 문제는 동시성 문제와 타이밍 이슈로 인한 결과였습니다. 공통 키를 통한 Locking 시스템을 도입하여 멤버십 해지와 결제 로직을 개선하여 해결하였습니다.

이중 결제 방지를 위한 시스템 구축

결제 관련 트랜잭션을 보다 철저히 관리하고 중복 결제 문제가 재발되지 않도록 하기 위해, 네트워크 문제뿐만 아니라 다른 잠재적인 문제 지점들도 확장성 있게 검토하였습니다. 또한, 중복 결제 문제를 즉시 인지하고 대응할 수 있도록 멤버십 결제 이상 모니터링(알림) 시스템을 구축하였습니다. 이를 통해 문제가 발생할 경우 빠르게 대응할 수 있는 환경을 마련하였으며, 향후 유사한 문제가 재발하지 않도록 예방하는 데 중점을 두었습니다.

결론

MongoDB의 트랜잭션은 강력한 데이터 일관성을 제공하지만, Write Conflict를 피하려면 데이터 구조와 트랜잭션 설계를 신중히 해야 합니다. 트랜잭션 설계 시 발생할 수 있는 충돌을 미리 예측하고, 이를 방지할 수 있는 대책을 마련하는 것이 매우 중요합니다. 비브로스에서는 이러한 문제를 해결하기 위해 Lock 메커니즘을 활용하여 동시 접근을 제어하고, 트랜잭션 충돌을 방지하였습니다. 이를 통해 데이터의 일관성을 유지하면서도 안정적인 서비스를 제공할 수 있었습니다.

Reference: MongoDB: The Definitive Guide 3rd Edition, MongoDB Official Documentation