본문으로 바로가기

16

쓰기 통과와 쓰기 지연

'저장했습니다'라고 말하는 순간, 정말 저장된 걸까

먼저 보면 좋은 강의: 지연 시간과 처리량, 캐시 — cache-aside 패턴

단계별로 보기

스페이스바로 재생/정지, 좌우 화살표로 단계 이동, R로 처음으로 돌아갑니다.

대기 중1 / 4 단계 — 끝까지 기다리는 쓰기 (쓰기 통과)

사용자가 저장을 누릅니다. 요청은 데이터베이스까지 가고, 기록이 끝난 뒤에야 "저장했습니다"가 돌아옵니다. 느리지만 정직합니다. 이 응답을 받았다면 데이터는 정말 안전한 곳에 있습니다.

지연 p5042ms

이 강의를 마치면

  • 쓰기 통과·쓰기 지연·쓰기 우회 세 가지 정책의 동작과 대가를 비교한다.
  • '접수했다'와 '처리했다'가 다른 약속임을 구분한다.
  • 쓰기 지연이 속도를 얻는 대신 무엇을 내주는지(유실 위험) 설명한다.
  • 쓰기를 미루는 구조가 몰리는 순간을 어떻게 흡수하는지 관찰한다.

읽기는 끝났다. 이제 쓰기다

앞의 두 강의는 전부 읽기였습니다. 읽기는 쉽습니다. 몇 번을 읽어도 세상은 변하지 않으니까요.

쓰기는 다릅니다. 쓰기는 세상을 바꿉니다. 그리고 캐시가 옆에 있으면 즉시 질문이 생깁니다.

사용자가 상품 42번의 가격을 바꿨다. 그럼 캐시에 적어둔 옛날 가격은 어쩌지?

선택지는 셋입니다.

세 가지 정책

정책하는 일얻는 것잃는 것
쓰기 통과 (write-through)캐시와 원본에 둘 다 쓰고 나서 응답캐시가 항상 최신쓰기가 가장 느리다
쓰기 지연 (write-back)캐시에만 쓰고 바로 응답. 원본에는 나중에쓰기가 가장 빠르다사고 나면 데이터가 사라진다
쓰기 우회 (write-around)원본에만 쓰고, 캐시의 그 항목은 지운다단순하고 안전그 항목의 다음 읽기는 반드시 미스

쓰기 통과 — 둘 다 쓰고 나서 대답한다

가장 정직합니다. 캐시와 원본 양쪽에 다 쓰고 나서 "저장했습니다"라고 말합니다. 캐시는 항상 최신이고, 방금 쓴 값을 바로 읽어도 맞습니다.

대가는 속도입니다. 두 곳에 다 쓸 때까지 사용자가 기다립니다. 그리고 거의 안 읽히는 항목까지 캐시에 밀어 넣습니다. 로그성 데이터를 쓰기 통과로 캐시하면 캐시가 쓰레기로 가득 찹니다.

쓰기 우회 — 지우고 원본에만 쓴다

실무에서 가장 흔한 기본값입니다. 캐시의 해당 항목을 지우고, 원본에만 씁니다.

캐시에 틀린 값을 남기지 않고(지웠으니까), 안 읽힐 데이터를 캐시에 넣지도 않습니다. 대신 방금 쓴 항목을 다음에 읽으면 반드시 미스입니다. 그건 대체로 감당할 만합니다.

쓰기 지연 — 일단 접수하고 나중에 기록한다

여기가 이 강의의 진짜 주제입니다.

캐시(또는 빠른 어딘가)에만 쓰고 즉시 "저장했습니다"라고 답합니다. 원본에는 나중에, 여유가 있을 때 씁니다.

압도적으로 빠릅니다. 사용자는 느린 데이터베이스를 기다리지 않습니다. 그런데 "저장했습니다"가 거짓말이 될 수 있는 창이 생깁니다. 그 사이에 캐시가 죽으면 데이터는 그냥 사라집니다. 사용자는 저장됐다고 믿고 있는데요.

두 개의 다른 약속

이 강의에서 하나만 가져간다면 이것입니다.

"접수했습니다"  ≠  "처리했습니다"

택배 접수증과 배송 완료 문자는 다른 약속입니다. 접수증은 즉시 나오지만 물건이 도착했다는 뜻은 아닙니다.

쓰기 지연은 사용자에게 접수증을 주는 것입니다. 그게 나쁜 게 아닙니다. 많은 경우에 그게 옳습니다. 문제는 접수증을 주면서 배송 완료라고 말하는 것입니다.

어디에 무엇을 쓰나

하는 일적절한 약속이유
결제, 계좌 이체처리 완료까지 기다린다사라지면 안 된다. 몇백 밀리초는 낼 만하다
주문 접수접수증을 주고 뒤에서 처리접수 자체는 확실히 남기되, 재고·정산은 뒤에서
좋아요, 조회수접수증만 준다몇 개 사라져도 세상이 안 무너진다
로그, 통계접수증도 필요 없다유실 허용치가 가장 크다

이 표를 채우는 건 기술자가 아니라 업무 담당자입니다. "이 데이터가 사라지면 얼마나 큰일인가"는 기술 질문이 아니기 때문입니다.

시스템 규모의 쓰기 지연

쓰기 지연의 아이디어는 캐시 안에만 있지 않습니다. 같은 생각을 시스템 전체로 키우면 이렇게 됩니다.

사용자 → 서버 → [접수함] → (나중에) 처리 담당 → 데이터베이스
                   ↑
            여기서 "접수했습니다" 응답

접수함에 넣는 것은 매우 빠릅니다. 그래서 몰리는 순간을 흡수합니다. 초당 200건이 갑자기 들어와도, 데이터베이스가 초당 133건밖에 못 써도, 접수함이 받아 두고 천천히 흘려보내면 아무도 오류를 보지 않습니다.

대신 접수함에 쌓인 일이 처리되기까지 시간이 걸립니다. 화면은 멀쩡한데 실제 처리가 몇 분 밀려 있을 수 있습니다. 이 밀림을 지켜보지 않으면 조용히 사고가 납니다.

직접 해보세요

아래 과제는 이 아이디어를 시스템 규모로 옮긴 것입니다. 쓰기가 몰리는 구간에서 데이터베이스가 감당하지 못해 요청이 버려집니다. 접수함과 처리 담당을 두어 몰리는 구간을 흡수하세요. 그리고 통과한 뒤에는 접수함에 얼마나 쌓였는지 꼭 확인해 보세요. 그것이 이 구조의 진짜 대가입니다.

이 강의가 단순화한 것

교육용 모형은 언제나 무언가를 생략합니다. 무엇을 생략했는지 아는 것도 학습의 일부입니다.

전제한 것

  • 이 과제는 쓰기 지연을 캐시 정책이 아니라 큐와 워커라는 시스템 규모의 구조로 표현한다. 아이디어는 같다 — 접수하고 나중에 기록한다.
  • 이 시뮬레이터의 큐는 죽지 않는다고 가정한다. 실제 쓰기 지연의 핵심 위험은 접수함이 죽었을 때 데이터가 사라지는 것이다.
  • '저장했습니다'라고 답한 뒤 실제 기록이 실패하는 경우는 모델링하지 않는다.
  • 큐는 한 번에 워커가 받을 수 있는 만큼만 꺼내므로 배달 속도에 상한이 있다. 그래서 접수함이 밀리는 현상이 그대로 보인다.

다루지 않은 것

  • 쓰기 지연에서의 더티 데이터 추적과 플러시 전략
  • 트랜잭션과 원자성
  • 메시지 중복 처리와 멱등성 키
  • 데드레터 큐와 재시도 정책

더 읽을거리: Google SRE Book — Handling Overload

이제 직접 만들어 보세요

읽어서 아는 것과 만들어서 아는 것은 다릅니다. 컴포넌트를 배치하고 트래픽을 흘려서 실제로 동작하는지 확인해 보세요.

실습 과제 열기

이 강의에 물어보기

하루 5번

이 강의 본문만 근거로 답합니다. 강의에 없는 내용은 지어내지 않습니다.