드라이런과 멱등성: 사고를 막는 시점의 차이
배포 스크립트에 안전장치를 넣는다고 할 때 두 가지가 자주 같이 나옵니다.
./deploy.sh --dry-run # 무엇이 일어날지만 본다PUT /orders/9f31 # 여러 번 보내도 결과가 같다둘 다 "실수해도 괜찮게" 만드는 장치라 한 묶음으로 이해하기 쉽습니다. 그런데 이 둘은 막는 시점이 다릅니다. 드라이런은 아직 아무 일도 안 일어났을 때 쓰고, 멱등성은 이미 한 번 일어난 뒤에 쓰입니다.
이 차이를 놓치면 둘 중 하나만 갖춰 놓고 다 갖췄다고 생각하게 됩니다.
부작용만 끄는 드라이런
흔히 「아무것도 안 하고 결과를 예측해 보여 주는 기능」으로 이해합니다. 절반만 맞습니다.
제대로 된 드라이런은 실행할 때와 똑같은 경로를 탑니다. 같은 설정을 읽고, 같은 목록을 만들고, 같은 검증을 통과합니다. 마지막의 쓰기 한 번만 건너뜁니다.
if (COMMIT) await api(`/edits/${id}/listings/${lang}`, { method: 'PUT', body: listing });
console.log(`${COMMIT ? '·' : '(미리보기)'} ${lang} 「${listing.title}」`);「예측」이 아니라는 점이 중요합니다. 예상 결과를 별도 코드로 계산해 보여 주는 방식은 실제 경로와 갈라지고, 갈라진 뒤에도 미리보기는 여전히 그럴듯하게 나옵니다. 그래서 드라이런의 값어치는 정확히 「실행 경로를 얼마나 그대로 탔는가」에 비례합니다.
부작용을 허용하되 겹쳐도 같게 만드는 멱등성
멱등성은 반대쪽입니다. 실제로 씁니다. 다만 두 번 써도 최종 상태가 한 번 쓴 것과 같습니다.
응답까지 같아야 하는 것은 아닙니다. DELETE 를 두 번 보내면 첫 번째는 204, 두 번째는 404 가 올 수 있는데 그래도 멱등합니다. 자원이 없다는 최종 상태가 같기 때문입니다. 이 구분은 따로 정리해 둔 글이 있습니다.
서로를 대신하지 못하는 이유
| 드라이런 | 멱등성 | |
|---|---|---|
| 언제 | 실행 전 | 실행 중·후 |
| 부작용 | 없음 | 있음 (겹쳐도 같음) |
| 막는 사고 | 틀린 것을 하는 것 | 맞는 것을 두 번 하는 것 |
| 누가 쓰나 | 명령을 내리는 사람 | 재시도하는 기계 |
드라이런은 「이게 맞나」에 답하고, 멱등성은 「또 해도 되나」에 답합니다.
멱등한 API 도 대상을 잘못 고르면 잘못된 대상에 멱등하게 씁니다. 몇 번을 보내도 틀린 곳이 틀린 상태로 정확히 수렴합니다. 반대로 드라이런으로 확인한 명령도 네트워크가 끊겨 재시도하면, 멱등하지 않은 경우 두 번 반영됩니다.
소방이 아니라 폭격 쪽일 가능성이 큰 유래
dry run 이라는 말은 컴퓨터에서 나오지 않았습니다.
널리 퍼진 설명은 소방입니다. 소방차 출동 훈련에서 물을 쓰지 않는 연습을 dry run, 실제 방수를 하는 것을 wet run 이라 불렀다는 것입니다.
다른 설명은 군용 항공입니다. 폭탄을 떨어뜨리지 않는 연습 비행이 dry run 이고 실탄 투하가 live run 입니다.
사전들이 이 표현의 등장을 1940년대 초로 잡습니다. 그 시기라면 2차대전 항공 쪽이 자연스럽습니다. 소방 이야기는 훨씬 자주 인용되는데 출처를 대기는 더 어렵습니다. 저도 어느 쪽이 먼저인지는 확신이 없습니다. 다만 두 설명이 공통으로 말하는 것은 있습니다. 실제와 똑같이 움직이되 나가는 것만 뺀다는 구조입니다. 소방차는 출동하고 폭격기는 목표 상공까지 갑니다.
더 중요한 것은 기본값의 위치
드라이런을 붙이는 방식이 둘로 갈립니다.
tool deploy -n # 기본은 실행. -n 을 붙여야 미리보기
tool deploy --commit # 기본은 미리보기. --commit 을 붙여야 실행앞쪽은 -n 을 기억해야 안전하고, 뒤쪽은 잊어버려도 안전합니다. make -n, rsync -n, git --dry-run 이 전부 앞쪽입니다. 관례가 그쪽으로 굳었고, 대부분의 도구에서는 그게 맞습니다.
다만 한 번의 실수가 되돌리기 어려운 명령이라면 뒤쪽이 낫습니다. 스토어 등록정보를 바꾸거나, 프로덕션 트랙에 릴리스를 올리거나, 라이브 DNS 를 갈아 끼우는 종류입니다. 플래그 하나 더 치는 비용과 잘못 나간 것을 되돌리는 비용이 비교가 안 됩니다.
드라이런의 약점
미리보기와 실행이 별개 실행이라는 점입니다.
tool deploy # 목록을 읽고 보여 준다
tool deploy --commit # 목록을 다시 읽고 실행한다그 사이에 무엇이 바뀌면 본 것과 다른 것이 나갑니다. 혼자 쓰는 스크립트에서는 부딪힐 일이 거의 없지만, 여럿이 같은 자원을 만지는 곳에서는 실재하는 틈입니다.
Terraform 의 plan 과 apply 가 그 틈을 메웁니다. plan 이 계획을 파일로 저장하고 apply 가 그 파일을 먹습니다. 검토한 것과 실행되는 것이 같은 물건입니다.
여기까지가 확실한 부분입니다. 저장된 계획을 쓰는 방식이 항상 옳은지는 저도 판단이 서지 않습니다. 계획이 낡으면 그건 그것대로 문제고, 그래서 Terraform 도 apply 시점에 상태를 다시 확인합니다. 틈을 없앤 게 아니라 좁힌 것에 가깝습니다.
설계할 때 무엇이 달라지나
한 문장으로 갈립니다. 드라이런은 사람을 위한 것이고, 멱등성은 기계를 위한 것입니다.
사람이 명령을 내리는 자리에는 드라이런이 필요합니다. 명령을 내리기 전에 무엇이 일어날지 봐야 하니까요. 기계가 재시도하는 자리에는 멱등성이 필요합니다. 재시도하는 쪽은 미리보기를 읽지 않으니까요.
CLI 를 만든다면 앞쪽을, API 를 만든다면 뒤쪽을 먼저 봐야 합니다. 둘 다 있는 자리도 있습니다. 배포 파이프라인이 그렇습니다. 사람이 트리거하고 기계가 재시도하니까요.
함께 읽기
- colima 자동 기동: brew services가 10초마다 재실행되는 이유개발기에 도커를 새로 깔 일이 생겼습니다. 컨테이너 하나만 돌리면 되는 일이라 가볍게 끝날 줄 알았는데, 설치 자체는 5분이었고 그 뒤에 두 가지를 더 고쳐야 했습니다. 하나는 깃 저장소가 더러워진 것이고, 하나는 코어 하나의 2.8%가 계속 타고 있던 것입니다.
- 새 맥북 개발 환경 세팅: git clone이 가져오지 않는 것들맥북을 새로 받고 하던 프로젝트를 이어서 하려 했습니다. 리포를 클론하고 npm install 을 돌린 다음 개발 서버를 띄웠는데, 브라우저에 이게 떴습니다.
- xcode-select와 DEVELOPER_DIR: 어느 쪽이 정석일까?Expo 앱을 iOS 시뮬레이터에 띄우려고 도구부터 점검했는데, 결과가 앞뒤가 안 맞았습니다.
- prisma generate 를 어디서 돌릴까: postinstall 과 도커 빌드 캐시새 맥에 프로젝트들을 하나씩 세우는 중이었습니다. 클론하고, 의존성을 깔고, 개발 서버를 띄우면 이 화면이 떴습니다.
- 환경변수로 둔 CLI 기본값: 비대화형 셸에서 사라지는 이유문서를 저장하는 CLI를 하나 만들었습니다. 저장할 때마다 그 문서가 어디로 떨어졌는지 찾는 일이 반복돼서, 새 문서는 인박스로 가도록 기본값을 정했습니다. 터미널에서 쳐 보니 의도한 자리에 들어갔습니다.