AI 시대의 개발 능력은 검증력으로 결정된다, Flava API Gateway 개발 중 배운 빠른 검증과 로컬 환경 구성 전략

2 hours ago 3

에이전트가 코드를 작성할 때 마주하는 문제

코딩 에이전트의 반복 속도는 매우 빠릅니다. 이런 에이전트의 속도에 비해 개발 사이클에는 검증하기 위해 CI를 오가는 작업이나 반나절은 걸리는 환경 프로비저닝 같은 느린 단계가 있으며, 이런 단계들이 병목이 됩니다.

또한 에이전트와의 경험에는 기복이 있습니다. 에이전트는 훌륭하게 잘 작동할 때도 있지만 잘못된 결과를 내놓기도 합니다. 저희 역시 컴파일되지 않는 코드를 작성하거나, 존재하지 않는 API를 참조하거나, 아무도 명확히 정해두지 않은 탓에 구현 도중에 임의로 설계 관련 결정을 내리기도 했습니다.

이는 저희만의 문제가 아니었습니다. 동일한 프롬프트도 실행할 때마다 다른 출력이 나올 수 있습니다[1]. 에이전트의 출력은 불안정해서 그 역량을 일반화해 주장하기 어렵습니다[2]. 같은 도구도 개발자마다 사용 경험이 달라집니다[3]. AI는 코딩 속도를 높여주지만[4] 동시에 개발자가 제대로 리뷰할 수 있는 속도보다 더 빠르게 코드를 생성하기도 합니다[5]. 심지어 에이전트의 출력이 좋다는 것을 어떻게 측정할 것인지조차 아직 결과가 열려 있는 연구 주제입니다[6], [7].

그렇다면 AI가 코드에 더 많이 기여하는 상황에서 소프트웨어의 신뢰를 어떻게 유지할 수 있을까요? 저희는 그동안 신뢰할 수 있는 소프트웨어를 만드는 데 늘 중요했던 요소들이 앞으로 더 중요해질 것이라고 생각합니다. 무엇보다 개발자의 전문성 및 세심한 주의가 중요합니다. 여기에 테스트와 린터(linter), 빠른 로컬 환경, 사전 설계 등이 뒷받침돼야 합니다. 이러한 요소가 없다면 에이전트는 더 일찍 드러났어야 할 문제 앞에서 쉽게 멈춰 설 것입니다.

이 글에서는 Flava API Gateway를 개발하면서 이러한 문제를 해결하기 위해 적용한 실무 관점의 방법을 공유하겠습니다.

Flava API Gateway 소개

LINE Corporation과 Yahoo Japan Corporation이 합병한 후 저희는 LY Corporation에서 내부 프라이빗 클라우드인 Flava를 구축해 왔습니다. Flava API Gateway는 Flava 생태계를 구성하는 제품 중 하나로, 각 팀은 Flava API Gateway를 이용해 웹 API를 생성 및 배포하고 모니터링할 수 있습니다.

아래는 대략적인 아키텍처를 표현한 그림입니다. 그림 속 Kong[8]은 데이터 플레인 역할을 담당하며, 저희 컨트롤 플레인은 각 팀이 독립적으로 API를 구성하고 배포할 수 있는 다중 테넌트 RESTful API를 제공합니다.

Flava API Gateway architecture

Flava API Gateway 팀은 2025년 중반에 구성됐습니다. 당시 사내에는 에이전트 기반 코딩이 빠르게 확산되고 있었는데요. 저희는 팀이 AI의 이점을 누리되 앞서 언급한 함정에 빠지지 않기 위한 방법을 고민했으며, 그 결과 다음 세 가지를 실천해야 한다는 결론에 도달했습니다.

  • 에이전트가 코드를 작성하기 전에 방향을 맞추기 위한 ‘스펙 주도 개발’
  • 에이전트가 자신의 실수를 점진적으로 발견하고 수정할 수 있게 만드는 ‘검증 자동화’
  • 에이전트가 CI를 기다리지 않고 전체 검증 루프를 실행할 수 있게 만드는 ‘빠르고 독립적인 로컬 환경 구축’

각 항목을 하나씩 살펴보겠습니다.

스펙 주도 개발: 코드 첫 줄 작성 전 방향 맞추기

앞서 말했듯 에이전트 기반 코딩의 주요 문제는 출력 품질이 일관적이지 않다는 것입니다. 저희 역시 초기에 설계가 확정되기 전에 에이전트로 구현을 시작해 에이전트가 자체적으로 설계 관련 결정을 내리면서 비결정성(non-determinism)을 더 키우는 문제를 경험했습니다.

이때 필요한 것이 스펙 주도 개발(spec-driven development)입니다. 스펙 주도 개발은 변경하려는 사항의 설계와 접근 방식, 작업 항목을 명시적으로 정리해서 모호함을 줄이고 출력의 예측 가능성을 높입니다.

컨트롤 플레인을 개발하는 과정에서 이 프로세스가 어떻게 작동했는지 조금 더 자세히 설명하겠습니다. 저희는 코드를 작성하기 전에 OpenAPI 스펙을 먼저 작성해서 전체 설계를 사전에 확정했습니다. 그 다음 이를 더 작은 기능 단위로 나누고, OpenSpec[9]을 사용해 각 기능을 구현했습니다(OpenSpec은 에이전트와 통합해 스펙 주도 개발을 돕는 도구입니다).

OpenAPI로 스펙 정의 후 Nickel로 관리 효율화

OpenAPI[10]는 REST API를 기술하기 위한 업계 표준입니다. 저희는 OpenAPI를 사용해 컨트롤 플레인을 정의했으며, 이렇게 정의한 스펙은 에이전트가 따라야 할 기준이자, 생성된 결과가 설계에서 벗어났을 때 이를 잡아낼 수 있는 구체적인 기준이 되었습니다.

이와 같은 스펙이 실제 구현과 동기화되지 않으면 더 이상 에이전트를 붙잡아 둘 기준으로 작용하지 못하는데요. 원본(raw) OpenAPI YAML은 장황하고 반복적이며 수작업으로 유지보수하기에는 부담이 크며 이는 현장에서 스펙이 뒤처지게 만드는 원인이 됩니다[11].

이에 저희는 OpenAPI YAML을 직접 편집하는 대신 데이터 검증과 보일러플레이트를 줄이는 데 강점이 있는 구성 언어 Nickel[12]을 사용했습니다. 이를 이용해 API 리소스를 간결하게 설명하는 선언적 설명을 전체 CRUD 엔드포인트 스펙으로 변환하는 코드를 작성했습니다. 예를 들어 다음 스니펫은 path 리소스를 설명합니다.

resources.path = { description = "URL routes exposed by API Gateway.", parent = "API", editable = 'none, # 업데이트 엔드포인트 없음. path는 생성과 삭제만 하고 업데이트하지 않음 timestamped = true, # created_at과 updated_at 자동 생성 properties = { id = { schema = lib.uuid_for "path", sortable = true }, path = { schema = lib.ref "#/components/schemas/PathTemplate", create = 'required, # POST 요청의 본문에 필수 sortable = true, # list 쿼리에서 ?sort=path 활성화 filterable = true, # ?path=<value>를 통한 완전 일치 필터 활성화 filter_contains = true, # ?path.contains=<value>를 통한 부분 일치 필터 활성화 }, }, }

이를 기반으로 CRUD 생성기는 listPaths, createPath, getPath, deletePath라는 전체 엔드포인트 세트를 생성합니다. 각 엔드포인트는 페이지네이션, 정렬 및 필터 파라미터, 낙관적 잠금(optimistic lock)을 위한 ETag 헤더, 일관된 오류 응답 세트를 포함합니다.

다음은 생성된 스펙의 일부를 가져온 것입니다.

/v1/projects/{project}/apis/{api_id}/paths: get: summary: List paths operationId: listPaths parameters: - $ref: '#/components/parameters/Page' - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/SortPaths' - $ref: '#/components/parameters/Order' - name: path in: query description: Filter by exact path - name: path.contains in: query description: Filter by path (substring match) responses: '200': description: Paths listed '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' # ...

OpenSpec을 이용한 에이전트 제어 및 협업 작업 흐름

OpenSpec은 에이전트가 코드를 작성하기 전에 행동 계약에 합의하도록 각 기능 변경 사항을 구조화합니다. 이를 위해 다음 네 가지 산출물을 사용합니다.

  • 제안(proposal): 왜, 무엇을 바꾸는가
  • 설계(design): 기술 관점의 결정과 트레이드오프
  • 델타 스펙(delta specs): 각 변경별 행동 요구사항으로, Given-When-Then[13] 시나리오로 작성
  • 작업 목록(task list): 구현 단계를 체크리스트로 나눈 것

워크플로는 다음 네 단계로 진행됩니다.

OpenSpec workflow

  1. 개발자는 에이전트와 함께 기능을 검토합니다. 이 과정에서 에이전트는 개발자가 충분히 명시하지 않은 세부 사항이나 에지 케이스, 설계 옵션이 드러나도록 안내합니다.
  2. 에이전트가 위 네 가지 산출물을 생성합니다.
  3. 에이전트가 단계별로 작업 목록을 수행하며 항목을 체크합니다. 이 단계에서 실제로 구현이 진행됩니다.
  4. 모든 작업이 완료되면 변경 사항을 아카이브합니다. 델타 스펙을 메인 스펙 라이브러리에 병합해 API의 작동 계약에 대한 버전 관리 기록을 남깁니다. 델타 스펙은 시간이 흐르면서 점점 축적돼 전체 시스템의 살아있는 스펙이 됩니다.

검증 자동화: 에이전트가 스스로 실수를 찾게 하기

코딩 에이전트는 종종 첫 시도에서 실수합니다. 코드가 빌드되지 않거나 존재하지 않는 API를 만들어 내기도 합니다. 처음에는 이를 개선하기 위해 에이전트가 피해야 할 함정 목록을 프롬프트에 추가했습니다. 하지만 이는 효과가 없었고 오히려 상황을 악화시켰을 가능성이 큽니다. 이와 관련해 제약 조건을 추가할수록 출력 품질이 저하된다는 연구 결과도 있습니다[14]. 저희가 효과를 본 방법은 에이전트가 스스로 실수를 발견하고 수정하게 하는 것이었습니다. 해답은 검증(verification)이었던 것입니다.

Flava API Gateway를 개발하면서 자동화된 테스트와 린터는 에이전트 기반 코딩에 특히 잘 맞는다는 것을 알았습니다. 이는 LLM 기반 코드 생성에 TDD(test-driven development)

원칙을 적용한 연구에서도 확인됩니다[15]. 모든 요구 사항을 한 번에 에이전트에 주입하는 대신 테스트와 린터가 문제를 점진적으로 드러내도록 만드는 것입니다. 테스트가 실패하면 에이전트는 무엇이 어디서 실패했는지 정확히 알 수 있습니다. 에이전트는 해당 문제를 수정한 뒤 다시 테스트를 실행하며 다음 실패로 넘어갑니다. 제약 조건은 필요한 순간에만 전달합니다.

에이전트가 루프를 호출하는 방식에도 같은 아이디어를 적용했습니다. 단일 프로젝트 스킬은 테스트와 린터, 포매터(formatter)를 번들로 제공하며 AGENTS.md[16]는 에이전트에게 언제 이를 로드해야 하는지 알려줍니다. 이 스킬 지침은 적용될 때에만 에이전트에게 전달됩니다. 매 턴마다 전달되지 않습니다.

물론 코드 품질에 자동화한 테스트와 린팅이 중요하다는 것이 새로운 사실은 아닙니다. 하지만 현실적으로 마감 압박이 커질 때 가장 먼저 줄어드는 게 이런 활동인데요. 에이전트 기반 코딩에서는 이런 활동이 더 이상 선택 사항이 아닙니다. 저희는 개발 과정에서 AI가 구현 중에 이런 검사들을 실행하고 실패하는 일을 빈번히 겪었습니다. 만약 자동화된 검사 없이 이런 문제들을 찾아내려 했다면 훨씬 더 많은 시간이 걸리는 바람에 AI를 사용하면서 얻는 생산성 향상의 상당 부분을 상쇄했을 것입니다.

과거 대부분의 프로젝트는 테스트하기 위해 CI와 공유 테스트 환경에 의존했습니다. 내부 의존성을 로컬에서 프로비저닝하는 일은 너무 번거로운 작업이었고 그 결과도 불안정한 경향이 있었습니다. 그럼에도 개발자가 직접 코드를 작성할 때는 이런 불편함을 감수할 수 있었습니다. 개발자가 코드를 푸시하기 전에 최대한 올바른 코드를 만들어 CI를 왕복하는 시간을 최소화하려고 노력할 수 있었기 때문입니다.

하지만 에이전트는 같은 방식으로 문제를 해결할 수 없습니다. 에이전트의 빠른 반복 속도를 생각할 때 모든 시도를 원격 파이프라인으로 보내는 것은 현실적이지 않습니다. 대기 시간이 길어지는 것은 물론 시도와 시도 사이에 에이전트가 컨텍스트를 잃는 문제도 발생할 것입니다. 그 대신 완전한 로컬 환경을 갖추는 데 투자하면 두 가지 문제를 모두 해결할 수 있습니다. 에이전트는 즉각 피드백을 받고 현재 진행 상황을 잊지 않고 지속적으로 개발 루프를 유지할 수 있습니다. 또한 로컬 의존성은 로그와 상태를 직접 조사할 수 있어 실패 진단도 훨씬 쉬워집니다.

저희의 전체 테스트 모음(suite)는 2,754개의 테스트로 구성되며 아래 세 계층을 포괄합니다.

  • 단위(unit): 비즈니스 로직을 분리하여 테스트
  • 통합(integration): 실제 PostgreSQL(제약 조건, 트리거, 소프트-삭제 연쇄(soft-delete cascade), 트랜잭션), 전체 인프로세스(in-process) HTTP 스택, 모든 응답에 대한 OpenAPI 스펙 준수 검증
  • E2E(end-to-end): 실제 Athenz 인증, Kong 데이터 플레인, API 키 적용, 멀티 테넌트 격리를 포함한 전체 시스템

에이전트가 반복할 때마다 자유롭게 이 테스트를 실행하기 위해선 속도가 중요했습니다. 모든 테스트는 개발자 기기에서 약 15초 내에 완료됩니다. 이 속도를 달성하기 위해서는 병렬 처리에 신경을 써야 했고 테스트 케이스를 최대한 서로 격리했습니다. 예를 들어 실행 간에 DB 테이블을 공유하고 삭제(truncate)하는 대신 각 테스트 패키지가 자체적으로 생성한 자신만의 PostgreSQL 스키마와 격리된 테이블 세트를 사용하도록 만들었습니다.

또한 에이전트가 생성한 코드는 미묘한 방식으로 스펙에서 벗어날 수 있습니다. 이를 잡아내기 위해 모든 통합 테스트 서버를 경량 래퍼(wrapper)로 감싸 일반적인 테스트 과정의 일부로 스펙을 준수하는지 검증했습니다. 아래는 해당 래퍼를 단순화한 버전입니다.

type specValidatingHandler struct { t testing.TB // 실행 중인 Go 테스트에 대한 참조 handler http.Handler } // ServeHTTP는 요청마다 호출됩니다. 내부 핸들러로 요청을 전달(dispatch)하고, // 기록된 응답을 호출자에게 전달한 다음, 해당 응답이 스펙에 맞는지 검증합니다. func (s *specValidatingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { rec := httptest.NewRecorder() s.handler.ServeHTTP(rec, req) // 응답을 실제 writer에 전달 ... validateSpecResponse(s.t, specRouter, req, rec.Code, rec.Header(), rec.Body.Bytes()) } func validateSpecResponse(t testing.TB, router routers.Router, req *http.Request, code int, header http.Header, body []byte) { t.Helper() route, pathParams, err := router.FindRoute(req) if err != nil { return // 스펙에 존재하지 않는 라우트는 스킵 } err = openapi3filter.ValidateResponse(req.Context(), &openapi3filter.ResponseValidationInput{ RequestValidationInput: &openapi3filter.RequestValidationInput{ Request: req, PathParams: pathParams, Route: route, }, Status: code, Header: header, Body: io.NopCloser(bytes.NewReader(body)), Options: &openapi3filter.Options{IncludeResponseStatus: true}, // 스펙에 없는 상태 코드도 실패로 처리 }) if err != nil { t.Fatalf("response does not conform to OpenAPI spec: %v", err) } } // setupTestServer는 모든 통합 테스트가 셋업에서 http.Handler를 가져오기 위해 호출하는 생성자입니다. // 항상 래핑된 핸들러를 반환합니다. func setupTestServer(tb testing.TB /* , ... */) http.Handler { appHandler := buildAppHandler(/* ... */) return &specValidatingHandler{t: tb, handler: appHandler} }

각 테스트는 설정 단계에서 setupTestServer를 호출해 요청 핸들러인 http.Handler를 생성한 뒤 해당 핸들러의 ServeHTTP를 호출해 요청을 전송합니다. 반환된 핸들러가 래퍼이기 때문에 모든 테스트 응답은 OpenAPI 스펙을 준수하는지 검증됩니다. 상태 코드나 헤더, 본문 스키마 중 어느 하나라도 어긋나면 테스트는 실패합니다.

테스트뿐 아니라 린터도 사람 중심 워크플로보다 에이전트 기반 워크플로에서 더 중요해집니다. 사람으로 구성된 팀은 코드 리뷰 문화나 멘탈 모델 공유, 암묵적인 규칙 같은 비공식 규칙에 의존하는데 코딩 에이전트는 이런 것들에 접근할 수 없어 팀의 규범과 일치하지 않는 출력을 생성할 수 있습니다[17]. 에이전트가 따를 수 있는 관습은 자동화 검사뿐입니다. 따라서 저희는 가능한 한 많은 자동화 검사를 도입했습니다. Go 린팅에는 옵트인(opt-in)이 아닌 옵트아웃(opt-out) 방식의 규칙을 적용한 golangci-lint[18]를 사용했고, SQL 및 마이그레이션 린팅[19][20], 빌트인 및 커스텀 규칙을 포함한 Semgrep[21]도 적용했습니다. 이를 통해 에이전트는 일반적인 작업 루프 안에서 각 제약에 대한 피드백을 받을 수 있습니다.

스펙 주도 개발과 검증 자동화 종합하기

스펙 드리븐 개발 루프

각 변경은 설계 단계에서 시작합니다. 개발자는 에이전트와 함께 기능을 탐색하며 에지 케이스와 설계 옵션을 도출하고, 에이전트는 네 가지 OpenSpec 산출물을 생성합니다. 이후 구현 단계에서는 에이전트가 작업 목록을 따라 수행하면서 각 변경 작업 후 테스트와 린터를 실행합니다. 실패는 다음 반복으로 직접 피드백됩니다. 모든 검사를 통과하면 델타 스펙은 메인 스펙 라이브러리에 아카이브되어 작동 계약의 영구 기록을 남깁니다. 그 결과 검증이 모든 단계에 포함된 촘촘한 피드백 루프가 만들어집니다.

로컬 개발 환경 구축: 개발 루프를 로컬에 유지하기

저희가 택한 검증 접근 방식의 중요한 전제 조건은 로컬 개발 환경입니다. 포괄적인 검증은 이를 실행하는 환경을 쉽게 설정할 수 있고 변경할 수 있을 때에만 유지됩니다. 설정 작업에 과도한 노력이 필요하면 건너뛰게 됩니다. 수정하기 어려운 환경은 프로젝트의 발전 속도를 따라가지 못합니다. 새로운 도구는 사라지고 서비스는 동기화에서 벗어나며 개발자는 환경과 함께 작업하기보다는 환경을 우회하기 시작합니다. 저희의 경우 전체 테스트 모음을 로컬에서 실행하려면 PostgreSQL과 실제 Athenz 인증 서비스, 호환 가능한 버전의 린터와 코드 생성기 세트가 필요합니다. 따라서 이 모든 것을 관리하는 로컬 개발 환경이 최대한 원활하게 작동하도록 만들어야 했습니다.

이 문제는 Nix[23]를 기반으로 구축한 Devenv[22]로 해결했습니다. Devenv는 모든 개발자와 CI 러너(runner)가 macOS와 Linux 양쪽에서 같은 devenv shell 명령으로 동일한 버전의 동일한 도구를 사용할 수 있는 프로젝트 범위의 셸을 제공합니다. 컨테이너나 VM과 달리 셸은 호스트 환경을 대체하는 게 아니라 그 위에 레이어를 얹기 때문에 개발자는 평소에 사용하는 편집기와 dotfiles, 워크플로를 그대로 유지할 수 있습니다. 여기에 direnv[24]를 결합하면 프로젝트 디렉터리에 들어갈 때 셸이 자동으로 로드됩니다.

direnv detecting a directory change and automatically loading the dev shell direnv가 프로젝트 디렉터리로 이동하는 것을 감지하고, 자동으로 개발 셸을 불러오는 모습

특히 유용한 점은 Nix가 패키지와 서비스, 설정(환경 변수 또는 생성된 파일 등)을 표현하는 단일 언어 역할을 한다는 점입니다. 예를 들어 저희 컨트롤 플레인은 Go로 작성되었습니다. Go 프로젝트에서 gopls[25]는 에디터와 에이전트가 자동 완성, 탐색, 진단을 위해 사용하는 표준 언어 서버로, 일반적으로 각 개발자는 이를 설치하고 구성해야 하는데요. Devenv는 이를 리포지토리에 한 번만 설정하면 모든 사람이 동일한 설정을 얻을 수 있도록 해줍니다.

다음 스니펫은 gopls 래퍼를 Nix 패키지로 인라인으로 빌드해 Devenv의 languages.go 모듈에 직접 연결해서 개발 셸의 PATH에 gopls를 올리는 예시입니다. 이 래퍼는 프로젝트별 플래그를 실제 gopls 바이너리에 전달하므로 에디터와 에이전트는 개발자별 추가 설정 없이 올바른 구성을 사용할 수 있습니다.

# 기존 gopls 패키지를 인수로 받아 항상 -tags=extdep를 주입한 커스텀 gopls 바이너리를 포함하는 # 래핑된 gopls 패키지를 반환하는 함수입니다. wrapGopls = gopls: pkgs.runCommand gopls.name { nativeBuildInputs = [ pkgs.makeWrapper ]; # 이 부분은 개요를 파악하는 데 필수는 아닙니다. # languages.go의 기대 사항에 맞추기 위해 포함했습니다. passthru.override = lib.setFunctionArgs (args: wrapGopls (gopls.override args)) (lib.functionArgs gopls.override); } '' mkdir -p "$out/bin" makeWrapper ${lib.getExe gopls} "$out/bin/gopls" \ --suffix GOFLAGS ' ' -tags=extdep ''; languages.go.lsp.package = wrapGopls pkgs.gopls;

각 설정 요소는 서로 참조할 수도 있습니다. 저희 컨트롤 플레인 서버는 Devenv가 생성한 YAML 파일에서 런타임 설정을 읽습니다. 예를 들어 PostgreSQL 포트가 서버 설정으로 직접 흘러들어가는 방식은 다음과 같습니다.

providerCfg.database.port = config.services.postgres.settings.port;

PostgreSQL 서비스 포트가 변경되면 생성된 YAML이 자동으로 이를 반영합니다.

Devenv는 120,000개 이상의 패키지를 보유한 Nixpkgs[26]를 활용합니다. 일반적으로 새 도구를 추가하려면 각 개발자 기기와 CI에 별도로 설치해야 하고, 이렇게 하면 시간이 지나면서 서로 버전이 어긋나기 쉬운데 Nix와 Nixpkgs를 사용하면 모든 패키지가 고정되고 재현 가능합니다. 어느 환경에서나 동일하게 작동하는 도구를 추가하는 일은 다음과 같이 한 줄이면 충분합니다.

packages = [ pkgs.openspec pkgs.semgrep pkgs.sqlc pkgs.squawk pkgs.yaml-language-server # OpenAPI pkgs.nickel pkgs.nls pkgs.redocly json-schema-to-nickel ];

설정을 독립적인 단위로 패키징하는 모듈 시스템은 서비스를 재사용하고 확장하고 조합하기 쉽게 만듭니다. PostgreSQL을 활성화하는 데는 다음과 같이 몇 줄이면 충분합니다. 데이터베이스 생성, 사용자 설정, 시작 과정이 모두 처리됩니다.

services.postgres = { enable = true; initialDatabases = [ { name = "flava-api-gateway"; user = "flava-api-gateway"; } ]; };

Devenv는 일반적으로 많이 사용하는 도구에 대한 모듈을 제공하므로 대부분의 경우 기본값과 다른 부분만 수정하면 됩니다. 예를 들어 언어 모듈은 특정 언어에 필요한 컴파일러와 포매터, 언어 서버 및 기타 표준 도구를 제공합니다. 여기서 유일한 커스터마이징은 앞서 언급한 gopls 래퍼를 연결하는 것 뿐입니다.

languages = { nix.enable = true; go = { enable = true; lsp.package = wrapGopls pkgs.gopls; }; };

이 원칙은 저희가 많이 의존하는 접근 제어 시스템인 Athenz[27] 같은 더 복잡한 서비스에도 적용했습니다. 로컬에서 Athenz를 사용할 수 있도록 설정하는 데에는 실제로 상당히 많은 노력이 필요했지만, Devenv가 환경 구성과 서비스 의존성 연결을 처리해 준 덕분에 그 노력은 주변 인프라와 씨름하는 데 쓰이지 않고 Athenz 자체를 이해하는 데 쓰일 수 있었습니다.

Athenz도 로컬에서 실행하기

저희 E2E 테스트는 실제 Athenz 인증에 의존하는데 이를 모킹(mocking)하는 것은 비현실적이었습니다. Athenz는 서비스가 인증서로 인증하고 도메인, 역할, 정책의 계층 구조를 통해 접근을 제어하는 등 인증 모델이 복잡합니다[28]. 따라서 충실한 목(mock)을 만들려면 상당히 많은 로직을 다시 구현해야 하고 이는 Athenz 자체가 아니라 저희가 Athenz를 얼마나 이해했는지를 테스트하게 될 것입니다. 따라서 Athenz는 로컬에서 실행되어야 했습니다.

다음과 같이 완전한 인증 모델은 서비스와 함께 선언됩니다. 서비스 ID와 도메인 계층, 역할과 정책은 모두 시작 시점에 프로비저닝됩니다.

services.athenz = { enable = true; provisionDomains = { # Athenz 서비스 등록 devenv.services = lib.genAttrs ["provider" "apiadminrw" "apiadminro"] (svcName: { pubkey = "${credentialsDir}/devenv/${svcName}.pub.pem"; key = "${credentialsDir}/devenv/${svcName}.key"; cert = "${credentialsDir}/devenv/${svcName}.cert.pem"; }); # Athenz TLD 생성 flava-api-gateway.admins = [ "devenv.provider" ]; # Athenz 서브도메인 생성 "flava-api-gateway.local" = { admins = [ "devenv.provider" ]; roles = { apigateway_admin = { members = [ "devenv.apiadminrw" ]; policies.grant_api_admin = { grant = "*"; on = "apigateway.api.*"; }; }; apigateway_readonly = { members = [ "devenv.apiadminro" ]; policies = { grant_readonly_1 = { grant = "GET"; on = "apigateway.api.*"; }; grant_readonly_2 = { grant = "GET"; on = "apigateway.api"; }; }; }; }; }; }; };

전통적인 실천 방식이 더 중요해진 시대

코드를 빠르게 생성하는 AI의 이점은 그 결과물이 신뢰할 수 있을 만큼 안정적일 때에만 실현됩니다. 이를 가능하게 하는 것이 검증이며, 검증을 하려면 몇 가지 기초 작업이 필요합니다. 저희는 실수를 빠르게 잡아내는 테스트 모음과 이를 원활하게 실행할 수 있는 로컬 환경, 에이전트가 코드를 작성하기 전에 방향을 맞추는 워크플로를 구축했습니다.

검증이 에이전트에게도 유용하려면 빨라야 하는데 빠르게 작동하는 로컬 환경이 없다면 각 검증이 CI를 왕복해야 합니다. 또한 검증을 스펙과 맞추지 않으면 테스트는 통과하지만 설계에서 벗어난 코드를 얻게 됩니다. 빠른 검증과 스펙과 정렬하기, 이 둘을 조합할 때 비로소 에이전트가 스스로 문제를 해결할 수 있을 만큼 충분히 촘촘한 피드백 루프를 만들 수 있습니다. 참고로 QA 팀에서는 시스템 복잡도에 비해 버그가 예상보다 적었다고 말했습니다. 여러 다른 요인과 저희가 이 글에서 소개한 실천 방식을 완전히 분리해 분석할 수는 없겠지만, 저희 역시 개발 과정 전반에 걸쳐 같은 것을 느꼈습니다.

이 글에서 소개하는 실천 방식은 예전부터 유용했던 방식입니다. 단지 달라진 게 있다면 중요한 정도일 것입니다. 이 글에서 소개하는 기반을 제대로 갖춘다면 AI는 품질을 끌어올릴 것입니다. 하지만 그렇지 않다면 AI는 지름길을 걸을 때 감당해야 하는 모든 대가를 증폭할 것입니다.

채용 안내

이 글에서 소개한 문제에 흥미가 있다면 채용 페이지를 확인하세요.

참고 문헌

[1] Ouyang, S. et al. 2025. An Empirical Study of the Non-Determinism of ChatGPT in Code Generation. ACM Transactions on Software Engineering and Methodology. 34, 2 (Jan. 2025), 1–28. https://doi.org/10.1145/3697010.

[2] Larbi, M. et al. 2025. When Prompts Go Wrong: Evaluating Code Model Robustness to Ambiguous, Contradictory, and Incomplete Task Descriptions. arXiv. https://doi.org/10.48550/arXiv.2507.20439.

[3] Tomaz, R. et al. 2026. Impacts of Generative AI on Agile Teams’ Productivity: A Multi-Case Longitudinal Study. arXiv. https://doi.org/10.48550/arXiv.2602.13766.

[4] Peng, S. et al. 2023. The Impact of AI on Developer Productivity: Evidence from GitHub Copilot. arXiv. https://doi.org/10.48550/arXiv.2302.06590.

[5] Afroz, S. et al. 2025. The Fast and Spurious: Developer Productivity with GenAI. arXiv. https://doi.org/10.48550/arXiv.2510.24265.

[6] METR. Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity. Retrieved from https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/.

[7] Wang, X. et al. 2025. CodeVisionary: An Agent-based Framework for Evaluating Large Language Models in Code Generation. arXiv. https://doi.org/10.48550/arXiv.2504.13472.

[8] Kong. Retrieved from https://github.com/Kong/kong.

[9] OpenSpec — A lightweight spec‑driven framework. Retrieved from https://openspec.dev.

[10] OpenAPI Initiative. Retrieved from https://www.openapis.org.

[11] Huang, R. et al. 2024. Generating REST API Specifications through Static Analysis. Proceedings of the IEEE/ACM 46th International Conference on Software Engineering. ACM. https://doi.org/10.1145/3597503.3639137.

[12] Nickel. Retrieved from https://nickel-lang.org.

[13] Given When Then. Retrieved from https://martinfowler.com/bliki/GivenWhenThen.html.

[14] Dente, F. et al. 2026. Constraint Decay: The Fragility of LLM Agents in Backend Code Generation. arXiv. https://doi.org/10.48550/arXiv.2605.06445.

[15] Mathews, N.S. and Nagappan, M. 2024. Test-Driven Development and LLM-based Code Generation. Proceedings of the 39th IEEE/ACM International Conference on Automated Software Engineering. ACM. https://doi.org/10.1145/3691620.3695527.

[16] AGENTS.md. Retrieved from https://agents.md.

[17] Wang, Y. et al. 2025. Beyond Functional Correctness: Investigating Coding Style Inconsistencies in Large Language Models. Proceedings of the ACM on Software Engineering. 2, FSE (June 2025), 690–712. https://doi.org/10.1145/3715749.

[18] Golangci-lint. Retrieved from https://golangci-lint.run.

[19] SQLFluff. Retrieved from https://www.sqlfluff.com.

[20] Squawk — a linter for Postgres migrations. Retrieved from https://squawkhq.com.

[21] Semgrep. Retrieved from https://semgrep.dev.

[22] devenv. Retrieved from https://devenv.sh.

[23] Nix & NixOS. Retrieved from https://nixos.org.

[24] direnv. Retrieved from https://direnv.net.

[25] Gopls: The language server for Go - The Go Programming Language. Retrieved from https://go.dev/gopls/.

[26] NixOS Search. Retrieved from https://search.nixos.org/packages.

[27] Athenz IO - Home. Retrieved from https://www.athenz.io.

[28] Athenz IO - Explore. Retrieved from https://www.athenz.io/explore.html#model.

Tech-Verse 2026 개최 안내 — 6월 29일

image

이 글은 이벤트의 공식 기사로 공개되었습니다.
Tech-Verse 2026은 LY Corporation가 개최하는 기술 컨퍼런스입니다.
혁신적인 기술적 도전 과정과 현장의 생생한 인사이트를 공유합니다.

YouTube LIVE를 통한 생중계도 꼭 시청해 주세요.
https://tech-verse.lycorp.co.jp/2026/ko/

Read Entire Article