레슨 3 - 아키텍처: 생각을 정리하는 법을 배우세요
요약
AI 에이전트가 시스템 아키텍처를 설계할 때 발생하는 '저항이 가장 적은 경로'의 위험성을 다룹니다. AI는 당장의 과제 해결에 최적화되어 경계를 허물거나 과잉 엔지니어링을 할 수 있으므로, 개발자의 명확한 아키텍처 조종이 필요함을 강조합니다.
핵심 포인트
- AI는 아키텍처 설계보다 프롬프트 해결에 최적화되어 있음
- 단기적 해결을 위해 경계를 허물거나 하드코딩하는 경향이 있음
- 잘못된 피드백 시 불필요한 추상화를 도입하는 과잉 엔지니어링 발생
- 에이전트 사용 시 시스템 레이어와 워크플로우에 대한 인간의 통제 필수
AI는 저항이 가장 적은 경로를 택합니다. 에이전트(Agent)가 루프(Loop) 안에 들어온 이후, 시스템 아키텍처를 설계하는 방식에 있어 저에게 일어난 변화의 대부분은 바로 이 한 가지 특성으로 설명됩니다.
AI는 프레임워크(Framework), 패턴(Pattern), 그리고 무언가를 연결하는 표준적인 방식에 있어 저보다 진정으로 더 빠릅니다. 제가 읽은 것보다 더 많은 자료를 읽었기 때문입니다. 하지만 "저항이 가장 적은 경로"란 AI가 눈앞에 놓인 과제, 예를 들어 엔드포인트(Endpoint)를 작동시키거나 테스트를 통과시키는 것에 최적화됨을 의미합니다. AI는 모든 세부 사항을 알지 못하기 때문에, 귀하의 유스케이스(Use case)에 필요한 시스템의 형태를 위해 최적화할 수는 없습니다. 그 부분에 대한 책임은 여전히 100% 귀하에게 있습니다.
저항이 적은 경로가 잘못되는 두 가지 방식
방치될 경우, 저항이 가장 적은 경로는 두 가지 반대되는 방향으로 무너집니다. 당장 눈앞의 일을 작동시키기 위해 지름길을 택합니다. 즉, 경계(Boundary)를 무너뜨리거나, 값을 하드코딩(Hardcode)하거나, 나중에 두 구성 요소가 독립적으로 움직일 수 있게 해줄 이음새(Seam)를 건너뛰어 버립니다. 그리고 귀하가 이를 바로잡으려 하면, AI는 과잉 엔지니어링(Over-engineer)을 하여 귀하가 요청하지도 않았고 아직 필요하지도 않은 패턴, 레이어(Layer), 추상화(Abstraction)를 끌어다 씁니다.
이 두 가지 모두 동일한 지점에서 기인합니다. 즉, AI는 아키텍처를 조종(Steering)하는 것이 아니라 프롬프트(Prompt)를 해결하고 있는 것입니다.
다음은 제가 실제로 겪었던 사례입니다. 제 앱은 일반적인 방식대로 레이어(Layer)가 나뉘어 있습니다. API 레이어, 그 아래의 서비스 레이어(Service layer), 그리고 그 아래의 데이터 액세스 레이어(Data-access layer)로 구성되어 있으며, 각 레이어가 무엇을 할 수 있는지에 대한 명확한 규칙이 있습니다. 데이터베이스 트랜잭션(Database transaction)은 서비스 레이어에 속해야 합니다. 하지만 에이전트는 이를 계속 무시했습니다. 제가 서비스 레이어로 범위를 지정한 커밋(Commit)들이 데이터 레이어에서 발견되거나 API에서 발견되곤 했습니다. 가장 최악의 경우는 서비스 레이어에서 시작된 트랜잭션이 두 단계 아래 레이어에서 커밋되는 것이었습니다. 단순한 프롬프트에만 의존한다면, AI는 작동하는 결과물을 가져다주겠지만, 그 과정에서 조용히 경계를 허물고 취약한 시스템을 만들어 놓았음을 깨닫게 될 것입니다.
여기 며칠 전 있었던 반대의 사례가 있습니다. 저는 main 브랜치에서 분기된 별도의 브랜치에서 에이전트(agent)와 함께 버그 수정 작업을 하고 있었습니다. 테스트 도중, 버그와는 직접적인 관련이 없지만 해당 기능과 관련된 별도의 공백(gap)을 발견했고, 에이전트에게 그것도 함께 수정해 달라고 요청했습니다. 에이전트는 합리적으로 그 공백을 별도의 브랜치에 담았지만, 버그 수정 브랜치(bug-fix branch)가 아닌 main에서 브랜치를 생성했고 저는 그 사실을 놓쳤습니다. 두 브랜치 모두 fix/...로 명명되어 있었기에, 언뜻 보기에는 아무런 문제가 없어 보였습니다. 에이전트는 변경 사항을 적용했고, 제 워크플로우(workflow)에 따라 커밋(commit)하기 전에 테스트를 요청했습니다. 공백 문제는 잘 해결되었지만, 버그가 다시 나타났습니다. 제가 이 문제를 지적하자 에이전트는 동의했습니다. 이 브랜치가 main에서 분기되었기 때문에 당연히 예상되는 결과라고 말이죠.
그래서 저는 에이전트에게 두 브랜치를 화해(reconcile)시키라고 요청했습니다. 에이전트가 제시한 계획은 제 머리를 어지럽게 만들었습니다. 길을 잃은 브랜치(stray branch)의 작업을 커밋하고, 그것을 버그 수정 브랜치 위로 리베이스(rebase)한 다음, 타겟을 패스트 포워드(fast-forward)하고, 길을 잃은 브랜치를 삭제한 뒤, 전체 과정을 다시 검증하라는 것이었습니다. 하지만 아직 아무것도 커밋되지 않은 상태였습니다. 전체 수정 작업은 단 세 개의 명령어로 끝날 수 있었습니다: git stash를 실행하고, 버그 수정 브랜치로 전환한 뒤, git stash pop을 하고, 올바른 브랜치에서 커밋하는 것이었습니다. 왜 그냥 그렇게 하지 않느냐고 묻자, 에이전트는 즉시 동의했습니다: "맞습니다, 그것은 과하게 설계(over-engineered)되었습니다."
편법은 편법으로 남지 않는다
이 부분이 저를 놀라게 했습니다. 당시에는 타당한 이유가 있었을지라도, 한 번 저지른 편법(cut corner)은 국소적인 실수로 머물지 않습니다. 이후의 세션들이 계속해서 그 실수를 찾아내고 반복합니다. 에이전트는 코드의 현재 상태를 읽고, 지름길(shortcut)을 발견하면 그것을 따라야 할 패턴으로 취급합니다. 여기서는 머피의 법칙(Murphy's Law)이 완전히 작용하는 것 같습니다. 에이전트가 따를 수 있는 10개의 좋은 예시가 있더라도, 반드시 따라서는 안 될 단 하나의 예시를 찾아내고야 맙니다.
그 경계 문제(boundary problem)가 바로 그것이었습니다. 그것은 단 한 번의 실수로 끝나지 않았습니다. 에이전트는 매 세션마다, 그리고 제가 그것을 잡아내어 다시 돌려보내야 할 때마다 레이어링(layering)을 깨뜨리는 코드를 반복해서 생성했습니다. 초기에는 그냥 제가 직접 수정하곤 했습니다. 그것이 실수였습니다. 제가 직접 수정하는 것은 다음 세션에서 똑같은 일이 반복되는 것을 막는 데 아무런 도움이 되지 않았습니다. 그래서 저는 직접 수정하는 것을 멈추고, 에이전트가 스스로 수정하게 한 뒤 규칙을 기록하도록 만들었습니다. 그러면 다음 세션에서 그 규칙을 읽고 따르게 됩니다. 하지만 여전히 100% 완벽하게 방지할 수 있는 것은 아닙니다.
두 가지가 상황을 반전시켰습니다:
- 계획 검토(Plan reviews)와 컨벤션 체크(convention check)가 제 워크플로(workflow)의 고정된 부분이 되었습니다. 이것들이 정착되기 전까지는 제 주의력의 상당 부분이 실제 작업 대신 이러한 누수(leaks)를 잡아내는 데 소모되었습니다. 이 과정이 루프(loop) 안에 포함되자 누수는 대부분 멈췄고, 이미 발생한 누수들을 해결하기 위해 전용 클린업 마일스톤(cleanup milestone)을 실행했습니다.
- 격리(Isolation). 저는 완전하게 분리된 시스템 테스트 스택(system-test stack)과 수동 테스트를 위한 개발 스택(dev stack)을 유지하며, 이는 타협할 수 없는 원칙입니다. 병렬로 진행되는 모든 작업에는 워크트리(worktrees)를 사용하여 각 작업 라인이 자신만의 격리된 스택을 갖게 하고, 무엇도 서로 침범하지 않도록 합니다. 메인 컨텍스트(main context)에 그대로 남겨둔 탐색(Exploration)은 에이전트가 계속해서 복사하게 되는 탐색이 되므로, 메인 컨텍스트를 깨끗하게 유지하십시오.
새로운 작업에서 제가 정착한 리듬은 다음과 같습니다: 실험하고, 정리(clean up)한 다음, 다음으로 넘어가는 것입니다. 정리(cleanup)는 선택 사항이 아닙니다. 그것은 오늘의 일회성 작업/컨텍스트가 의도치 않게 당신의 컨벤션(conventions)의 일부가 되는 것을 막아주는 장치입니다.
당신은 비전을 가져오고, 에이전트는 프레임워크를 채웁니다
저에게 효과적인 분업 방식은 다음과 같습니다:
- 당신이 판단(judgment calls)과 방향을 소유해야 합니다. 모든 세부 사항을 아직 갖추지 못했더라도, 더 큰 비전을 먼저 제시하세요. AI는 세부 사항을 구체화하는 데 도움을 줄 수 있지만, 이는 오직 당신이 설정한 목적지가 있을 때만 가능합니다.
- AI는 프레임워크(frameworks)는 알고 있지만, 당신의 맥락(context)은 알지 못합니다. 추상적인 접근 방식이나 트레이드오프(trade-offs)에 대해서는 우리보다 더 빠르게 파악하고 있을 수 있습니다. 하지만 당신이 말해주기 전까지는 당신의 제약 조건(constraints), 사용자, 이력 등 그 무엇도 알지 못합니다.
- AI가 당신이 이해하지 못하는 선택지를 제시한다면, 설명을 요구하세요. 권위 있게 들린다는 이유만으로 특정 접근 방식을 무비판적으로 승인(rubber-stamp)하지 마세요. AI에게 옵션과 트레이드오프(trade-offs)를 단계별로 설명해 달라고 요청한 뒤, 결정은 당신이 내리십시오.
예시: 제가 만들고 있던 제품에 LLM을 통합해야 할 때, 저는 설정(configs)과 사양(specs)을 통해 구동될 수 있는 플러그인 기반 시스템(plugin-based system)으로 만들기로 결정했습니다. 저는 요구 사항과 목표에 대해 AI와 상당한 시간을 주고받으며 논의했습니다. 수많은 반복(iterations) 끝에, 제가 원하는 형태를 갖춘 이상적인 시스템 설계를 얻을 수 있었습니다. 그 후 우리는 지금 구축할 부분과 나중에 필요에 따라 추가할 부분에 대해 논의했습니다. 그러고 나서 저는 AI에게 하나의 레버(lever)를 추가해 달라고 요청했습니다. 어차피 모든 것이 단일 컨테이너(single container)에 배포되기 때문에, 매번 플러그인을 로드하는 대신 내장된 코드를 실행할 수 있도록 말입니다. 에이전트(agent)는 플러그인 메커니즘을 구축하는 데는 문제가 없었겠지만, 순수 플러그인 방식(pure-plugin path)이 운영 측면에서 매우 고통스러운 선택이 될 것이라는 경고는 해주지 않았을 것입니다.
AI는 그것을 구축할 수 있습니다. 하지만 결정은 여전히 당신의 몫입니다.
목적지와 제약 조건을 먼저, 그다음 단계별 계획
가장 큰 변화는 제가 시작하는 지점입니다. "이 부분을 먼저 만들고 나머지는 나중에 생각하자"가 아닙니다. 그렇게 하면 아키텍처(architected)가 막다른 길로 몰리게 됩니다.
당신이 가고자 하는 목적지와 당신을 제약하는 요소들(자원, 타이밍, 프로세스)에서 시작하세요. 그런 다음 그곳에 도달하기 위한 단계적 계획(phased plan)을 세우고, 계획이 발전함에 따라 이를 문서로 기록(write to disk)하세요. 계획이 종이 위에 존재하기 전까지 완벽해질 때까지 기다리지 마세요. 공개적으로 논의되며 구체화되는 계획은 머릿속에만 있는 완벽한 계획보다 훨씬 가치 있으며, 문서는 결정 사항들의 이력(history)을 보관해 줍니다. 나중에 무언가를 변경해야 할 때, 문서는 왜 그런 방식으로 결정되었는지를 알려줍니다. 이는 당신이 그때쯤이면 아마 잊어버렸을 내용일 것입니다.
성과를 거둔 단계적 계획: 앞서 언급한 동일한 예시로 돌아가 보겠습니다. AI 서비스는 처음에는 앱에 직접 임포트(import)되는 라이브러리(library) 형태로 시작되었습니다. 그러다 거의 동일한 프롬프트(prompt)를 사용하는 두 번째 앱이 필요해졌습니다. 빠른 길은 그것을 다시 라이브러리로 임포트하는 것이었습니다. 대신, 저는 해당 서비스가 HTTP를 통해 독립적으로 존재하여 어떤 앱이든 공유할 수 있도록 한다는 목표를 세우고, 그곳으로 가는 단계를 나누었습니다. 먼저 마이크로서비스(microservice)로 구축합니다. 그다음 새로운 앱을 HTTP를 통해 해당 서비스에 맞춰 구축합니다. 기존 앱은 플래그(flag) 뒤에서 라이브러리 경로를 유지하도록 합니다. 새로운 앱을 통해 HTTP 경로가 작동함을 먼저 증명한 다음, 기존 앱을 마이그레이션(migrate)합니다. 플래그 덕분에 새로운 구조가 검증되는 동안 아무것도 망가지지 않았습니다.
내가 유지하는 것
AI를 활용한 아키텍처 설계(Architecting with AI)는 판단력의 기준을 낮추지 않았습니다. 오히려 높였습니다. 그리고 올바르게 사용한다면 매우 강력한 힘을 실어줍니다. 저에게 에이전트(agent)는 저를 느리게 만들던 마찰(friction)을 제거해 주었지만, 이는 양날의 검이기도 합니다. 에이전트는 컨텍스트(context)(당신이 명시하지 않았거나 현재 윈도우(window)에서 벗어난 경우)를 가지고 있지 않기 때문에, 당신을 올바른 구조가 아닌 잘못된 구조로 빠르게 몰아넣을 가능성도 그만큼 높습니다. 방향성은 반드시 당신으로부터 나와야 하며, 초기에 명시되고, 기록되어야 하며, 강제되어야 합니다.
저는 계획의 어느 정도를 에이전트에게 넘기고 어느 정도를 제 머릿속에 남겨둘지 여전히 다듬는 중입니다. 하지만 자신의 생각을 정리하는 것은 엄청난 보상으로 돌아옵니다. 그것을 제대로 해낸다면, 에이전트 또한 당신을 따르게 만들 수 있습니다.
도구에 관한 참고 사항: 저는 의도적으로 도구에 구애받지 않도록(tool-agnostic) 작성하고 있습니다. 중요한 경우, 저는 "에이전트(the agent)"를 일반적인 의미로 사용합니다. 모델과 도구는 빠르게 변화하고 있으므로, 여기에 기술된 구체적인 내용은 하나의 스냅샷(snapshot)일 뿐, 확정된 결론은 아닙니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기