· 16 min read
병목은 사라지지 않고 옮겨 간다 병목은 사라지지 않고 옮겨 간다
구현 → 이해 → 환경 → 검증. AI에게 일을 맡긴 뒤 막히는 곳이 옮겨 간 순서. 구현 → 이해 → 환경 → 검증. AI에게 일을 맡긴 뒤 막히는 곳이 옮겨 간 순서.
공장의 한 공정을 두 배로 빠르게 해도 전체 생산량은 두 배가 되지 않습니다. 그다음으로 느린 공정이 드러날 뿐입니다. 골드랫의 제약 이론이 말하는 것입니다.
예전 개발에서 가장 느린 공정은 손이었습니다. 지금은 그 공정을 AI가 맡고, 막히는 곳이 차례로 옮겨 가고 있습니다.
구현에서 이해로
구현이 빨라지면 변경이 많아지고, 변경이 많아지면 그걸 이해하는 쪽이 느려집니다. worktree 네 개를 동시에 돌리던 때는 PR이 너무 자주 올라와서 diff를 읽는 것 자체가 일이 됐습니다(CI 글).
처음에는 이해를 빠르게 하는 방법을 찾았습니다. 요약을 붙이고 PR을 묶었습니다. 요즘은 한 발 더 나가서, 이해하는 것 자체를 내려놓는 실험을 하고 있습니다.
이해를 내려놓기
9월 27일에 시작한 Spill이라는 게임이 있습니다. 저는 이 프로젝트의 코드를 본 적이 없고, PR 리뷰를 한 적도 없습니다. 무엇을 만들지 정하고, 만들어진 게임을 해 보고, 머지만 합니다.
나흘 동안 PR 59개가 머지됐습니다. 세션 기록을 세어 보면 이 저장소를 고친 세션 41개 중 40개가 에이전트가 이슈를 읽고 혼자 PR까지 올린 구현 세션입니다. 속도는 경이로운 수준이고, 품질도 만족스럽습니다. 모든 변경을 제 머릿속 모델에 맞춰 넣으려 할 때는 에이전트가 아무리 빨라도 제 속도에 묶여 있었습니다. 이해하려는 것 자체가 병목이었습니다.
아무것도 안 보는 건 아닙니다. 이해하지 않는 것은 코드이고, 판단하는 것은 결과입니다. 아키텍처 문제는 결국 드러나고, 그때 리팩터링하면서 한 번씩 정리합니다. 처음부터 좋은 구조를 붙잡고 있는 것보다 그쪽이 빨랐습니다.
테스트도 AI가 짰는데
여기서 짚고 넘어갈 게 있습니다. 테스트도 다 AI가 짭니다. 코드를 잘못 이해한 AI는 테스트도 같은 방향으로 잘못 짤 수 있으니, 테스트가 있다는 것만으로는 근거가 약합니다.
제가 실제로 기대고 있는 건 테스트 그 자체보다, 테스트가 무엇에 묶여 있느냐입니다. Spill에서는 빌드가 나오면 제가 직접 해 보고, 이상한 게 있으면 이슈로 남깁니다. 무엇이 통과인지는 제가 정한 설계와 결정에서 나오고, 에이전트는 그걸 테스트로 옮깁니다. 그리고 Spill의 판은 같은 입력이면 같은 결과가 나와야 합니다. 이건 AI가 코드를 어떻게 짰든 속이기 어려운 기준이라 꽤 믿고 있습니다.
이게 한 번에 갖춰진 건 아닙니다. 10월 1일에 실험용으로 만든 꺾인 계단 층이 정식 둘째 층으로 들어간 채 빌드에 나갔습니다. 해 보니 병이 깨지지 않고 골까지 가는 게 거의 불가능했습니다. 실험 기록에는 이미 그런 신호가 있었는데, 그걸로 막는 장치가 없었습니다. 그래서 그날 층마다 통과 기준을 정하고, 그 검사를 통과한 층만 플레이어 순서에 넣게 했습니다. 그 뒤로 기존 둘째·셋째 층이 이 검사에서 떨어졌고, 다시 설계한 층이 통과하고 나서야 들어갔습니다. 해 보면서 찾은 걸 검사로 옮겨 두면, 다음번에는 제가 해 보기 전에 걸립니다.
물론 Spill이 유리한 경우이기는 합니다. 게임이라 결과를 몇 초 만에 눈으로 볼 수 있고, 해 본 기간도 아직 일주일이 안 됩니다. 결과가 눈에 안 보이는 일이나 사람도 맞는지 판단하기 어려운 일이라면, 검증 환경을 잘 갖춰도 엉뚱한 데로 갈 수 있다고 봅니다. 코드를 안 읽어도 되는 건 결과를 제가 판단할 수 있는 동안까지라고 생각합니다.
이해 대신 측정
코드와 아키텍처는 중요합니다. 그보다 중요한 건 제품이 달성해야 하는 목표입니다. 제품 가치를 빼고 기술 쪽만 보면 목표는 품질 속성 세 가지로 좁혀집니다. 성능, 확장성, 유지보수성. 이걸 잴 수 있으면 코드를 읽지 않아도 됩니다.
성능은 벤치마크와 회귀 테스트를 CI에 걸어 두면 됩니다. Building Evolutionary Architectures는 이렇게 아키텍처 특성을 객관적으로 재는 장치를 적합도 함수라고 부릅니다.
확장성과 유지보수성은 새 요구사항에 얼마나 고쳐야 했는지로 봅니다. 요구사항 하나에 여러 곳을 동시에 고쳐야 한다면 산탄총 수술이라는 냄새입니다. 그래서 요구사항 앞에서는 고칠 범위를 에이전트에게 먼저 묻고, 끝난 뒤에는 git 기록을 넘겨 늘 함께 바뀌는 파일이나 유독 자주 바뀌는 파일을 찾게 합니다. 찾은 걸 놓고 논의한 뒤 리팩터링합니다.
이 글을 쓰면서 Spill에 실제로 돌려 봤습니다. 커밋 73개를 세는 데 몇 초가 걸렸고, 결과는 분명했습니다. 판 진행을 맡은 RunDirector.cs 하나가 2,422줄이고, 전체 커밋의 37%(27개)에서 바뀌었습니다. 판 기록 파일은 14번 바뀌는 동안 매번 이 파일과 함께 바뀌었고, 개발 도구 쪽 파일도 14번 중 13번을 같이 바뀌었습니다. 새 기능이 들어올 때마다 같은 파일에 살이 붙고 있다는 뜻입니다. 저는 이 파일을 열어 본 적이 없지만, 어디를 정리해야 하는지는 알게 됐습니다.
정리할 곳은 이슈로 올려 두었습니다. 그런데 이틀 뒤 다시 재 보니 이 파일은 3,412줄이 되었고, 바뀐 커밋 비율도 41%로 올라 있었습니다. 기능을 넣는 속도가 빠른 만큼 구조가 무너지는 속도도 빠릅니다. 측정을 한 번 하고 끝내지 않고 주기적으로 돌려야 하는 이유입니다.
써 놓고 보니 전부 있던 얘기입니다. 설계를 미루는 건 XP의 진화적 설계이고, 테스트와 리팩터링이 있으면 나중에 바꾸는 비용이 크게 늘지 않는다는 Kent Beck의 평평한 변경 비용 곡선이 그 근거입니다. 세 번째 중복에서 정리하라는 Rule of Three, git 기록에서 핫스팟과 변경 결합을 찾는 Adam Tornhill의 Your Code as a Crime Scene도 있습니다. 달라진 건 비용입니다. 알면서도 사람 시간이 없어 못 하던 방법론을 이제 에이전트가 돌립니다.
환경도 병목이 된다
사람 혼자 일할 때는 문맥이 한두 개라 한 머신에 여러 프로젝트를 띄울 일이 드물었습니다. 에이전트 여럿이 동시에 로컬에서 빌드하고 에디터를 띄우자 머신이 나눠 쓰는 자원이 됐습니다. 제가 에디터로 열어 둔 프로젝트를 에이전트와 같이 쓸 수도 있습니다. 하지만 제가 플레이해 보는 동안 에이전트가 파일을 고치면 에디터가 다시 컴파일하고, 둘이 같은 에디터를 조작하면 서로의 작업을 망칩니다. 그래서 작업마다 저장소 사본(git worktree)을 따로 만들었고, Unity 는 프로젝트 폴더 하나에 에디터 하나만 띄울 수 있어서 사본마다 에디터도 하나씩 더 떴습니다. 그렇게 에디터가 여러 개 뜨면 메모리를 두고 다투다 엉뚱한 오류가 났고, 성능을 재는 동안 다른 작업이 돌면 그 수치는 버려야 했습니다. 예전엔 서버 운영에서나 하던 고민(격리, 점유, 정리)이 책상 위로 올라왔습니다.
빌드와 테스트를 기다리는 시간
이해를 검증에 넘기고 나니 마지막 병목이 선명해졌습니다. 검증 루프 자체입니다.
고치는 데는 몇 분이면 되지만, 빌드와 테스트를 거쳐 결과를 받기까지는 몇십 분에서 한 시간이 걸립니다. 지난번 세션 분석에서 6주치 기록을 세어 보니 에이전트가 명시적으로 기다린 시간만 12.6시간, 테스트 명령이 돈 시간은 따로 8.7시간이었습니다. CI는 한 번에 35분에서 70분이 걸립니다. 러너는 여건상 한 대만 쓰고 있어서, 차례를 기다리는 PR이 쌓입니다. 검증에 기대는 방식은 검증이 빠를 때만 빠릅니다.
그래서 요즘은 프로젝트 자체가 더 빨리 검증되게 만드는 쪽을 고민하고 있습니다.
먼저 어디서 기다리는지 쟀습니다. 하루 동안 Unity 에이전트 9개가 쓴 726분을 나눠 보니 생각하고 코드를 쓰는 데 33%, 45초 넘게 기다리는 데 53%를 썼습니다. 가장 큰 덩어리는 플레이어 빌드(21번, 96분)였고, 에이전트 하나가 IL2CPP 빌드를 15번 돌린 경우도 있었습니다. 원인의 상당수는 “플레이어 빌드로 확인하라”고 쓴 제 지시서였습니다.
그래서 지금까지 바꾼 것은 이렇습니다.
- 확인은 에디터에서, 플레이어 빌드는 끝에 한 번. 플레이어에서만 달라지는 변경이 아니면 빌드하지 않습니다.
- 새 사본은 데워진 상태로 시작. 새 worktree는 임포트를 처음부터 다시 해서 첫 빌드가 14~32분 걸렸습니다. 이미 데워진
Library폴더를 APFS 클론(macOS의 복사 방식으로, 내용을 실제로 복사하지 않고 원본과 공유하다가 바뀐 부분만 새로 씁니다)으로 가져오면 몇 초에 끝나고 디스크도 거의 안 씁니다. - 고친 곳만 로컬에서, 전체는 CI에서. 검증 명령 하나가 고친 범위의 테스트만 돌리고, 끝나기를
sleep으로 기다리지 않고 이벤트로 받습니다.
남은 건 CI입니다. 러너 한 대를 여러 저장소가 나눠 쓰는 한, PR이 늘수록 줄은 길어집니다.
맺으며
AI로 일을 굴리면 병목은 없어지지 않고 자리를 옮깁니다. 그래서 병목을 없애려고 하기보다는, 지금 어디로 옮겨 갔는지 보고 거기를 손보는 쪽으로 생각하고 있습니다. 무엇을 AI에게 맡길지도 “AI가 뭘 잘하나”보다 “지금 어디가 막히나”를 보고 정합니다.
가장 내려놓기 어려웠던 병목은 제 자신이었습니다. 이해하고 통제하고 싶은 마음은 개발자로서 자연스럽지만, 그게 가장 느린 공정일 수 있습니다. 다만 내려놓은 자리를 뭔가는 채워야 합니다. 저는 직접 해 보는 것, 그리고 해 보다 찾은 걸 검사로 남겨 두는 것으로 채우고 있습니다.
참고
- Eliyahu M. Goldratt, The Goal (1984). 제약 이론(Theory of Constraints)
- Neal Ford, Rebecca Parsons, Patrick Kua, Building Evolutionary Architectures (O’Reilly, 2017). 2장 Fitness Functions
- Thoughtworks Technology Radar, Architectural fitness function
- Martin Fowler, Refactoring (1999, 2판 2018). 코드 냄새 Shotgun Surgery, Don Roberts의 Rule of Three
- Kent Beck, Extreme Programming Explained (1999). 평평한 변경 비용 곡선은 Martin Fowler, Is Design Dead?에 잘 정리되어 있습니다
- Adam Tornhill, Your Code as a Crime Scene (Pragmatic Bookshelf, 2015). Code as a Crime Scene, SE Radio 554: Behavioral Code Analysis