TT Lab
시작하기
배우기 러닝패스 코스

되돌릴 수 없는 변경

읽기와 쓰기 사이에는 문턱이 하나 있다

TT Lab 에서 이어서 보기

한 줄 요약

조사는 틀려도 다시 하면 되지만 UPDATE 는 다시 할 수 없고, 그래서 쓰기 앞에는 조사에 없던 네 가지가 붙는다 — 범위 산정, 되돌릴 길, 시험 실행, 중단 기준.

왜 이게 필요했나

FDE 가 고객 환경에서 하는 일의 대부분은 읽기입니다. 로그를 읽고, 테이블을 조회하고, 코드를 따라가고, 그 결과를 문서로 옮깁니다. 이 구간에서 틀린 판단의 비용은 시간입니다. 가설이 어긋나면 지우고 다시 세우면 되고, 잘못 센 숫자는 다시 세면 됩니다.

그런데 어느 시점에 반드시 이 문장이 옵니다. "그럼 그거 좀 고쳐 주실 수 있나요."

여기서부터 성질이 완전히 바뀝니다. 틀린 조회는 다시 하면 되지만 틀린 UPDATE 는 다시 할 수 없습니다. 되돌릴 수단을 미리 만들어 두지 않았다면, 그 순간 우리가 한 일은 조사가 아니라 사고입니다.

그리고 문제가 하나 더 있습니다. 요청은 조건이 아니라 문장으로 옵니다. "오래 멈춰 있는 결제 건 정리해 주세요" 라는 문장을 WHERE status='pending' 으로 옮기는 순간, 요청한 사람이 머릿속에 그린 범위와 실제로 바뀌는 범위가 갈라집니다. 갈라진 폭만큼이 사고이고, 그 폭은 세어 보기 전에는 요청한 쪽도 받은 쪽도 모릅니다.

요청 문장이 조건이 되는 순간 범위가 갈라진다

본문은 요청이 조건이 아니라 문장으로 온다는 점에서 쓰기의 위험이 시작된다고 말한다. 문장이 조건으로 옮겨지기까지의 순서를 옮겼다.

  1. 요청은 문장으로 온다오래 멈춰 있는 결제 건 정리해 주세요 같은 문장은 조건이 아니다. 요청한 사람의 머릿속에는 자기가 그린 범위가 있다.
  2. 문장을 WHERE 로 옮기면 범위가 갈라진다status 가 pending 인 행으로 옮기는 순간 요청한 사람이 그린 범위와 실제로 바뀌는 범위가 다르다. 갈라진 폭만큼이 사고이고, 세어 보기 전에는 요청한 쪽도 받은 쪽도 모른다.
  3. 두 숫자를 나란히 놓고 제외를 설명한다문장을 그대로 옮긴 조건의 행 수와 좁힌 조건의 행 수를 놓는다. 45건과 20건이면 25건이 위험이고, 그 25건이 왜 제외되는지 한 줄씩 적을 수 있어야 조건이 완성된다.

여기서 구분할 것 건수 차이가 곧 잘못이라는 뜻은 아니다. 제외는 일을 덜 한 것이 아니라 판단을 기록한 것이다. 45건과 20건은 본문의 설명용 예시이고, 쓰기 앞의 네 장치는 레슨 본문이 제시하는 절차이다.

잠깐, 예측해 보세요 주문 상태를 지금까지 테이블에 없던 cancelled 로 바꾸는 변경이다. 바뀌는 건수가 계획과 정확히 맞았다. 그래도 따로 해야 할 일은 무엇일까?

설명 확인 · 채점 없는 자가 점검

이 변경을 알려야 한다. 그 값을 모르는 집계 쿼리와 대시보드는 해당 행들을 조용히 빼고 센다. 에러가 나지 않아서 아무도 모르고, 한 달 뒤 정산이 맞지 않는 것으로 드러난다. 새 값을 도입하는 변경은 데이터 변경이면서 통보해야 하는 변경이다.

근거 문서

어떻게 동작하나

쓰기 앞에 붙는 네 가지 장치는 전부 이 갈라짐을 좁히거나, 좁히지 못했을 때를 대비합니다.

하나, 범위 산정. 두 숫자를 나란히 놓습니다 — 요청 문장을 그대로 옮겼을 때 바뀌는 행 수와, 조건을 좁혔을 때 바뀌는 행 수. 이 두 숫자의 차이가 이 변경의 위험입니다. 45건과 20건이라면 25건이 위험이고, 그 25건이 왜 제외되는지를 한 줄씩 적을 수 있어야 조건이 완성된 것입니다.

둘, 되돌릴 길. 백업은 두 종류를 남기는데 쓰임이 다릅니다.

파일 사본   전부 잘못됐을 때 통째로 되돌린다.
            단점 — 그 사이에 들어온 남의 변경까지 함께 되돌아간다.
행 스냅샷   바꿀 행의 변경 전 값을 id 와 함께 남긴다.
            id 를 열쇠로 그 행만 되돌릴 수 있고, 무엇이 어떻게 바뀌었는지 설명할 수 있다.

파일 사본만 있으면 되돌릴 수는 있어도 설명할 수 없고, 행 스냅샷만 있으면 스키마가 바뀌는 변경을 되돌릴 수 없습니다. 그래서 둘 다 남깁니다.

셋, 시험 실행. 트랜잭션을 열고 실제 UPDATE 를 친 다음 커밋하지 않고 되돌립니다. SELECT COUNT(*) 로 미리 세는 것과 결과가 같아 보이지만 성질이 다릅니다. 세는 조건과 바꾸는 조건이 문자 그대로 같기 때문입니다. 두 문장을 따로 쓰면 그 사이에 오타가 숨고, 오타는 언제나 두 문장 중 실행되는 쪽에 있습니다.

넷, 중단 기준. 어떤 결과를 보면 멈출지를 실행 전에 정합니다. 이건 다음 모듈에서 따로 다룹니다.

그리고 적용이 끝난 뒤의 검증에는 절반이 더 있습니다. 대조군 — 바뀌지 말아야 할 것이 그대로인지 세는 일입니다. 바뀌어야 할 것만 세는 검증은 조건이 넓어 옆을 함께 건드린 사고에서도 멀쩡히 통과합니다.

현장에서 만나는 모습

첫째, 확인할 수 없는 것은 바꾸지 않습니다. 고객 테이블에 없는 customer_id 를 가진 주문처럼, 조건은 만족하지만 누구의 것인지 알 수 없는 행이 실제 데이터에는 거의 항상 섞여 있습니다. 오래됐다는 이유만으로 함께 취소하면 나중에 그 한 건을 설명할 수 없습니다. 제외하고, 제외한 사실과 이유를 계획서에 적고, 고객사에 물어보는 것이 정답입니다. 제외는 일을 덜 한 것이 아니라 판단을 기록한 것입니다.

둘째, 없던 값을 새로 만드는 변경은 조용히 퍼집니다. status 에 지금까지 없던 cancelled 를 넣으면, 그 값을 모르는 집계 쿼리와 대시보드는 그 행들을 조용히 빼고 셉니다. 에러가 나지 않으니 아무도 모르고, 한 달 뒤 정산이 맞지 않는 것으로 드러납니다. 그래서 새 값을 도입하는 변경은 데이터 변경이면서 동시에 통보해야 하는 변경입니다.

셋째, 실행 기록은 실행한 사람이 아니라 두 시간 뒤에 이 표를 이상하게 여길 다른 사람을 위한 것입니다. 언제, 무엇을, 몇 건, 무엇으로 되돌리는지가 한자리에 없으면 그 사람은 우리를 찾아내는 데 시간을 씁니다.

되돌릴 수 없는 변경을 되돌릴 수 있게 나누기

변경 계획에서 가장 중요한 항목은 "실패하면 어떻게 되돌리는가" 인데, 어떤 변경은 그 칸에 쓸 말이 없다. 그때는 계획을 고치는 것이 아니라 변경 자체를 나눈다.

열을 지우는 일은 세 번에 걸쳐 한다. 한 번에 하면 옛 코드가 도는 동안 깨진다.

  1. 코드에서 그 열을 안 읽게 만들어 배포한다(쓰기는 계속한다).
  2. 며칠 두고, 되돌릴 일이 없는지 확인한 뒤 안 쓰게 만들어 배포한다.
  3. 그다음에 열을 지운다.

각 단계는 앞뒤 어느 버전과도 함께 돌 수 있으므로, 언제든 배포를 되돌려도 된다.

열을 더하는 일도 마찬가지다. not null 을 처음부터 걸면 옛 코드의 insert 가 전부 실패한다. 널을 허용해 넣고, 기본값을 채우고, 코드가 항상 값을 넣게 만든 뒤에 제약을 건다. 큰 표에서는 제약을 not valid 로 먼저 걸고 나중에 검증하면 잠금 시간이 짧아진다.

긴 잠금은 배포 실패로 보이지 않는다. 마이그레이션이 표를 잠근 채 5분을 끌면 그동안 모든 요청이 쌓인다. 배포는 "성공" 으로 끝나고 장애만 남는다. 그래서 마이그레이션에는 반드시 잠금 상한을 건다.

set local lock_timeout = '3s';
set local statement_timeout = '30s';
alter table orders add column region text;

못 잡으면 실패하고 다시 시도하는 편이, 잡을 때까지 기다려 서비스를 멈추는 것보다 낫다.

되돌리기를 실제로 해 본다. 계획서의 "롤백: 이전 버전으로 배포" 는 대개 검증된 적이 없다. 개발계에서 한 번 되돌려 보면 설정 파일이 안 맞거나, 캐시에 새 형식이 남아 있거나, 큐에 새 메시지가 쌓여 있는 것을 발견한다.

변경 창은 사람을 위한 것이다. 새벽 3시에 배포하면 문제를 아무도 못 본다. 사람이 깨어 있고, 트래픽은 적고, 다음 날이 근무일인 시간이 가장 안전하다.

열을 지우는 일을 세 번에 나눈다

본문은 되돌릴 길을 쓸 말이 없는 변경을 만나면 계획을 고치지 말고 변경 자체를 나누라고 한다. 열을 지우는 경우의 세 걸음이다.

  1. 코드에서 그 열을 안 읽게 만들어 배포한다쓰기는 계속한다. 이 판은 옛 코드와 함께 돌아도 깨지지 않는다.
  2. 며칠 두고 되돌릴 일이 없는지 본 뒤 안 쓰게 만들어 배포한다이제 아무 코드도 그 열을 읽거나 쓰지 않는다. 각 단계는 앞뒤 어느 버전과도 함께 돌 수 있으므로 언제든 배포를 되돌려도 된다.
  3. 그다음에 열을 지운다앞의 두 단계가 끝난 뒤에만 지운다. 한 번에 하면 옛 코드가 도는 동안 깨진다.

여기서 구분할 것 열을 더하는 쪽도 같은 원리이다. not null 을 처음부터 걸면 옛 코드의 insert 가 전부 실패한다. 마이그레이션이 표를 잠근 채 오래 끌면 배포는 성공으로 끝나고 장애만 남으므로 잠금 상한을 건다. 이 순서는 레슨 본문이 제시하는 절차이다.

잠깐, 예측해 보세요 읽기를 끊는 배포 직후, 쓰기를 끊는 배포를 건너뛰고 곧바로 열을 지웠다. 이 시점에 무슨 일이 생길까?

설명 확인 · 채점 없는 자가 점검

읽기만 끊은 판은 쓰기를 계속하므로, 열이 사라지는 순간 그 판의 쓰기가 실패한다. 읽기를 끊는 배포와 쓰기를 끊는 배포를 나눠 두는 것은 단계마다 앞뒤 어느 버전과도 함께 돌 수 있게 하려는 것이다.

근거 문서

다음 실습에서 할 것

고객사가 "pending 인 주문을 전부 cancelled 로 바꿔 주세요" 라고 요청한 상황을 처음부터 끝까지 밟습니다. 요청대로 옮기면 45건이 바뀌는데 실제로 바꿔야 할 것은 20건입니다. 그 25건의 차이를 찾아내고, 되돌릴 길을 만들고, 사본에서 되돌리기를 시연한 뒤, 본 적용과 대조군 검증까지 끝냅니다.