원문 캡처 · oioannou.com
원문 캡처 · oioannou.com

코딩은 이미 해결됐고 범용 인공지능(AGI)이 코앞에 왔다고들 한다. 그런데 나는 최신 모델에게 왜 정수가 숫자인지 또 확인하는지 묻고 있다.

이런 일이 왜 반복되는지 알아보려고 지난 두 달간의 OpenCode 대화와 Plannotator 피드백을 다시 살펴봤다. 프로젝트와 작업은 달랐지만, 나는 계속 같은 방향으로 요구했다. 이미 자리 잡은 방식을 따르고, 불필요한 변환은 없애고, 관련 없는 코드는 그대로 두며, 이 동작이 정말 필요한지 다시 생각하라는 것이었다.

이 대화는 오픈AI의 최신 모델인 Sol 5.6과 그다음 모델 Astra와 나눴다.

코드 작성은 해결됐을지 몰라도 버그는 그렇지 않다.

일상적인 업무에서 구현을 만들어내는 일은 더는 병목이 아니다. 변경 사항을 설명하고 에이전트와 논의하면 프로젝트의 관련 부분에 동작하는 코드를 작성할 수 있다. 여전히 어려운 점은 그 변경이 실제 문제를 해결하면서 불필요한 동작을 추가하거나 없던 버그를 만들지 않았는지 확인하는 일이다.

그래서 계속 코드를 단순하게 해달라고 한다.

조건이 하나 늘 때마다 따져봐야 할 경로가 하나 더 생긴다. 표현 방식이 하나 늘 때마다 같은 정보를 서로 다르게 나타낼 가능성이 생긴다. 관련 없는 리팩터링은 요청한 작업과 함께 이해하고 검증해야 할 동작을 더 많이 바꾼다.

이런 추가 사항을 만드는 비용은 이제 아주 작다. 하지만 그 비용은 현재 작업이 끝난 뒤에도 계속 남는다.

기존 코드베이스에서는 특히 중요하다. 시스템이 동작하는 방식에는 이미 패턴이 있고, 호출하는 쪽은 그 동작에 의존한다. 에이전트는 기존 패턴을 따를 수도 있고, 당장의 작업에 편리해 보인다는 이유로 같은 일을 처리하는 두 번째 방식을 도입할 수도 있다. 두 번째 방식은 따로 보면 잘 동작할 수 있지만, 버그가 생길 수 있는 영역을 하나 더 만든다.

에이전트가 작성하는 코드에 Effect 같은 프레임워크가 매력적인 이유는 알 것 같다. 오류와 의존성을 명시하고 동시성 및 리소스 관리를 위한 표준 도구를 제공하면 처음부터 새로 만들어야 할 요소가 줄어든다. 하지만 모든 코드베이스가 Effect를 쓰는 것도 아니고, 모두가 Effect로 옮겨갈 것도 아니다. 타입과 구조가 잘 잡힌 변경이라도 불필요할 수 있다.

오늘 잘못 설계한 함수는 다음 에이전트에게도 맥락이 된다. 다른 에이전트가 그 함수를 쓰기 시작한다. 나중에 또 다른 에이전트가 처음에 왜 그런 방식으로 동작했는지 모른 채 함수를 바꾼다. 임의로 내린 선택이 이후 작업의 제약이 되고, 실제 요구사항을 반영한 부분과 앞선 모델이 해석해 넣은 부분을 구분해야 하는 상황이 생긴다.

다음 에이전트도 테스트를 통과시키며 동작하게 만들 것이다. 하지만 어떤 방식으로 동작하게 할까? 우리가 원하는 방식일까, 아니면 물려받은 코드에서 우리가 원한다고 추론한 방식일까?

모델은 스스로 복잡성을 필수로 만들 수 있다

가장 답답한 패턴은 요구사항도 아니었던 설계 선택에서 시작한다.

에이전트가 그런 선택을 하고 그 결과에 부딪히면, 이를 처리할 코드를 더 작성한다. 내가 결과를 볼 때쯤이면 추가된 코드마다 그럴듯한 이유가 붙어 있다. 앞선 결정을 했으니 추가 처리가 필요하고, 추가 처리가 있으니 테스트가 더 필요하며, 새 동작이 생겼으니 다른 경우도 고려해야 한다는 식이다.

데이터베이스에서 읽은 정수를 예로 들어보자. 열의 자료형은 정수이고 API 계약에는 숫자라고 적혀 있으며 Kysely의 쿼리 타입도 이에 동의한다. 그런데도 값이 내부 코드를 통과할 때마다 에이전트가 숫자인지 반복해서 확인하는 모습을 본다. 값이 어디서 왔고 어떤 값일 수 있는지 이미 확인했다. 왜 사용할 때마다 그 보장을 다시 모르는 것처럼 다룰까?

그 확인 자체는 완전히 올바를 수 있다. 예외를 던지지 않을 수도 있고 의미 있는 성능 영향도 없을 수 있다. 하지만 값이 타입을 보장하는 경로를 이미 거쳤다면, 이 확인으로 무엇을 얻는가? 데이터베이스 제약을 자주 우회해 잘못된 값을 직접 만들어 넣는다고 가정하는 셈이다.

잠금과 동시성 처리에서도 같은 일이 보인다. 어떤 작업이 실제로 동시에 실행될 수 있는지 확인하기도 전에 에이전트는 작업을 조율하기 시작한다. 그러면 잠금을 얻고 해제하는 규칙, 실패에서 복구하는 방법, 그 규칙을 테스트하는 방법까지 필요해진다. 실행 모델상 그런 조율이 필요하지 않다면, 실제로 없는 문제를 막으려다 실패할 방법을 새로 만든 것이다.

각 확인이나 잠금 방식을 개선해달라고 요청할 수도 있다. 하지만 더 유용한 질문은 그 작업에 그런 기능이 애초에 필요한지다.

우리에게는 이미 이에 관한 원칙이 있었다. 성급한 최적화를 피하라는 것이다. 직접 코드를 작성하던 때는 각 아이디어를 구현하는 데 드는 노력 자체가 추측성 작업을 얼마나 더할 수 있는지 제한했다. 에이전트는 최적화와 방어 분기, 이를 뒷받침하는 테스트까지 필요성을 확인하기 전에 만들어낼 수 있다.

모든 것이 잘 동작하더라도 검토할 때 인지 부담이 늘고, 다음 에이전트가 참고할 맥락이 늘며, PR과 CI에 테스트가 더해진다. 이전보다 훨씬 빠르게 코드를 만들게 된 지금은 그 추가 사항이 정당한지 판단하는 데 더 많은 노력이 들어간다.

단순화 요청에 여러 번 손이 가는 이유도 여기에 있다. 에이전트는 내가 지적한 부분만 제거하고, 그 부분을 만들어낸 사고방식은 그대로 둔다. 그러면 또 다른 사례를 발견하고 같은 문제를 다시 설명해야 한다.

불필요한 요구사항을 훌륭하게 구현해도 결국 불필요한 요구사항을 계속 떠안게 된다.

검사 통과만으로는 충분하지 않다

타입 검사, 린트, 테스트, 스크린샷은 이 작업 흐름에 꼭 필요하다. 에이전트가 나를 거치지 않고도 많은 문제를 찾아 고칠 수 있게 해준다. 하지만 모든 검사가 통과했다고 해서 새 코드가 전부 변경 사항에 포함돼야 한다는 뜻은 아니다.

타입 검사는 우리가 정한 타입을 검사한다. 테스트는 우리가 지정한 동작을 검사한다. 에이전트가 불필요한 제약을 도입한 다음 그에 맞춰 코드와 테스트를 작성했다면, 검사 결과가 모두 일치할 수 있다.

적대적 테스트도 이 문제를 더 키울 수 있다. 데이터베이스에서 온 정수를 받는 내부 함수에 문자열을 넣으면 방어 검사가 이를 처리한다는 테스트를 통과할 수 있다. 하지만 실제 데이터 경로에 그런 검사가 필요한지 확인하지 않은 채, 만들어낸 오류 경로가 작동한다는 점만 입증한 셈이다.

스크린샷은 에이전트가 UI의 시각적 일관성을 확인하는 데 도움이 된다. 그래도 잘못된 정보를 일관되게 표시하거나 사용자를 잘못된 흐름으로 안내할 수 있다. 보기 좋다는 점도 유용한 확인이지만, 우리가 올바른 것을 만들었는지는 답해주지 않는다.

그래서 테스트가 실제로 무엇을 증명하는지 계속 묻는다. 사용자에게 중요한 동작을 보호하는가? 의미 있는 회귀를 잡아내는가? 아니면 구현을 재구성했을 때 주로 문제를 제기하는가?

한 에이전트는 이런 질문을 모의 객체를 많이 쓰는 테스트를 선호하지 않는다는 뜻으로 받아들였다. 문제는 테스트가 의미가 있느냐는 것이었다고 바로잡아야 했다. 그 판단을 모의 객체에 관한 규칙으로 바꾸는 것은 요점을 놓친 것이다.

복잡성도 마찬가지다. “도우미 함수를 줄이라”는 말로 어떤 책임을 함께 묶어야 하는지 판단할 수는 없다. “테스트를 늘리라”는 말로 어떤 동작이 올바른지 결정할 수도 없다. 이런 지름길은 지침 파일에 쉽게 적을 수 있고, 겉으로만 따르기도 쉽다.

이미 프롬프트에 넣었다

나는 스킬과 프로젝트 지침, AGENTS.md를 사용한다. 이를 최신 상태로 유지하고, 새 모델로 옮길 때 다시 살펴보며, 에이전트의 행동이 지침에 맞는지도 확인한다. 이 설정을 관리하는 일도 이미 업무의 일부다.

테스트의 가치를 따지는 질문은 테스트 스킬에 넣어둬야 한다. 하지만 에이전트가 항상 그 지침을 따르는 것은 아니며, 테스트가 지침을 반영하는지도 여전히 확인해야 한다.

이 글을 쓰는 동안에도 기존 함수를 재사용하거나 개선하지 않고 도우미 함수를 추가한 이유를 에이전트에게 다시 물었다. 에이전트의 답은 이랬다.

추가 도우미 함수는 제가 판단해서 넣었습니다. 저장소 지침은 오히려 재사용을 권장하고 있습니다.

그래서 “스킬이 부족하다”거나 “AGENTS.md를 고쳐라”는 말만으로 설명이 충분하다고 보기 어렵다. 지침은 이미 있었고, 에이전트도 지침을 찾아낸 뒤 자신의 구현이 다른 방향으로 갔다는 점을 인정할 수 있었다.

원칙을 알 수 있게 하는 것과 원칙을 잘 적용하는 것은 다르다. 에이전트는 불필요한 복잡성이 왜 좋지 않은지 정확히 설명하면서도 다음 변경에서 복잡성을 더할 수 있다. 어떤 요구사항이 범위 밖이라는 데 동의하고도 나중에는 그 요구사항에 의존하는 무언가를 만들 수 있다.

더 정확한 지침은 미리 막고 싶은 실수를 이미 알고 있을 때 도움이 된다. 하지만 그 실수는 코드를 읽다가 발견하는 경우가 많다. 그때 문제를 찾아내고 바로잡을 프롬프트를 쓰는 일이 엔지니어링 작업이다. 그 결과를 더 나은 프롬프트 작성이라고 부르는 것만으로는 그런 판단을 어떻게 생략할 수 있었는지 설명되지 않는다.

나는 에이전트가 일상적인 구현 결정을 독립적으로 내리길 바란다. 모든 분기와 경계를 미리 지정해야 한다면 구현 설계의 상당 부분을 직접 하는 셈이다. 유용한 위임이란 에이전트가 그 작업을 하도록 맡기고, 맞지 않는 결정을 따져볼 수 있게 하는 것이다.

긴 세션에서는 이미 합의한 결정이 사라지기도 한다. 명령줄 옵션을 제거해달라고 요청했고 초기 압축 요약에는 그 결정이 남아 있었다. 압축을 여섯 번 거친 뒤에는 그 옵션이 다시 생겼고, 이를 요구하는 테스트까지 붙었다. 그사이에 더 넓은 계획을 승인하기도 했으므로, 이 회귀를 압축만으로 설명할 수는 없다. 하지만 요약에서 명시적인 제외 사항이 사라졌고, 결국 거부한 동작을 의도된 것으로 설명하게 됐다. 앞서 합의했다고 해서 최종 결과까지 보장되지는 않았다.

유용한 맥락을 제공하는 작업 흐름을 갖추고 최신 모델을 써도 계속 마주치는 간극이 바로 이것이다.

프로덕션 코드를 계속 읽는 이유

Plannotator는 이 작업 흐름을 유난히 매끄럽게 해준다. 계획이나 diff, 설명에서 문제가 되는 부분에 주석을 달고 왜 필요한지 물은 뒤 더 단순한 방법을 논의할 수 있다. 관련 맥락과 함께 피드백이 에이전트에게 돌아간다. 긴 프롬프트로 코드 위치를 설명하는 대신, 변경 사항을 함께 따져보고 다시 작업해달라고 요청하기 쉽다.

이런 대화는 마지막 승인 버튼이 아니라 반복되는 과정이므로 중요하다. 조사하고 구현하는 속도가 훨씬 빨라진 점은 매우 만족스럽다. 결과에 이의를 제기할 좋은 방법이 있어야 그 속도를 유용하게 활용할 수 있다.

Opus 4.5 때부터 이 문제를 고민해왔다. 더 새로운 모델은 이런 필요를 크게 줄여주길 바랐다. 분명 개선은 있었지만, 중요한 프로덕션용 코드를 더는 읽지 않아도 된다고 확신할 만큼은 아니다.

정말 코드를 그만 읽고 싶다. 이 문제가 처음부터 끝까지 해결되길 바란다. 하지만 아직 해결됐다고 볼 수는 없다.

개인 프로젝트나 재미로 하는 실험은 다른 선택의 문제다. 결과의 중요성이 작다면 확인에 시간을 덜 쓰기로 할 수도 있다. 하지만 중요한 프로덕션 코드도 사람이 읽을 필요가 없다고 말한다면, 지금 도구들이 무엇을 확인할 수 있는지 스스로 속이는 것이라고 생각한다.

이 글을 쓸 때 AI는 OpenCode 대화와 Plannotator 피드백을 분석하고, 글을 초안 작성하고 수정하는 데 도움을 줬다. 경험과 결정, 의견은 내 것이다.