더 많은 도움을 드리기 위해

열심히 포스팅 중입니다!


지나가다 📢 광고 한 번 눌러주시면

더 좋은 글로 보답하겠습니다. 🥰

기술 면접 준비

갱신 손실(lost update) 이슈는 어떻게 방지할까요? - 기술 면접 준비

평비 - Giveloper 2025. 5. 11. 10:56

 

 

면접관 : A 금융 프로젝트에서 송금 기능을 구현하셨는데요.
송금과 동시에 자동 이체로 돈이 빠져나가면 잔고 정합성이 맞지 않는 문제가 있을텐데,
이 이슈는 어떻게 방지하셨나요?

 

 

동시성 이슈 도식화

 

📢 문제 상황

문제 발생 전 잔고 상태

A 잔고: 50만원

B 잔고: 50만원

C 잔고: 80만원

총 180만원

 

 -> 동시에 A로 자동이체, B로 송금

 

문제 발생 후

A 잔고: 100만원

B 잔고: 100만원

C 잔고: 30만원

총 230만원

 

전형적인 갱신 손실 문제입니다.

읽고 계산하고 쓰는 과정이 분리되어 있는 상황에서 둘 다 같은 값을 읽고 각자의 계산으로 갱신했을 때, 하나의 결과가 다른 하나를 덮어쓰는 현상.

 

🍼 왕초보

송금과 자동이체가 동시에 일어나면, 잔고가 틀어질 수 있습니다. 이걸 방지하기 위해 잔고를 먼저 확인하고 송금하는 식으로 순서를 잘 지켰습니다. 시스템적으로 송금을 요청할 때 먼저 잔고를 확인하고, 충분하다면 그때 보내는 방식입니다.

가능한 추가 질문
그럼 잔고를 확인한 이후 다른 트랜잭션이 잔고를 줄이면 어떻게 될까요? ... 🤷‍♂️

 


🐣 초보

송금과 자동이체가 거의 동시에 실행되면, 두 작업이 같은 잔고를 읽고 업데이트하려다가 충돌합니다. 예를 들어, 두 쪽 다 잔고 80만원을 보고, 송금과 자동이체가 각각 50만 원이면 출금 시도를 하게 됩니다. 이런 문제를 막으려면 동시 접근을 제어해야 합니다. 데이터베이스에서 SELECT FOR UPDATE 구문을 써서 출금 대상 계좌를 락(lock) 걸어두고, 다른 트랜잭션이 동시에 접근하지 못하도록 막았습니다.

가능한 추가 질문
1. SELECT FOR UPDATE는 정확히 어떤 락을 걸게 되나요?
2. 락이 걸렸을 때 다른 트랜잭션은 어떻게 되나요?

 


🥉 하수

이 문제는 Race Condition(경합 조건)과 관련 있습니다. 송금과 자동이체가 동시에 같은 계좌를 수정하려고 하면, 트랜잭션 충돌이 일어나고 의도하지 않은 동작이 발생할 수 있습니다. 이를 해결하기 위해 두 작업을 각각 트랜잭션으로 감싸고, 출금 계좌에 대해 SELECT FOR UPDATE로 Row-level Lock을 걸었습니다. 그러면 한 트랜잭션이 먼저 락을 획득하고 처리하는 동안, 다른 트랜잭션은 대기하거나 타임아웃돼서 데드락은 방지하고 충돌도 막을 수 있습니다.

가능한 추가 질문
1. 만약 두 트랜잭션이 서로 다른 순서로 여러 테이블을 락 걸면 어떤 문제가 생길 수 있죠?
2. 락 대기 시간이 너무 길어지면 어떻게 대응하나요?
3. 트랜잭션 실패 시 사용자 경험은 어떻게 보장하셨나요?

 


🥈 중수

락을 활용해서 동시성을 제어했습니다. 다만, (락 경합이 많은) 대규모 트래픽 시스템에서는 성능 저하로 이어질 수 있다는 것을 고려했습니다. 송금, 이체와 같은 출금 연산은 계좌별 단일 큐 처리 방식을 적용했습니다. 계좌마다 큐를 두고, 모든 출금 요청은 큐에 넣어 순차적으로 처리하게 되면 동시 충돌 없이 안정적으로 운용할 수 있습니다. 다른 방법으로는, CQRS를 도입해 출금 전용 쓰기 모델에서만 상태를 변경하고, 읽기는 분리된 리드 모델을 사용해 병목을 줄이기도 합니다.

 

CQRS : 명령과 쿼리의 역할 분리를 의미 (DB에 대한 읽기 및 쓰기 작업을 구분하는 패턴)

(Command and Query Responsibility Segregation)

가능한 추가 질문
1. 단일 큐 처리 방식을 사용하면 병렬성은 어떻게 확보하나요?
2. CQRS를 적용하면 어떤 장단점이 있나요?

 


🥇 고수

Versioning(Optimistic Locking)을 활용해서 동시성을 제어했습니다. 단순히 락을 걸어서 한쪽을 막는 방식은 성능 저하락 경합 같은 문제가 추가적으로 발생하게 됩니다. 그래서 락을 최소화하면서도 데이터 정합성을 보장할 수 있는 구조로 설계했습니다. 스케일과 안정성 모두 고려했습니다. 다른 고민했던 방법으로는 출금 요청을 메시지 큐로 비동기 처리한 뒤, 실제 출금은 단일 처리 시스템에서 처리하는 방식 등이 있었습니다.

가능한 추가 질문
1. Optimistic Locking과 Pessimistic Locking은 언제 각각 사용하나요?
2. 메시지 큐로 처리할 때 실패 처리는 어떻게 하나요?
3. Optimistic Lock을 사용할 때 충돌이 자주 발생하면 어떻게 처리하셨나요? Retry? 혹은 다른 우회 방안이 있나요?

 


🧙 고인물

운영 환경에서는 이 문제를 단순히 트랜잭션 충돌 정도가 아니라, 시스템 전체 아키텍처의 신뢰성 이슈로 보기 때문에, 실시간성 요구와 동시성 문제가 교차되는 이런 문제를 신중하게 고민하고 해결했습니다.

계좌Aggregate Root로 설정하고, 한 트랜잭션 내에서만 상태 변경이 일어나도록 도메인 모델을 설계했습니다. Serializable 격리 수준은 비용이 크므로 Repeatable Read + Optimistic Lock 조합을 사용했고, 고빈도 계좌에 대해서는 Event Sourcing 기반의  비동기 Command Queue를 통해 성능정합성을 확보했습니다.

가능한 추가 질문
1. Aggregate Root를 계좌로 설정한 이유는 무엇인가요?
2. 한 트랜잭션 내에서만 상태 변경이 일어나도록 했다고 했는데, 송금은 출금과 입금이 함께 일어나는 행위인데 어떻게 나눴나요?
3. Repeatable Read에서도 Phantom Read는 발생할 수 있는데, 그에 대한 보완은 어떻게 하셨나요?
4. Command Queue 기반으로 처리하다가 실패했을 때, 재처리는 어떻게 했나요? Idempotency 키를 사용하셨나요?

 

 

 

👏

자, 이렇게 금융권에서 접하는 동시성 이슈에 대한 트랜잭션 관련 면접 질문과 답변 그리고 추가 질문에 대해 다뤄봤습니다!

여러분들은 왕초보 ~ 고인물 중에서 어떤 위치에 속하시나요? 참고로 저도 고인물은 아니에요! 😂

 

평비의 이 평범한 글이 여러분에게 비범한 도움이 되셨으면 좋겠습니다 👍