제 LinkedIn: https://www.linkedin.com/in/kristiyan-stoyanov-395310184/
비디오 형식을 선호하신다면:
AI는 점점 더 많은 양의 코드를 생성하고 있습니다. 하지만 엔지니어가 구현을 훨씬 빠르게 할 수 있음에도 불구하고, 기능이 고객에게 도달하는 데 걸리는 시간은 여전히 똑같다면, 우리는 실제로 무엇을 개선한 것일까요? 이것이 저에게는 백만 달러짜리 질문입니다: 생산성 향상은 어디로 가고 있을까요?
코드는 완성되었지만 엔지니어가 여전히 수동으로 확인하고 있는 경우도 있습니다. 그러면 merge request가 검토를 기다립니다. 병합(merge)된 후에는 더 많은 유효성 검사, 더 많은 대기 시간이 있고, 결국 출시가 이루어집니다. 우리는 소프트웨어 개발 생명주기(SDLC)의 한 부분을 가속화했지만, 변경 사항은 여전히 나머지 과정을 거쳐야 합니다.
제 주장은 코딩 속도가 그 주변으로 업무를 재설계할 때 더 빠른 전달로 이어진다는 것입니다. 우리는 어떤 검사를 수행하는지, 리뷰어에게 내리도록 요청하는 결정들이 무엇인지, 엔지니어들이 그러한 검사에 대해 갖는 확신은 어떠한지, 그리고 조직으로서 무엇을 측정하는지를 살펴봐야 합니다. 저는 리드 엔지니어와 엔지니어링 리더가 여기서 상당한 변화를 만들 수 있다고 생각합니다.
소프트웨어 전달을 위한 선언문 (A manifesto for software delivery)
저는 그 주장을 여섯 가지 약속으로 정리했습니다. 이것들은 제가 엔지니어링 조직의 관행을 어떻게 변경할지 결정할 때 사용할 원칙들입니다:
- 전달(Delivery)이 결과물이다. 유용한 변화는 사람들에게 도달해야 한다. 코드를 더 많이 생성하는 것은, 그것을 프로덕션에 배포하고 작동 상태를 유지할 수 없다면 거의 의미가 없다.
- 신뢰에는 증거가 필요하다. 우리의 검증 과정은 의도했던 동작과 그 변경 사항이 상호작용할 실제 시스템에 연결되어야 한다.
- 자동화는 업무 방식을 바꿔야 한다. 자동화가 대체하는 노력이 무엇인지 합의해야 한다. 만약 모두가 여전히 같은 수동 단계를 반복한다면, 우리는 더 많은 작업을 해야 한다.
- 신뢰는 함께 쌓는다. 엔지니어들은 의존할 수 있는 피드백과 그에 대한 팀과의 합의가 필요하다. 이는 더 나은 도구뿐만 아니라 팀과의 대화를 통해서도 이루어진다.
- 검토는 결과에 따른다. 이 특정 변경 사항이 무엇을 유발할 수 있는지, 우리가 얼마나 빨리 알아차릴 수 있을지, 그리고 그 영향을 되돌릴 수 있는지에 따라 통제 장치를 선택해야 한다.
- 학습은 변화보다 오래간다. 발견한 것들을 다음 작업으로 가져가라: 더 나은 테스트 환경(fixtures), 더 나은 맥락, 더 나은 검증 과정, 그리고 프로세스 자체에 대한 더 나은 결정들로 말이다.
이 유용한 논의는 이러한 약속들이 실제 팀의 하루 일과에서 무엇을 변화시키는지에 달려 있다. 그러니 생성된 코드가 우리를 어디에 남겨두었는지부터 시작해 보자: 에이전트는 작업이 완료되었다고 말한다. 우리가 그 변경 사항을 검토 요청으로 보내기에 충분히 자신감을 갖게 하려면 무엇이 필요할까?
선언문(manifesto): https://krisitown.github.io/agentic-software-delivery-manifesto/
이야기하기 (Story Time)
나는 여러 다운스트림 서비스로의 호출을 조정하는 애플리케이션에 변경 사항을 구현하는 에이전트를 가지고 있었다. 내가 명세(specification)를 작성했고, 에이전트가 로컬 통합 테스트를 구축하게 했으며, 그 결과를 검토했다. 테스트는 통과했다.
문제는 이것이었다: 에이전트는 다운스트림 API 계약에 대해 가정을 한 것이다. 예상했던 것보다 JSON 응답에서 한 레벨 더 깊은 곳에 필드가 나타나는 것을 상상해 보라. 믿을 만한 차이점이다. 내가 검토하는 과정에서도 놓치기 쉬울 만큼 충분히 그럴듯했다.
구현(implementation)과 테스트 픽스처(test fixtures)는 서로 일치했습니다. 불행하게도, 그들이 같은 잘못된 가정(wrong assumption)을 공유했기 때문에 합의가 가능했던 것입니다.
이것이야말로 AI와 생산성에 대해 생각할 때 제가 계속 돌아오는 경험입니다. 코드를 생성하는 것은 쉬워졌습니다. 하지만 우리가 그것을 전달할 수 있다는 것을 확립하는 것은 여전히 노력이 필요했고, 그 노력 중 일부는 너무 늦게 일어나고 있었습니다.
저는 여기에서 상당한 기회가 있다고 생각합니다. 하지만 이를 실현하려면 소프트웨어를 개발하고, 검토(review), 유효성 검사(validate), 그리고 출시(release)하는 방식 자체에 변화가 필요합니다. 모두에게 에이전트(agent)를 구매해 주고 나머지 프로세스를 그대로 두는 것은 그 기회의 상당 부분을 대기열 속에 방치하게 만듭니다.
통과하는 테스트가 실제로 우리에게 알려주는 것은 무엇일까요?
제 개인적인 경험으로는, 스펙 기반 개발(spec-driven development)이 유용하다는 것을 알게 되었습니다. 저는 에이전트와 함께 비즈니스 요구사항을 명세서(specification)로 변환하는 작업을 하고, 그 명세서를 검토하는 데 시간을 보냅니다. 이것은 제가 매우 신중하게 접근하고 싶은 부분 중 하나입니다. 만약 우리가 필요한 동작(behavior)을 오해한다면, 더 빠른 구현 속도는 우리에게 오해를 더 빨리 구축하도록 도울 것입니다.
명세서로부터 에이전트는 개별 메서드보다 높은 수준에서 동작을 테스트하는 테스트를 작성합니다. 저는 이것들을 통합 테스트(integration tests)라고 부르는 경향이 있습니다. 이들은 로컬에서 실행되며, 데이터베이스와 같은 종속성에는 Testcontainers를, 다운스트림 서비스의 제어된 응답에는 WireMock을 사용합니다. 에이전트는 변경 사항을 구현하는 동안 이를 반복적으로 실행할 수 있습니다.
이것이 저의 짧은 피드백 루프(feedback loop)입니다. 이것은 에이전트에게 작업할 구체적인 대상을 제공하고, 제가 검토를 위해 변경 사항을 보내기 전에 확인할 수 있는 증거를 저에게 제공합니다.
로컬 루프는 의도된 동작, 구현, 그리고 반복 가능한 검사를 연결합니다.
인간의 작업에는 중요한 시나리오와 그 예상 결과를 검토하는 것이 포함됩니다. 또한 구현 전에 테스트가 누락된 동작에 대해 실패한다는 것을 확립하고 싶습니다. 그렇지 않으면, 기능을 요구하지 않아도 통과하는 테스트를 작성했을 수 있습니다. 만약 에이전트가 구현 과정에서 기대치를 변경한다면, 그것 역시 주의 깊게 살펴봐야 합니다. 좋은 이유가 있을 수도 있지만, 단순히 녹색(green)을 얻기 위해 기대치를 약화시키는 것은 진전이라고 할 수 없습니다.
이 모든 것을 수행하더라도, '녹색'에는 한계가 있습니다. 이는 구현이 해당 테스트에 인코딩된 예상치를 만족시킨다는 것만을 알려줄 뿐입니다. 이 예상치들은 여전히 불완전하거나 잘못될 수 있으며, 이는 제가 제시한 API 예시에서도 마찬가지였습니다.
이 실패는 특별히 신비로운 것이 아닙니다. 서비스와 통합하는 엔지니어라면 일반적으로 해당 문서(documentation)를 검토하고, Postman이나 Insomnia 같은 도구를 통해 요청을 보내고 응답을 살펴볼 것입니다. 만약 우리가 에이전트에게 그러한 맥락 없이 동일한 통합 구현을 요청한다면, 우리는 스스로 했을 일부 작업을 생략하게 된 것입니다.
제가 제공하기 좋아하는 실용적인 방법 중 하나는 통합 환경에서 서비스에 대한 작동하는 curl 요청입니다. 에이전트는 실제 응답을 검사하고 이를 사용하여 현실적인 더미 데이터(fixtures)를 구축할 수 있습니다. 적절한 경우, 읽기 전용 개발 데이터베이스 접근은 비슷한 이유로 도움이 됩니다. 이는 추측하는 대신 스키마와 데이터의 형태를 볼 수 있게 해줍니다.
실제 예시는 시작점을 개선합니다. 여전히 실제 종속성(dependencies)을 사용하여 애플리케이션을 실행함으로써 검증되어야 합니다.
더 긴 루프가 짧은 루프를 개선해야 한다
변경 사항이 병합되고 통합 또는 개발 환경에 배포된 후, 저는 또 다른 피드백 루프를 원합니다. 이는 관련된 실제 애플리케이션을 사용하여 중요한 동작을 실행하는 엔드투엔드(end-to-end) 검사입니다.
이 지점에서 가정된 API 계약의 불일치가 드러날 수 있습니다. WireMock은 우리가 반환하라고 지시한 것을 반환합니다. 다운스트림 서비스는 실제로 반환하는 것을 반환합니다. 이들은 서로 다른 증거 출처이며, 그 차이가 중요합니다.
설득력 있는 목(mock)은 구현과 일치하면서도 실제 API와는 여전히 불일치할 수 있습니다.
이 루프가 문제를 발견하면, 구현을 수정하는 것은 작업의 일부에 불과합니다. 로컬 픽스처(fixtures)도 함께 수정해야 합니다. 이 발견 사항을 테스트로 되돌려 보내고 다음 엔지니어 또는 에이전트가 사용할 컨텍스트를 제공해야 합니다. 그렇지 않으면 우리는 같은 교훈에 대해 또다시 비용을 지불하게 됩니다.
여기에 관련 성능 및 기타 비기능적(nonfunctional) 검사도 포함하는 것이 좋습니다. 적절한 스위트는 애플리케이션에 따라 다르지만, 그 결과는 우리가 조치할 수 있을 만큼 충분히 알려줘야 합니다. 무엇이 실패했는지, 어떤 조건에서 실패했는지, 그리고 우리가 무엇을 관찰했는지 말입니다. 이 정보는 누군가 수동으로 장애를 재구성하도록 내버려 두기보다는 다음 변경 사항에 다시 투입될 수 있습니다.
루프가 길어질수록 비용이 더 많이 듭니다. 이것이 바로 제가 그것이 더 짧은 루프를 개선하기를 바라는 이유입니다. 시간이 지남에 따라, 실제 시스템에 대해 우리가 배우는 것 중 더 많은 부분이 개발 초기에 이용 가능해야 합니다. 또한 오랫동안 조용했기 때문에 유용한 검사를 제거하는 것에 대해서도 주의해야 합니다. 먼저 그것이 어떤 실패로부터 보호하고 있는지 이해해야 합니다.
엔지니어는 왜 여전히 모든 것을 수동으로 확인하는가?
만약 당신이 이러한 테스트를 가지고 있음에도 불구하고, 엔지니어가 병합 요청(merge request)을 열기 전에 여전히 직접 기능을 실행한다고 가정해 봅시다. 이것을 신뢰 문제라고 부르며 사람들에게 습관을 바꾸도록 요구하는 것이 유혹적일 수 있습니다.
저는 먼저 그들이 무엇을 알아내려고 하는지 물어볼 것입니다.
어쩌면 테스트가 '해피 패스(happy path)'만 다루고 걱정하는 시나리오를 놓쳤을 수도 있습니다. 어쩌면 피처(fixture) 자체가 이전에 잘못되었을 수도 있고 말입니다. 어쩌면 유용한 엔드투엔드 테스트(end-to-end test)가 병합(merge) 후에 실행되는데, 엔지니어는 병합 전에 결정을 내려야 하는 상황일 수 있습니다. 나중에 도착하는 결과로는 지금 가지고 있는 질문에 답할 수 없습니다.
수동 검토(Manual checking)는 누락되었거나 늦게 도착한 증거에 대한 완벽하게 합리적인 반응일 수 있습니다. 이를 이해하면 우리가 개선해야 할 구체적인 지점을 얻습니다: 테스트를 개선하거나, 피드백을 더 일찍 제공하거나, 혹은 어떤 문제를 나중에 발견해도 안전하다고 동의하는 것입니다.
심리적인 측면도 존재합니다. 자신이 작성하지 않은 코드에 대해 덜 편안함을 느낄 수 있습니다. 팀 프로세스를 따랐더라도, 자신의 변경 사항에서 결함이 새어 나오는 것이 개인적인 창피함으로 다가올 수 있다는 것을 배웠을 수도 있습니다. 이런 상황에서는 재차 검사하는 것이 이해할 만합니다.
리드(lead)나 매니저로서, 단순히 자동화가 신뢰할 만하다고 발표해서는 이 문제를 해결할 수 없습니다. 팀은 의존할 수 있는 피드백과, 언제 그것에 의존하는 것이 허용되는지에 대한 공유된 합의가 필요합니다. 무언가가 그 합의된 프로세스를 벗어날 때, 대응에는 프로세스 개선이 포함되어야 합니다. 그렇지 않으면 사람들은 조용히 개인적인 안전 루틴을 유지할 것입니다.
이것이 제가 자동화는 그것이 변화시키는 작업에 대한 대화와 함께 와야 한다고 생각하는 이유입니다. 만약 우리가 자동화된 검사를 추가하고 모든 수동 단계를 무기한으로 유지한다면, 커버리지를 개선할 수는 있겠지만, 배포 노력(delivery effort)을 줄였다고 가정해서는 안 됩니다.
70개 파일짜리 병합 요청이 여전히 기다리고 있다
제가 본 또 다른 패턴은 거대한 AI 생성 병합 요청입니다: 70개가 넘는 파일에, 사람이 검토할 준비가 되어 있습니다. 만약 구현 속도가 빨라졌는데도 필수적인 검토가 여전히 소수의 사람들에게 의존한다면, 대기열이 커질 명백한 지점이 존재합니다.
더 빠른 구현은 같은 검토자에게 더 많은 작업을 남겨둘 수 있습니다.
작은 변경 사항은 목적과 결과를 이해하기 쉽게 만듭니다. 하지만 큰 diff를 여러 임의 조각으로 나누는 것이 필요한 총 사고량을 자동으로 줄여주지는 않습니다. 우리는 검토자들이 무엇을 하도록 요청받고 있는지 살펴봐야 합니다.
저는 결정론적(deterministic) 확인 과정에서 기계적인 규칙들을 원합니다. 포맷팅, 린팅(linting), 그리고 우리가 신뢰성 있게 표현할 수 있는 규칙들은 검토 과정에서 반복적으로 사람의 주의를 소모해서는 안 됩니다. 자동화된 검토자들 또한 우려 사항을 조사하고 의심스러운 행동에 주의를 환기시킬 수 있습니다. 인간은 무언가를 놓치고; 에이전트(agent)도 무언가를 놓칩니다. 저는 리뷰 도구를 그것이 포착하는 유용한 실패 사례와 지원하는 결정들을 기준으로 판단할 것입니다.