회고/인사이트 • Koal & KoalStudio

WineDuck 릴리스 사고 연대기: 실패를 런북 게이트로 바꾸기

WineDuck 앱 릴리스에서 확인된 잘못된 소스, 오래된 내장 번들, 툴체인 선택 실패를 시간순으로 돌아보고 사고를 불변식과 중단 조건으로 바꾼 과정을 정리합니다.

앱스토어 심사를 통과한 새 버전을 설치했는데 바뀌어야 할 앱 이름이 그대로였어요. 최근에 고친 화면도 보이지 않았고, 네이티브 변경도 빠져 있었어요. 빌드와 업로드는 정상적으로 끝났지만 사용자가 받은 것은 우리가 내려고 한 앱이 아니었습니다.

문제는 한 줄의 코드가 아니었어요. 여러 클론 중 오래된 소스에서 아카이브했고, 다시 만드는 과정에서는 최신 웹 자산이 앱 안에 들어갔는지를 놓쳤어요. 다른 시도에서는 빌드 도구가 의도한 Xcode 툴체인을 가리키지 않아 아카이브 전 단계에서 멈췄습니다. 각각 복구할 수 있는 문제였지만 복구 절차만 기억하면 다음 릴리스에서도 같은 자리가 열려 있어요.

WineDuck은 와인을 기록하고 다시 발견하는 현재 서비스예요. AsterDuck은 CoffeeDuck과 WineDuck을 역사적으로 포괄해 온 도메인 이름이고, 현재 네이티브 제품 정체성과 로그인 흐름은 WineDuck입니다. 웹 화면과 iOS 앱이 코드를 공유하지만, 라이브 웹과 앱에 내장된 웹 자산, 네이티브 바이너리는 저절로 같은 시점에 맞춰지지 않아요. 이번 글은 그 릴리스 구조를 다시 설명하기보다, 실제 실패가 어떻게 런북의 중단 조건이 됐는지를 시간순으로 봅니다.

첫 번째 사고: 폴더 이름을 최신이라고 믿었어요

여러 작업 폴더 중 하나에서 Xcode를 열었어요. 그 폴더가 최신 소스를 담고 있다고 생각했고, 아카이브와 업로드 자체는 문제없이 끝났습니다. 빌드 도구는 주어진 소스를 패키징할 뿐 “이 코드가 오늘 내기로 한 기준과 같은가”까지 판단하지 않아요.

사고 직후에는 최신 소스를 다시 받아 새 빌드를 만드는 것으로 복구했어요. 그러나 “다음에는 올바른 폴더를 연다”는 회고는 약했습니다. 사람의 기억과 폴더 이름은 참·거짓을 판정할 수 있는 조건이 아니기 때문이에요.

런북에는 폴더 선택 요령 대신 소스 동일성 조건을 남겼어요. 아카이브 대상의 커밋이 릴리스 기준 커밋과 다르면 다음 단계로 가지 않습니다. 무엇을 열었는지가 아니라 무엇에서 만들고 있는지를 비교하게 된 거예요.

두 번째 사고: 라이브 웹과 내장 번들을 같은 것으로 봤어요

최신 소스로 돌아왔지만 그것만으로 충분하지 않았어요. 라이브 웹에는 최근 화면이 보이는데 새 아카이브 안에는 이전 웹 자산이 들어갈 수 있었습니다. 웹 배포와 iOS 앱에 넣는 웹 자산 생성은 서로 다른 작업이기 때문이에요.

당시에는 웹 자산을 다시 만들고 네이티브 프로젝트에 동기화한 뒤 재아카이브했어요. 이 복구를 그대로 “웹 빌드 명령을 한 번 더 실행한다”로 적지 않았습니다. 명령 성공만으로는 복사 대상과 내장 결과가 맞는지 알 수 없기 때문이에요.

새 조건은 아카이브에 포함될 웹 자산이 검증한 릴리스 소스에서 방금 만들어졌고, 실제 내장 위치까지 동기화됐다는 것이었어요. 생성 또는 복사 증거가 없으면 아카이브를 시작하지 않습니다. 콜드 스타트와 OTA 적용 같은 기기 동작은 다음 글의 릴리스 레인에서 자세히 다루고, 여기서는 사고가 만든 중단 조건만 남겨요.

세 번째 사고: 코드가 아니라 툴체인 선택에서 멈췄어요

다음 실패는 소스 내용과 달랐어요. 빌드 명령이 의도한 Xcode 툴체인을 가리키지 않아 필요한 빌드 단계가 진행되지 않았습니다. 같은 명령도 어떤 개발 도구가 선택돼 있는지에 따라 다른 결과를 내요.

복구는 올바른 툴체인을 선택하고 다시 실행하는 일이었어요. 런북에는 특정 개발 환경을 당연한 전제로 두지 않고, 릴리스 시작 전에 선택된 도구와 버전을 확인하는 조건을 넣었습니다. 요구한 툴체인이 아니면 빌드를 계속하지 않아요.

이 사고가 중요했던 이유는 실패 위치가 달랐기 때문이에요. 첫 사고는 소스 정본, 두 번째는 산출물 동기화, 세 번째는 실행 환경의 문제였습니다. 모두 “빌드가 잘못됐다”로 묶었다면 예방책도 막연한 재확인에 머물렀을 거예요.

사고 문장을 중단 조건으로 바꿨어요

회고가 운영 자산이 되려면 “무슨 일이 있었다”에서 끝나지 않아야 해요. 틀린 가정을 찾고, 다음 실행에서 반드시 참이어야 할 문장으로 바꾸고, 거짓일 때 어느 행동을 막을지 정해야 합니다. 이번 연대기는 아래 한 표로 런북에 옮겨졌어요.

확인된 사고틀렸던 가정런북의 불변식거짓일 때 막는 행동통과 증거
오래된 클론에서 아카이브폴더 이름이 최신성을 보장한다아카이브 소스와 릴리스 기준 소스가 같다커밋 불일치 시 아카이브 중단두 커밋의 일치 결과
이전 웹 자산이 앱에 내장라이브 웹이 최신이면 앱 안도 최신이다내장 자산은 검증한 소스에서 새로 생성·동기화됐다생성·복사 확인 전 아카이브 중단빌드 결과와 동기화 기록
잘못 선택된 툴체인으로 빌드 실패실행 환경은 항상 같게 설정돼 있다릴리스가 요구하는 Xcode 툴체인이 선택돼 있다도구·버전 불일치 시 빌드 중단선택된 툴체인과 버전 출력

“확인한다”만 적으면 체크리스트예요. “일치하지 않으면 아카이브하지 않는다”까지 적어야 게이트가 됩니다. 복구는 현재 릴리스를 정상으로 돌리고, 게이트는 다음 릴리스의 자유도를 줄여요.

자동화할 수 있는 조건도 이 단계에서 드러났어요. 커밋 일치, 산출물 생성과 복사, 선택된 툴체인처럼 기계적으로 비교할 수 있는 값은 스크립트가 중단시킬 수 있습니다. 화면의 의미나 공개 시점처럼 맥락이 필요한 판단은 사람이 맡지만, 이것이 곧 저장소 런북에 별도 검증자 승인 사고나 필수 승인 게이트가 기록돼 있다는 뜻은 아니에요.

독립 검토를 운영상 추가할 수는 있어요. 다만 그것은 위 사고에서 직접 도출된 저장소의 필수 진실이 아니라, 나중에 팀이 선택할 수 있는 운영 보강책으로 구분해야 합니다. 사고 기록과 후속 운영 아이디어를 섞으면 무엇이 실제로 일어났고 무엇이 제안인지 알 수 없게 돼요.

버전 규칙도 사고 이력과 분리해 적어요

배포 식별자 규칙은 중요하지만, 이번 저장소 런북의 사고 연대기에 없는 버전 재사용 실패를 실제 사건처럼 덧붙이지 않아요. 대신 현재 절차에 필요한 규칙만 별도로 둡니다.

새 OTA 번들을 업로드할 때 버전은 고유하고 단조 증가해야 해요. 하지만 장애 대응으로 롤백할 때는 서버에 이미 보관된 이전 번들을 다시 활성 대상으로 지정할 수 있습니다. 정상 내용을 더 높은 새 버전으로 다시 발행하는 것은 롤백의 유일한 방법이 아니라 앞으로 이동하는 롤포워드 전략이에요.

App Store 식별자의 세부 규칙도 OTA와 하나로 묶지 않습니다. 출시된 마케팅 버전은 다시 쓸 수 없고, 아직 출시 전인 같은 마케팅 버전에 빌드를 추가로 올릴 때는 고유한 더 높은 빌드 번호가 필요해요. 이 구조와 적용 방식은 다음 글에서 각 릴리스 레인과 함께 설명합니다.

런북이 달라진 지점

사고 전 문서는 앱을 업데이트하는 순서를 설명하는 데 가까웠어요. 사고 뒤에는 각 단계가 다음 단계로 넘어갈 자격을 묻게 됐습니다. 기준 소스와 같은가, 웹 자산을 새로 만들고 동기화했는가, 요구한 툴체인이 선택돼 있는가. 하나라도 거짓이면 실행은 멈춰요.

각 조건은 입력, 통과 증거, 실패 시 돌아갈 위치를 가져야 해요. 그래야 담당자가 바뀌어도 지난 대화를 복원하거나 “이번에는 맞을 것”이라는 자신감에 기대지 않습니다. 코드와 릴리스 방식이 바뀌면 저장소의 런북도 같은 변경 단위에서 고쳐야 해요.

사고를 빨리 복구하면 다시 움직일 수 있어요. 사고의 틀린 가정을 불변식으로 바꾸고, 그 불변식이 거짓일 때 다음 행동을 막으면 같은 자리에서 다시 멈출 가능성을 줄일 수 있습니다. 이번 연대기가 남긴 것은 더 긴 절차가 아니라, 기억 대신 통과 조건으로 움직이는 릴리스예요.

댓글

댓글 남기기

댓글 삭제 시 사용됩니다
공개되지 않습니다
0/2000
← 목록으로