선착순 쿠폰 동시성 면접 질문: 100장 한정에 101장째가 나가는 이유
시스템 디자인 Vol.1 · 06장 「딱 100장만, 선착순 쿠폰」에서
이런 화면입니다
배달 앱에서 낮 12시 정각에 선착순 쿠폰이 열립니다. 수천 명이 같은 순간에 받기 버튼을 누르고, 몇 초 만에 마감됩니다.
그런데 101번째 쿠폰이 나갔습니다. 면접관은 남은 수량 게이지의 뒤를 묻습니다. 수천 개의 요청이 한꺼번에 도착할 때 어떻게 딱 100장에서 멈추느냐는 질문입니다.
면접관이 이렇게 묻습니다
선착순 100장 쿠폰 발급을 설계해 보세요. 12시 정각에 수천 명이 동시에 누릅니다. 101장이 나가는 일은 절대 없어야 합니다.
범위부터 좁힙니다
한 줄로 좁힙니다. 정확히 100장까지만 발급하고, 마감 뒤에 누른 사람에게는 마감을 알려 준다.
한 사람이 여러 장 받는 문제는 의식적으로 뒤로 미룹니다. 이 문제의 무게는 총량 100장에 있습니다.
테이블 모양은 누구나 그립니다. 진짜 질문은 지금 몇 장 나갔는지 세는 일과 한 장을 더 내주는 일이 수천 개의 요청 속에서도 어긋나지 않게 할 수 있느냐입니다.
가장 많이 나오는 답
가장 순진한 코드는 이렇게 생겼습니다. 지금까지 발급된 쿠폰 수를 세고, 100보다 작으면 한 장 발급합니다.
SELECT COUNT(*) FROM coupons WHERE event_id = 7; -- 99
-- 코드에서: 99 < 100 이니까 발급
INSERT INTO coupons (event_id, user_id) VALUES (7, :userId);
혼자 테스트하면 완벽하게 돕니다. 99장에서 한 장 나가고, 100장이 되면 멈춥니다.
그런데 여기서 깨집니다
문제는 12시 정각입니다. 두 요청이 동시에 99를 읽습니다. 둘 다 100 미만이니 둘 다 발급합니다. 101장입니다.
손에 든 99는 다음 문장으로 넘어가는 순간 이미 틀린 값입니다. 읽을 때 맞았던 값이 쓸 때 틀려 있는 것. 면접관이 "동시에 누르면요?" 하고 묻는 순간 보고 있는 틈이 바로 여기입니다.
떨어지는 답은 이런 모양입니다
- COUNT 후 INSERT를 트랜잭션으로 감싸면 된다고 답합니다. 트랜잭션이 이 틈을 정말 막는지는 답하지 못합니다.
synchronized로 막겠다고 답합니다. 서버가 두 대가 되는 순간 무너집니다.
면접관은 여기서 멈추지 않습니다
한 사람이 한 장만 받게 하려면요?
총량과 1인 제한이 서로 다른 문제라는 것을 아는지 봅니다.
세 장까지라면요?
한 장 제한에서 쓴 방법이 그대로 통하는지 스스로 따져 보는지 봅니다.
정각에 수십만 명이 몰리면요?
같은 설계가 어디까지 버티고, 어디서 다음 계단이 필요한지 봅니다.
이 다음 판단은 책에서 이어집니다
책은 이 틈을 어디서 없앨지 정하고, 그때 데이터베이스 안에서 무슨 일이 일어나는지 순서대로 풉니다. "트랜잭션으로 감싸면 되지 않나요"와 "그럼 잠그면 되지 않나요"에 답하고, 정말 안전한지 확인하는 방법까지 이어집니다.
06장에서 이어지는 것
- 그때 데이터베이스 안에서 일어나는 일
- 트랜잭션으로 감싸면 되지 않느냐는 질문과 격리 수준
- 정말 안전한지 확인하는 방법
- 잠그면 되지 않느냐는 질문