203. 갱신 손실 (Lost Update) - 병행 수행 문제점 동시 갱신 덮어쓰기 트랜잭션 격리성 위배 자물쇠(Lock) 부재 재고 관리 에러

핵심 인사이트: (202번 1번 문제 심화) 내 통장에 10만 원이 있다. 내가 폰뱅킹으로 5만 원을 입금하는 순간(트랜잭션 A), 와이프가 체크카드로 3만 원을 출금했다(트랜잭션 B). 정상이라면 내 통장엔 "10 + 5 - 3 = 12만 원"이 있어야 한다. 그런데 맙소사, 잔고가 '7만 원'으로 찍혀있다! 내가 입금한 5만 원은 어디로 증발했지?! "야! 내가 10만 원을 읽어서 15만 원으로 계산(Update)하고 저장하려는 그 0.1초의 빈틈에, 와이프가 옛날 데이터인 10만 원을 쓱 읽어가서 7만 원으로 계산해 버렸잖아! 그리고 와이프가 마지막에 7만 원을 덮어써서 저장해 버리니까, 내가 고생해서 저장한 15만 원은 허공으로 날아가고 흔적도 안 남은 거잖아!!" 내 노력과 돈을 허공에 태워버리는 가장 끔찍하고 빈번한 덮어쓰기 에러, 갱신 손실이다.

Ⅰ. 갱신 손실 (Lost Update)의 발생 원리 🌟

여러 개의 트랜잭션이 같은 데이터를 동시에 읽고(Read) 동시에 덮어쓸(Write) 때 벌어지는 최악의 병행 수행 오류입니다.

시나리오 (에어팟 재고 100개)

  • **T1(철수)**가 에어팟 1개를 주문합니다.
  • **T2(영희)**도 에어팟 2개를 주문합니다.
  1. T1이 재고 엑셀 파일을 읽습니다. "현재 재고 100개!"
  2. (아직 T1이 빼기 연산을 안 한 찰나) T2가 엑셀 파일을 똑같이 읽어갑니다. "어 나도 현재 재고 100개네!"
  3. T1이 계산을 끝내고 100 - 1 = 99를 엑셀에 적어 넣고 쿨하게 COMMIT(성공!) 합니다. 재고는 99개가 되었습니다.
  4. 그 직후, T2도 아까 자기가 읽었던 100개에서 계산한 결과 100 - 2 = 98을 엑셀에 쾅! 적어버리고 COMMIT 합니다.
  5. 대참사 (Lost Update): 원래 재고는 100 - 1 - 2 = 97개가 되어야 맞습니다. 하지만 T2가 마지막에 98개를 덮어써 버리는 바람에, T1이 고생해서 99개로 바꿔놓은 업데이트(Update) 내역이 우주의 먼지처럼 영원히 날아가고 손실(Lost)되어 버렸습니다!

Ⅱ. 이 끔찍한 현상을 막는 2가지 흑마법 🌟 핵심 기출 🌟

결국 195번 **격리성(Isolation)**을 빡세게 지켜서 남이 중간에 못 끼어들게 해야 합니다.

1. 비관적 락 (Pessimistic Lock / 배타 락 X-Lock) - "의심병 환자"

  • 마인드: "분명히 내가 엑셀 열 때 딴 놈이 또 열러 올 거야!" (비관적)
  • 방어: T1이 재고를 읽는 순간, 데이터에 거대한 쇠사슬(X-Lock)을 걸어버립니다.
    • T2가 엑셀을 열러 왔다가 쇠사슬을 보고 "아 ㅆㅂ 누가 쓰고 있네" 하고 문밖에서 줄을 서서 무한 대기(Wait)합니다.
    • T1이 99개로 고치고 도장을 찍은 뒤 쇠사슬을 풀면, 그제야 T2가 들어가서 99개를 읽어와 97로 정확히 계산합니다.
  • 장점: 갱신 손실을 100% 물리적으로 틀어막습니다. 단점: 동시 접속자가 10만 명이면 1명씩 들여보내야 해서 쇼핑몰이 터집니다.

2. 낙관적 락 (Optimistic Lock / 버전 관리) - "긍정주의자"

  • 마인드: "에이, 내가 쓰는 이 찰나의 순간에 설마 딴 놈이 들어오겠어?" 일단 자물쇠 없이 쿨하게 다 열어줍니다. (속도 개빠름)
  • 방어 (버전 확인): 데이터 옆에 버전(Version) = 1이라는 꼬리표를 달아둡니다.
    • T1T2가 동시에 "재고 100개, 버전 1"을 읽어갑니다.
    • T1이 99개로 바꾸고, 버전 2로 업데이트하며 1등으로 저장 성공합니다.
    • 1초 뒤 T2가 98개로 저장하러 옵니다. "재고 98개야, 나 아까 버전 1 읽었어!"
    • DB 엔진의 철퇴: "야 ㅆㅂ 지금 DB에 버전 2로 업데이트돼 있잖아! 너 아까 옛날 데이터 읽어간 거지?! 네가 덮어쓰면 앞사람(T1) 꺼 날아가! 너 저장 실패(ROLLBACK)! 처음부터 99개짜리 다시 읽어서 계산 다시 해와!!"
  • 현대의 대용량 웹 시스템이 트래픽을 감당하며 갱신 손실을 막는 가장 위대한 꼼수(CAS, Compare-And-Swap)입니다.

Ⅲ. 실무 면접 팁 (트랜잭션 격리 수준과의 관계)

  • 놀랍게도 오라클(Oracle)이나 MySQL의 기본 격리 수준(Read Committed)만으로는 이 '갱신 손실'을 완벽히 막아주지 못합니다. 개발자가 SQL 문에 명시적으로 FOR UPDATE를 달아서 자물쇠(락)를 걸어주지 않으면 수강 신청이나 티켓 예매 시 재고 빵꾸(초과 예매) 대참사가 필연적으로 터집니다.

📢 섹션 요약 비유: **갱신 손실(Lost Update)**은 하나의 칠판(데이터)을 두고 **'두 명의 멍청한 비서가 동시에 재고 조사를 하는 코미디'**입니다. 칠판에 사과가 '10개'라고 적혀있습니다. A비서가 칠판을 보고 머릿속에 '10개'를 외우고 1개 팔린 걸 계산하러 자기 자리로 갑니다. 1초 뒤 B비서도 칠판을 보고 '10개'를 외우고 2개 팔린 걸 계산하러 갑니다. A비서가 계산을 끝내고 와서 칠판을 지우고 **'9개'**라고 적습니다. 그 직후, B비서가 와서 방금 A가 적은 '9개'를 쿨하게 지워버리고 자기 머릿속에서 계산한 **'8개'**라고 덮어쓰고 퇴근해 버립니다. 원래 사과는 3개가 팔렸으니 7개가 되어야 하는데, A비서가 고생해서 빼놓은 1개의 기록이 B비서의 덧칠에 의해 우주 끝으로 날아가 버렸습니다. 이 참사를 막기 위해 칠판에 **'열쇠 달린 유리문(비관적 락)'**을 달거나, **'칠판 갱신 버전 번호(낙관적 락)'**를 써서 앞사람의 피땀 눈물이 뒷사람에 의해 억울하게 지워지는 일을 완벽하게 방어해야 합니다.