← 01 THINK어떻게 생각하는가
검사한 것과 실제로 내보내는 것이 달랐다
"테스트할 땐 됐는데 배포하니 깨졌다"가 반복되는 이유. 사람 실수가 아니라, 검사한 묶음과 내보낸 묶음이 애초에 다르기 때문이다.
이 쪽은 원문을 쉬운 말로 다시 쓴 판입니다. 저자가 쓴 문장은 원문 쪽에 있습니다. 이 판의 문장은 사람이 쓰지 않았습니다.
"테스트할 때는 잘 됐는데, 실제로 내보내니까 깨졌어요."
이 말이 자주 나오는 팀에서는 보통 누가 실수했는지를 찾습니다. 그런데 일하는 방식에 따라서는 이게 처음부터 그렇게 될 수밖에 없는 구조일 때가 있습니다.
먼저 알아야 할 말
- 갈래(브랜치) — 같은 코드를 여러 갈래로 나눠서 따로 작업하는 것입니다. 각자 자기 갈래에서 만들고, 다 되면 다시 하나로 모읍니다.
- 합치기 — 갈라진 갈래를 다시 하나로 모으는 일입니다.
- 배포 — 완성된 코드를 실제 서비스에 올리는 일입니다.
무엇이 어긋나 있었나
기능 A, B, C를 만들고 있다고 해 봅시다.
셋을 한 갈래에 모아 놓고 "A와 B와 C가 같이 잘 도는지" 검사합니다. 그런데 실제로 내보낼 때는 다른 갈래를 새로 만들어서 A와 B만 골라 담습니다.
그러면 이렇게 됩니다.
- A+B+C는 검사했지만 내보내지 않습니다.
- A+B는 내보내지만 검사한 적이 없습니다.
고장은 기능 하나하나보다 여럿이 섞였을 때 더 많이 납니다. 섞이는 조합이 바뀌면 앞서 한 검사도 같이 쓸모없어집니다.
그러니 "테스트할 땐 됐는데"는 사람 잘못이 아닙니다.
그럼 그 방식이 나쁜가
아닙니다. 그렇게 하는 데는 이유가 있습니다.
내보낼 갈래를 최대한 늦게 만들면, 만드는 그 순간에 무엇이 나갈 수 있는지 이미 정해져 있습니다. 나갈 것만 골라 담으면 되고, 기능 하나가 빠져도 그냥 안 넣으면 그만입니다.
정기적으로 배포하는 조직에서는 합리적인 방식입니다. 버릴 이유가 없는 능력입니다.
그래서 질문은 "이 방식이 틀렸나"가 아닙니다. 늦게 고르는 편함은 그대로 두면서, 검사한 것을 그대로 내보낼 수는 없나 입니다.
착각하고 있던 지점
늦게 갈래를 만들어서 늦게 고를 수 있는 거라고 생각했습니다. 아니었습니다.
고를 수 있는 진짜 이유는 아직 합치지 않았기 때문입니다. 갈래를 언제 만들었는지는 상관이 없습니다.
그러니 내보낼 갈래를 처음부터 만들어 두고, 기능이 검사를 통과할 때마다 하나씩 합쳐도 됩니다.
고르는 시점은 여전히 늦습니다. 달라지는 건 하나입니다 — 검사를 실제로 내보낼 그 갈래에서 하게 됩니다.
능력을 잃지 않고 구조만 고치는 길입니다.
기능 하나를 빼야 할 때
방법이 네 가지 있고, 갈래를 먼저 만들어 놨어도 전부 쓸 수 있습니다.
| 방법 | 드는 비용 | 언제 | |
|---|---|---|---|
| ① | 아직 안 합쳤으면 그냥 안 합친다 | 없음 | 기본 |
| ② | 넣되 스위치로 꺼 둔다 | 스위치 장치가 필요 | 여러 기능이 코드를 나눠 쓸 때 |
| ③ | 넣었다가 되돌린다 | 얽히면 골치 | 이미 합쳤는데 취소할 때 |
| ④ | 다음 번에 넣는다 | 기다림 | 일정이 확실히 밀렸을 때 |
주의할 게 둘 있습니다. 여러 기능이 같은 코드를 나눠 쓰고 있으면 ③을 쓸 때 남의 기능까지 같이 되돌아갑니다. 그리고 데이터베이스 구조 변경은 ②로 못 숨깁니다. 스위치 뒤에 숨을 수 있는 종류가 아닙니다.
되돌리기는 생각한 대로 동작하지 않는다
③을 쓰기 전에 직접 시험해 봤는데, 결과가 예상과 달랐습니다.
기능을 합쳤다가 되돌리고, 그 기능에 새 내용을 조금 더한 뒤 다시 합쳐 봤습니다. 처음에 넣었던 코드는 돌아오지 않았습니다. 되돌린 뒤에 새로 만든 것만 돌아왔습니다.
관리 도구가 "이미 합친 적 있는 것"은 건너뛰기 때문입니다.
가장 위험한 점은 경고가 하나도 안 뜬다는 것입니다. 조용히 성공한 것처럼 끝납니다. 담당자는 "합쳤는데?"라고 생각하고, 검사하는 사람은 없는 기능을 검사합니다.
게다가 새로 만든 내용이 같은 파일을 통째로 덮어쓰면 우연히 돌아오기도 합니다. 결과를 예측할 수 없다는 뜻이고, 이건 "절대 안 돌아온다"보다 나쁩니다. 한 번 우연히 성공하면 규칙을 잘못 배웁니다.
해결법은 되돌린 것을 다시 되돌리는 것입니다. 시험해 보니 전부 정확히 복구됐습니다.
바깥 회사와 함께하는 일은 절반만 통한다
날짜를 우리가 정하지 못하기 때문입니다. 상대 회사의 개발·검수·계약·오픈 날짜가 우리 일정과 상관없이 움직이고, 미리 알려 주지도 않습니다. 그리고 취소가 아니라 연기라서 코드를 버릴 수도 없습니다.
바꿀 수 있는 건 하나입니다. 코드를 올리는 날과 기능을 켜는 날을 따로 두는 것.
그러면 기다리는 상태가 '갈래에 묶인 미완성'에서 '이미 올라가 있는데 꺼진 것'으로 바뀝니다. 연기 통보가 와도 되돌릴 게 없습니다. 켜는 날짜만 미루면 됩니다.
그리고 바깥 연동은 스위치 장치가 없어도 거의 공짜로 됩니다. 어차피 주소나 열쇠 같은 설정값이 있어야 동작하니, 설정값이 없으면 아예 시작되지 않게 짜면 그게 곧 스위치입니다.
- 설정이 없으면 고장이 아니라 꺼진 상태로 둡니다. 대신 "지금 꺼져 있다"는 기록은 남깁니다. 안 그러면 안 켜진 걸 아무도 모릅니다.
- 화면의 버튼도 같은 스위치에 묶습니다. 버튼만 보이고 안 되면 사용자는 고장으로 받아들입니다.
- 꺼지면 연동 전 동작으로 돌아갑니다. 상대 회사에 장애가 났을 때도 같은 스위치를 씁니다.
안 그러면 이렇게 됩니다. 연동 코드가 갈래에 몇 주씩 묶여 있는 동안 본체는 수백 걸음 앞서 갑니다. 나중에 합치려 하면 연동 작업이 아니라 그동안 쌓인 충돌부터 풀게 되고, 그 코드는 처음 검사했던 코드가 아니라서 검사도 다시 해야 합니다. 가장 오래 기다린 기능이 가장 위험한 상태로 나갑니다.
널리 쓰는 방식 네 가지
| 방식 | 계속 두는 갈래 | 갈래 수명 |
|---|---|---|
| Git Flow | main, develop | 며칠 ~ 몇 주 |
| GitHub Flow | main | 몇 시간 ~ 이틀 |
| GitLab Flow | main + 환경별 갈래 | 몇 시간 ~ 며칠 |
| Trunk-Based | main | 하루 미만 |
넷이 공통으로 지키는 것은 하나입니다 — 검사한 그 코드를 그대로 내보낸다. 서로 다른 건 갈래를 얼마나 짧게 두느냐, 계속 두는 갈래를 몇 개 두느냐뿐입니다.
Git Flow를 만든 사람은 2020년에 자기 글에 주석을 달았습니다. 계속 배포하는 팀이라면 더 단순한 방식을 쓰라고요. 버전을 여러 개 동시에 지원해야 할 때만 여전히 쓸 만하다고 봤습니다.
Trunk-Based는 스위치 장치가 있어야 성립합니다. 없이 하면 미완성이 그대로 나갑니다.
계속 두는 갈래와 한 번 쓰고 지우는 갈래를 섞지 않는다
main 처럼 계속 두는 갈래와, 기능용처럼 합치면 지우는 갈래는 성격이 다릅니다.
이 구분이 흐려지면 갈래 목록이 신호를 잃습니다. 지금 살아 있는 게 뭔지 눈으로 알 수 없게 되고, 죽은 갈래에 실수로 코드를 올립니다.
오늘 당장 할 수 있는 것
- 배포할 때마다 표시를 자동으로 남깁니다. 되돌릴 지점을 하나로 가리킬 수 있어야 합니다.
- 되돌리기를 평소에 한 번 해 봅니다. 문서에만 있는 절차는 사고 났을 때 처음 해 보게 됩니다.
- 합쳐진 갈래는 자동으로 지웁니다.
- 계속 두는 갈래 목록은 문서 한 곳에만 적습니다.
구조를 바꾸는 가장 작은 방법은 내보낼 갈래를 먼저 만들고 거기서 검사하는 것입니다. 준비된 것만 합치면 늦게 고르는 편함은 그대로이고, 검사한 코드가 곧 내보낸 코드가 됩니다.
어느 쪽을 택하든 갈래 수명 목표를 숫자로 적어야 합니다. "짧게"는 목표가 아닙니다.