AI에게 구현을 맡기며 설계를 한 번에 끝내지 않기로 했다
- AI
- 설계
들어가며
AI에게 구현을 전적으로 맡기게 되면서 설계에 가장 많은 시간을 들이게 됐다. 설계만 완벽하면 AI가 한 번의 구현으로도 원하는 결과물을 뽑아낼 수 있다고 믿었기 때문이다. AI는 설계를 충실히 따르는 만큼, 결과물이 기대와 다르다면 원인은 AI가 아니라 설계의 빈틈에 있다고 봤다. 구현을 몇 번씩 고치느니 그 빈틈을 설계 단계에서 미리 메우는 편이 낫다고 생각했다.
실제로는 그렇게 흘러가지 않았다. AI와 함께 공들여 짠 설계라도 코드로 옮기는 순간 어긋나는 지점이 생겼고, 구현을 몇 번씩 다시 고쳐야 했다. 처음에는 설계가 덜 꼼꼼했던 탓이라 여기고 설계 방식을 바꿔 봤다. 그런데 방식을 바꿀 때마다 어긋남은 사라지지 않고 모양만 달라졌다. 그렇게 몇 번을 실패하고 나서야, 설계를 어떻게 하느냐보다 먼저 던져야 할 질문이 있다는 걸 깨달았다. 설계는 정말 구현 전에 한 번에 끝낼 수 있는 일일까?
1단계: 기획서만 보고 설계했을 때
기획서에는 화면과 기능, 정책이 모두 적혀 있으니, 처음에는 그 내용만 빠짐없이 옮기면 설계가 된다고 봤다. 화면을 영역별로 나누고 컴포넌트 구조와 데이터 흐름을 정한 뒤, 기획서와 하나씩 대조해 봤다. 빠진 요구사항이 없었으니 이 정도면 AI가 그대로 구현할 수 있는 완벽한 설계라고 판단했다.
구현을 시작하자 가장 먼저 어긋난 건 컴포넌트를 어떤 단위로 나누느냐 같은 추상화 수준이었다. 목록 화면을 예로 들면, 설계는 기획서의 화면 영역을 기준으로 필터 영역, 목록 영역, 페이지 이동 영역을 각각 컴포넌트로 나눴다. 하지만 기존 코드는 화면 영역이 아니라 데이터를 기준으로 나뉘어 있어서, 필터 상태와 페이지 상태를 함께 다루는 컴포넌트가 이미 있었다.
설계대로 구현하니 그 컴포넌트가 하던 상태 관리를 새 컴포넌트들이 또 하게 됐다. 겹치는 상태 관리를 기존 컴포넌트에 맡기고 나니 영역별 컴포넌트에는 각자 애매한 조각만 남았고, 그 조각들을 한곳에 모으다 보니 새 컴포넌트 하나가 계획보다 훨씬 많은 역할을 떠안았다. 기획서만 봐서는 알 수 없는 충돌이었다.
어긋남은 설계가 틀린 곳에서만 나오지 않았다. 앞의 경우처럼 설계가 틀린 곳에서는 AI도 그대로 따라 틀렸지만, 설계가 아무 말도 하지 않은 곳에서는 이미 있는 공통 유틸을 두고 같은 기능의 함수를 새로 만들거나 로직을 계층에 맞지 않는 자리에 넣었다.
처음에는 이런 결과물을 보고 AI가 제멋대로 구현했다고 생각했다. 하지만 다시 들여다보니, AI는 기존 코드를 알 길이 없어 설계가 비워 둔 칸을 자기 방식대로 채웠을 뿐이었다. 문제는 AI의 판단이 아니라, 설계가 담지 못한 코드의 맥락이었다.
2단계: 주변 코드를 먼저 읽고 설계했을 때
그래서 설계에 들어가기 전에 코드의 맥락부터 채우기로 했다. AI에게 기존 코드를 탐색시켜 비슷한 기능이 이미 구현돼 있는지, 주변 코드는 어떤 패턴으로 작성돼 있는지 정리하게 하고, 그 결과를 설계에 담았다. AI가 찾아 준 코드를 "왜 이렇게 짰지?" 하며 직접 따라 읽느라 설계 전에 드는 시간은 늘었지만, 구현 결과는 눈에 띄게 달라졌다.
AI가 설계에 담긴 패턴을 거의 그대로 따라 하면서, 공통 유틸을 두고 같은 기능의 함수를 새로 만드는 일도 사라졌다. 구현 뒤에 몰리던 수정도 크게 줄어, 설계 방향만 맞으면 작업은 거의 막힘없이 끝났다. 1단계에서 설계가 비워 둔 칸을 자기 방식대로 채우던 AI가, 이번에는 같은 칸을 함께 넘겨받은 코드를 보고 채운 것이다. 설계가 말하지 않는 부분에서 AI는 눈앞의 코드를 견본으로 삼았다.
문제는 견본이 늘 지금 있는 코드뿐이라는 데 있었다. 설계를 기존 패턴에 맞출수록 그 패턴이 정말 맞는지는 묻지 않게 됐다. 새 컴포넌트가 들어오면 기존 코드와 묶어 공통으로 분리하거나 더 낫게 고칠 지점도 생겼을 텐데, 설계 단계에서는 아직 없는 코드까지 내다볼 수 없으니 그 지점이 보이지 않았다. 그래서 작업은 늘 기능을 빠르게 구현하고, 다 만든 뒤에야 공통으로 묶을 부분을 찾아 리팩터링하는 순서로 흘렀다. 틀린 방식은 아니었고 속도도 빨라졌지만, 처음부터 공통까지 설계해 두면 이 리팩터링도 줄일 수 있지 않을까 하는 생각이 들었다.
3단계: 공통까지 미리 설계해 두었을 때
이번에는 리팩터링에서 하던 일을 설계 단계로 당겼다. 기획서와 주변 코드를 함께 보면서 여러 화면에서 쓰일 만한 로직을 미리 찾아 추상화하고, 유틸로 분리해 둔 뒤 구현에 들어갔다. 2단계에서 봤듯 AI는 눈앞의 코드를 견본으로 삼는다. 그러니 공통 구조를 먼저 깔아 두면 다 만든 뒤 다시 묶을 일도 없을 거라고 봤다.
실제로는 리팩터링이 사라지지 않았고, 다 만든 뒤에 하던 그 일이 이번에는 구현 도중에 끼어들었다. 기능을 하나 구현할 때마다 미리 만든 추상화가 조금씩 맞지 않아서 공통 코드를 열어 고쳐야 했다. 설계할 때는 보이지 않던 공통 로직이 작업 도중에 새로 나타났고, 반대로 공통으로 분리해 둔 코드가 결국 한 곳에서만 쓰이기도 했다. 어떤 로직이 정말 여러 곳에서 같은 모양으로 쓰일지는 그 여러 곳을 실제로 만들어 봐야 알 수 있는데, 그걸 구현 전에 맞히려 한 것이다.
눈앞의 코드를 견본으로 삼는 AI의 성질도 이번에는 반대로 작용했다. 미리 만든 공통 코드가 새 기능과 맞지 않아도 AI는 그걸 쓰려고 했고, 맞지 않는 부분은 분기를 덧붙여 메웠다. 견본이 틀렸으니 그 틀린 부분까지 충실하게 따라 한 것이다. AI가 견본을 따르는 방식은 그대로였고, 달라진 건 그 견본이 실제 코드가 아니라 추측으로 만들어졌다는 점뿐이었다.
세 방식은 같은 이유로 어긋났다
세 방식을 나란히 놓고 보면 설계에 담은 것은 매번 늘었다. 1단계는 기획서의 내용만, 2단계는 거기에 주변 코드를, 3단계는 앞으로 쓰일 공통 구조까지 담았다. 담는 것이 늘수록 설계는 정교해졌지만, 구현 전에 설계를 한 번에 끝낸다는 전제는 한 번도 바뀌지 않았다.
따져 보면 설계에 필요한 정보는 두 종류였다. 하나는 코드를 읽으면 아는 정보로, 기존 컴포넌트가 어떤 기준으로 나뉘어 있는지, 쓸 수 있는 공통 유틸이 무엇인지 같은 것이다. 다른 하나는 구현해 봐야 아는 정보로, 어떤 로직이 정말 여러 곳에서 같은 모양으로 쓰일지, 미리 만든 추상화가 다음 기능에도 맞을지 같은 것이다.
1단계는 두 정보 모두 없이 설계했고, 2단계는 읽으면 아는 정보를 채워 덜 어긋났지만 구현해 봐야 아는 정보는 리팩터링으로 미뤘다. 3단계는 그 정보마저 구현 전에 추측으로 채우려다 다시 어긋났다. 따져야 할 건 설계를 얼마나 꼼꼼히 하느냐가 아니라, 구현해 봐야 아는 정보를 언제 채우느냐였다.
4단계: 흐름은 처음에, 세부 설계는 기능마다 이어받는다
그래서 설계를 두 층으로 나눴다. 첫 층은 작업을 시작할 때 한 번 정하는 전체 흐름이다. 어떤 기능이 필요한지, 기능 사이의 경계와 순서는 어떻게 되는지, 데이터가 어디서 어디로 흐르는지까지만 정한다. 이 정도는 기획서와 주변 코드를 읽으면 판단할 수 있어서, 구현해 봐야 아는 정보에 기댈 일이 없다. 앞의 세 방식이 어긋난 지점도 모두 이 범위 밖, 컴포넌트를 어떻게 나누고 무엇을 공통으로 묶을지 정하는 단계에 있었다.
둘째 층인 세부 설계는 기능 하나를 시작할 때마다 한다. 전체 흐름 설계와 앞 기능에서 실제로 만들어진 코드를 이어받아, 이번 기능의 컴포넌트를 어떻게 나눌지, 어떤 패턴을 따를지 정한다.
공통 구조도 이 자리에서 정한다. 어떤 로직이든 처음 쓰일 때는 인라인으로 두고, 같은 로직이 두 번째로 필요해지는 기능을 만나면 그 기능의 첫 작업으로 공통화부터 한다. 필요해질 때까지 만들지 않는다는 YAGNI 원칙을 기능 단위 설계에 옮긴 셈이다. 두 번째 사용처가 실제 코드로 눈앞에 있으니 구현해 봐야 아는 정보를 추측할 필요가 없고, 3단계처럼 한 곳에서만 쓰이는 공통 코드도 애초에 생기지 않는다.
공통화하려면 앞서 인라인으로 작성한 코드를 다시 열어야 하니, 수정이 완전히 사라지지는 않았다. 다만 3단계에서 구현 도중 예고 없이 공통 코드를 고쳐야 했다면, 지금은 다음 기능을 시작하는 첫 작업으로 수정이 들어간다. 예상하지 못한 수정이 예정된 수정으로 바뀌었고, 언제 손댈지 아는 수정은 더 이상 설계를 흔들지 않았다.
이 방식이 현실적으로 돌아가는 데에는 AI의 몫이 크다. 기능마다 설계를 이어받으려면 기능 하나의 구현이 짧게 끝나야 하는데, 그 구현을 AI가 맡는다. 작업 단위가 작으니 AI가 한 번에 만드는 코드도 줄어, 결과물을 끝까지 읽고 검토할 수 있다. 무엇보다 공통화를 첫 작업으로 끝내 두면, AI는 실제 사용처 두 곳을 보고 만든 공통 코드를 견본 삼아 이번 기능을 구현한다. 견본이 실제 코드에서 나오니, 3단계에서 역효과를 냈던 AI의 성질이 다시 장점이 됐다.
정리하며
처음 목표는 완벽한 설계 하나로 AI가 한 번의 구현에서 원하는 결과물을 뽑아내는 것이었다. 지금은 전체 흐름만 먼저 정하고, 세부 설계는 기능 하나를 시작할 때마다 앞 작업의 결과를 이어받아 정한다. 목표와는 꽤 멀어진 방식이지만, 설계를 한 번에 끝내려던 욕심을 내려놓자 구현 도중 예고 없이 설계로 되돌아가는 일은 오히려 줄었다.
설계는 구현 전에 한 번에 끝낼 수 있는 일이 아니었다. 완벽한 설계를 노린다는 건 구현해 봐야 알 수 있는 정보까지 설계에 미리 담으려는 일이었고, 그런 정보는 설계에 아무리 공을 들여도 구현 전에는 채울 수 없었다. 그렇다고 설계에 들이는 노력을 줄이자는 이야기는 아니다. 그 노력을 처음 한 번에 몰아 쓰지 않고, 기능을 하나씩 구현할 때마다 나눠 쓰자는 것이다.
아직 풀지 못한 부분도 있다. 전체 흐름 설계에 무엇까지 담을지, 기능 하나의 크기를 어디까지로 볼지는 여전히 감에 기대고 있다. 이 기준을 말로 정리할 수 있게 되면 그때 다시 써 보려 한다.