AI 에이전트에게 Rails 구현과 RSpec 작성을 맡기면, 한 번의 요청만으로 수백 줄에 달하는 차이(diff)가 도착할 수 있습니다. 이 모든 것을 읽어보는 것은 현실적으로 불가능합니다. 게다가 테스트가 통과하더라도, 그 테스트가 구현상의 버그를 찾아낼 것이라고 보장할 수는 없습니다.
필자는 사람이 설계 단계에서 지켜야 할 규칙을 정해 '계약(Contract)'으로 표로 정리하고, AI에게 이 표를 지키는 구현 코드와 테스트 코드를 작성하도록 하는 방식을 시도했습니다. 테스트의 견고함은 Mutation Test 도구인 evilution과 메서드별 CRAP 값으로 검증합니다. 이렇게 하면 사람이 리뷰할 때 집중해야 할 영역을 '계약' 표와 검증 도구가 지적한 곳으로 좁힐 수 있습니다.
서론
예상 독자는 다음과 같습니다.
- Rails의 설계와 구현은 가능하지만, AI 에이전트가 작성한 방대한 코드 중 어디를 리뷰해야 할지 모르는 사람
- AI가 작성한 RSpec 테스트가 통과하더라도, 그 테스트를 신뢰할 수 있을지 판단하기 어려운 사람
- Mutation Test를 시도해보고 싶지만, mutant의 라이선리가 신경 쓰여 도입을 미루고 있는 사람
본문에서 다루지 않을 내용은 다음과 같습니다.
- mutant 자체의 사용법
- 형식 검증(Formal Verification)이나 Mutation Test에 대한 연구 논의
- Minitest에서의 설정
검증에는 필자의 macOS를 사용했습니다. 버전은 Ruby 4.0.7, Rails 8.1.4, MySQL 8.4.11, rspec-rails 8.0.4, evilution 1.4.0, SimpleCov 1.3.2입니다. 코드와 측정용 스크립트는 rails-invariant-gates에 있습니다. 본문에서는 설명이 없으면 오해하기 쉬운 부분만 코드를 넣고, 나머지는 리포지토리 파일로 링크했습니다.
절차를 하나씩 따라가고 싶은 분들을 위해, 같은 내용을 장별로 자세히 쓴 책 『AI가 작성한 Rails의 어디를 읽을까』도 준비되어 있습니다.
용어집
| 용어 | 의미 |
|---|
| 불변 조건 (Invariant) | 객체나 데이터가 유효한 상태인 한, 항상 성립하는 조건 |
| ... | |
테스트가 통과해도 버그를 놓칠 수 있다
먼저, 테스트가 통과한다는 것과 테스트가 버그를 찾아낼 수 있다는 것은 별개의 문제임을 인지해야 합니다. 다루는 예제는 18세 이상이면 성인으로 판정하는 User#adult?입니다.
20세와 15세 두 건만 확인하는 spec에서는 SimpleCov로 볼 때 대상 라인은 모두 실행되었습니다. 그럼에도 불구하고 evilution은 변이(mutation) 17개 중 2개가 살아남았다고 보고했습니다. 그중 한 개가 다음의 변경입니다.
만 18세 경계의 사람에 대한 판정이 바뀌는 변경이지만, 15세와 20세에서는 결과가 변하지 않습니다. 라인 커버리지(Line Coverage)는 '그 라인을 통과했는지'만 보기 때문에, 이러한 경계 버그는 수치로 나타나지 않습니다.
경계의 한 단계 전인 17세와 경계인 18세를 추가하는 것만으로는 부족합니다. 이 두 건의 spec에서는 >=를 ==로 변경하는 변이 등 3개가 살아남았습니다. 이는 18세와 17세에서는 원래 구현과 같은 결과가 나오지만, 19세에서 처음 차이가 발생하기 때문입니다. 기존의 20세와 15세 예시는 그대로 두고, 한 단계 전인 17세, 경계인 18세, 한 단계 후인 19세를 spec에 추가하자, 17개 모두가 killed(제거)되었습니다.
판단 기능 자체는 변경하지 않았으므로 기존의 예시는 삭제하지 않았습니다.
Mutation Test는 실제 코드에서 버그를 찾는 도구와 역할이 다릅니다. 현재 존재하는 테스트가 코드 의미의 변화를 찾아낼 수 있는지 조사하는 도구입니다. 커버리지 수치에 나타나지 않는 테스트의 결함에 대해서는 Rails, React, Go, Rust, Python 등의 예시로 'AI에게 테스트 작성을 맡겨 커버리지 100%인데도 버그가 발생하는 이유'를 정리했습니다. 본문에서는 이러한 결함을 Rails에서 어떻게 찾아내고 막을 수 있는지 다룹니다.
설계로 계약을 표로 만들기
본문에서 지켜야 할 규칙을 정하는 방식은 계약 기반 설계(Design by Contract)에 영향을 받았습니다. 동작을 시작할 수 있는 조건을 사전 조건(Precondition), 정상적으로 끝난 후에 성립하는 조건을 사후 조건(Postcondition)이라고 부릅니다. 언제나 성립하는 상태의 조건이 불변 조건(Invariant)이며, 이 세 가지를 모아 '계약'으로 다루는 사고방식입니다.
여기서부터 다룰 예제는 EC 사이트의 재고 할당입니다. 재고는 총 재고, 새로운 주문에 할당 가능한 가용 재고(Available Inventory), 주문에 이미 할당된 할당済み 재고(Committed Inventory) 세 가지 수치로 표현됩니다.
사람이 설계 단계에서 결정하는 것은 다음 표입니다. AI에게 지시 파일(AGENTS.md)에도 같은 표를 두고, 계약을 변경할 때는 이 표에서 수정하기로 정했습니다.
| ID | 분류 | 계약 | 검증할 spec |
|---|
| INV-01 | 불변 조건 | 재고 수는 음수가 되지 않는다 | InventoryPosition의 .new, Product의 유효성 검사 및 CHECK 제약 |
| INV-02 | 불변 조건 | 총 재고는 가용 가능 재고와 할당된 재고의 합과 일치한다 | 위와 동일 |
| INV-03 | 불변 조건 | 재고 수는 정수이다 | InventoryPosition의 .new, Product의 유효성 검사 |
| PRE-01 | 사전 조건 | 할당 수량은 양의 정수이다 | InventoryPosition#reserve |
| PRE-02 | 사전 조건 | 할당 수량은 가용 가능 재고 이하이다 | InventoryPosition#reserve |
| POST-01 | 사후 조건 | 가용 가능 재고가 지정된 수량만큼 줄어든다 | InventoryPosition#reserve |
| ... | | | |
AGENTS.md | | | |
여기에는 이 재고 할당 표 외에도, 성인 판정, 배송비, 주문 취소, 결제, HTTP API, CRAP 값 계산에 대한 계약이 같은 형태의 표로 놓여 있습니다. 모든 spec은 이름에 어느 한 행의 ID를 포함하도록 규칙을 정했습니다. 어떤 spec도 이름으로 인해 잘못된 경우와 spec 중 어느 것이 올바른지 표에서 판별하려면, 모든 spec이 표의 어딘가 행에 대응해야 하기 때문입니다.
불변 조건은 재고가 어떤 상태에 있더라도 지켜야 할 성질을 나타냅니다. 사전 조건은 할당을 시작할 수 있는 입력 범위이며, 사후 조건은 할당이 끝난 후의 수량을 결정합니다.
표의 가장 오른쪽에 '검증할 spec' 열을 둔 것은, AI가 계약과 테스트를 혼동하지 않도록 하기 위해서입니다. 계약을 한 줄 추가하면, 추가해야 할 spec도 동시에 결정됩니다. 검토하는 사람에게도 이 열은 어떤 spec을 읽어야 할지 알려주는 지표가 될 것입니다.
이 진행 방식은 표가 업무적으로 올바르다는 것을 전제로 합니다. evilution, 커버리지(coverage), CRAP 값은 모두 spec과 구현이 표대로인지를 확인하는 도구일 뿐, 표의 내용 자체가 옳은지는 측정할 수 없습니다. 따라서, 구현을 작성하기 전 업무 설계 과정에서, 업무 분석을 통해 계약을 정하고 사람이 표를 검토하는 단계가 필수적입니다. 분석에 AI의 도움을 받는 것은 괜찮습니다. 다만, 현재 존재하는 spec이나 구현으로부터 행을 옮겨 적으면, 거기에 포함된 오류까지 올바른 계약으로 표 안에 들어가 버립니다.
재고 수량은 DB에 의존하지 않는 값 객체(InventoryPosition)로 처리합니다. 생성자(constructor)에서 불변 조건을 확인하고, freeze를 통해 수정하는 것을 방지하는 설계입니다. reserve는 자신을 바꾸지 않고 새로운 인스턴스를 반환하므로, 할당 후의 상태도 반드시 생성자의 검증을 통과합니다.
불변 조건만으로는 부족하다
불변 조건만 테스트하면, 수량 이동을 잘못한 구현을 놓칠 수 있습니다. 필자는 할당 후 합계가 총 재고와 일치하는 것만을 확인하는 spec을 작성하고, 재고 모델에 대해 evilution을 실행했습니다. 150건 중 62건의 변이가 살아남았습니다. 그중에는 두 개의 재고에 넣는 값을 서로 바꾼 변이가 있습니다.
- available_stock: available_stock - quantity,
- reserved_stock: reserved_stock + quantity
+ available_stock: reserved_stock + quantity,
...
재고 10개에서 3개를 할당한 결과는, 올바른 구현이라면 7과 3입니다. 이 변이에서는 3과 7이 되지만, 합계는 여전히 10이므로 불변 조건 테스트는 통과해 버립니다.
'가용 가능 재고가 3 줄어들고, 할당된 재고가 3 늘어난다'라는 사후 조건까지 확인하자, 이 변이도 killed 됩니다. 할당 수량의 두 경계 전후(-1개, 0개, 1개와, 9개, 10개, 11개)를 추가한 최종 버전의 spec에서는, 150건 모두가 killed 되었습니다. 설계 표에 사후 조건 행을 넣은 것은, 이 놓침을 방지하기 위함입니다.
evilution으로 테스트의 견고함을 검증하다
라이선스로 도구를 고르기
Ruby의 Mutation Test에는 이전부터 mutant가 있습니다. 하지만 mutant의 README에서는 무료로 사용할 수 있는 것이 공개 리포지토리에서의 이용에 한정되어 있다고 합니다. 상업적으로 사용하려면 개발자 1인당 월 $30 또는 연 $250의 구독이 필요합니다. 회사 비공개 Rails 앱에 적용하기 전에 이 조건을 반드시 확인하세요.
MIT 라이선스의 포크(fork)인 mutest도 시도했습니다. 하지만 mutest 리포지토리는 2020년에 아카이브되었습니다. 의존하는 morpher의 오래된 버전이 RubyGems에 없어, bundle install이 의존 관계를 해결하지 못하고 실패했습니다.
그래서 MIT 라이선스로 공개된 evilution을 선택했습니다. RSpec이 기본 연동처이며, Rails에서는 변이(mutation)마다 프로세스를 분리하는 설정이 자동으로 적용됩니다. 마찬가지로 MIT의 mutineer도 후보가 될 수 있습니다. 둘 다 2026년에 초판이 나온 지 얼마 안 된 도구이므로, 도입하기 전에 변경 이력과 issue의 움직임을 살펴보는 것이 좋겠습니다.
살아남은 뮤턴트(Surviving Mutants) 처리 방법
살아남은 뮤턴트는 다음 중 하나로 대응합니다.
- 계약을 확인하는 spec을 추가합니다. 지금까지의 예시는 모두 여기에 해당합니다.
- 동작 변화 없이 제거할 수 있는 구현을 삭제합니다.
- 동작이 변하지 않는 등가 뮤턴트(equivalent mutant)라면, 이유를 적어 검증 대상에서 제외합니다.
이유를 적어 제외하는 방식은 간편한 만큼 남용되기 쉬운 부분이 있습니다. 그래서 제외할 때는 행 끝에 # evilution:disable을 붙이고, 바로 앞 줄에 이유를 작성하기로 규칙을 정했습니다. CRAP 값을 계산하는 코드에서는 다음과 같이 작성합니다.
# 배열의 size와 length는 같은 값을 반환하므로, 위치를 바꾸는 변이는 구별할 수 없다.
total, over = rows.size, offenders.size # evilution:disable
이 줄 단위 비활성화(disabling)는 같은 줄에 있는 의미 있는 변이까지 제외해 버립니다. 재고 모델에서는 행 끝에 붙이자 46개의 변이가 검증에서 제외되었습니다. 그래서 재고 모델은 is_a?(Integer)를 동작이 같은 instance_of?(Integer)로 수정했습니다. 이렇게 하니 아무것도 제외하지 않고 150개를 검증할 수 있었습니다.
evilution은 실행하지 않고 등가라고 판정한 변이도 스코어에서 제외합니다. 이 판정은 차분 문자열을 보는 휴리스틱스(heuristics)입니다. 필자가 등가라고 판단된 4건을 직접 수정하여 spec을 실행해 보니, 정말 등가였던 것은 단 1건뿐이었습니다. 스코어가 100%여도 JSON 출력의 equivalent에 들어간 변이는 한 번 읽어보는 것이 마음 편합니다.
CRAP 값으로 너무 복잡한 메서드 차단하기
CRAP 값은 Alberto Savoia가 crap4j 소개 기사에서 제시한 지표입니다. 순환적 복잡도(cyclomatic complexity)를 comp, 테스트 커버리지(test coverage)를 cov (%)라고 할 때, comp² × (1 − cov/100)³ + comp으로 계산합니다. 분기가 많은 메서드가 테스트로 보호되지 않으면 값이 급격히 커지는 것이 특징입니다.
원문에서는 첫 번째 임계값(threshold)으로 30을 제시했지만, 이 리포지토리에서는 15로 설정했습니다. 그럼에도 복잡도 3의 메서드는 커버리지 0%여도 CRAP 값이 12이므로 게이트에서는 막히지 않습니다. CRAP 값만으로는 단순한 메서드의 테스트 부족을 막을 수 없기 때문에, 라인 커버리지와 evilution의 게이트를 조합하여 사용하고 있습니다.
기존 gem인 skunk는 파일 단위의 스코어이기 때문에, 메서드별로 측정하는 Rake 태스크를 직접 만들었습니다. Prism으로 순환적 복잡도를 세고, SimpleCov의 라인 커버리지와 대조합니다. 약한 spec만으로 커버리지를 가져가면 CRAP 값이 15 이상인 메서드가 4개 발견되었고, 태스크는 종료 코드 1로 중단되었습니다.
Rails의 영속화 계층에서 불변 조건 지키기
값 객체(Value Object)의 검증을 통과하지 못하는 업데이트 경로도 있습니다. update_columns는 Rails 문서에 나와 있듯이 유효성 검사(validation)를 건너뜁니다. 그래서 마이그레이션의 add_check_constraint로 불변 조건(invariant condition)을 DB에도 설정했습니다. MySQL 8.4.11에서 음수 재고를 쓰려고 하면, Rails 8.1.4는 ActiveRecord::CheckViolation을 발생시킵니다.
다만, CHECK 제약조건이 확인하는 것은 저장되는 상태가 유효한지 여부뿐입니다. 7개를 6개로, 3개를 4개로 바꾸는 업데이트도 제약을 만족하므로, 할당 수량의 오류는 DB에서는 발견되지 않습니다. 이 부분은 값 객체의 사후 조건(postcondition)을 담당합니다.
동시 실행에는, 할당 서비스의 with_lock
으로 대비하고 있습니다. 병렬 실행 spec에서는 재고 1개의 상품에 2개의 DB 연결에서 동시에 할당합니다. with_lock을 제거한 버전으로 20회 실행했을 때, 20회 모두 2건의 할당이 성공했습니다.
이때 DB의 재고는 할당 가능 0개, 할당 완료 1개이며, CHECK 제약 조건과 저장 규칙(save rule)도 충족합니다. DB 상태는 정상적으로 보이지만, 2건의 주문이 같은 재고를 할당한 것이 됩니다. with_lock을 복구하면, 20회 모두 성공은 단지 1건만 되었습니다. 개별 spec과 CHECK 제약 조건만으로는 이 버그를 찾을 수 없습니다.
spec은 실제 DB에 읽기/쓰기를 시키다
지금까지의 spec은 ActiveRecord를 stub하지 않고, compose.yaml로 구동한 MySQL에 읽기/쓰기를 시킵니다. Rails의 테스트 가이드는 테스트 전용 DB에 대해 테스트를 실행하는 것을 전제로 작성되어 있습니다. Rails 개발자인 DHH도 "Test-induced design damage"에서 모델 테스트를 DB로부터 분리하려는 것을 권장하지 않았습니다. 테스트를 빠르게 하기 위해 Active Record를 Repository 뒤에 숨겨도, 설계가 명확해지지 않는다는 주장입니다. stub한 모델은 실제 DB에서는 일어나지 않는 상태에서도 spec을 통과시키기 때문에, CHECK 제약 조건이나 락의 작동 여부를 확인할 수 없습니다.
하지만, 실제 DB만으로는 만들 수 없는 상태가 하나 있습니다. update!이 예외를 발생시키는 것은 유효성 검사(validation)에 실패했을 때입니다. 그런데 재고 행은 CHECK 제약 조건이, 미저장 변경은 with_lock이, 할당 후 값은 값 객체(value object)가 거부하기 때문에, 유효성 검사에 실패하는 상태에 도달하지 않습니다. 주문에는 유효성 검사 자체가 없습니다. 이 spec을 제외하고 evilution을 실행하면, update!를 update로 변경하는 변이가 살아남았습니다. 그래서, 저장 실패를 확인하는 spec만은 valid?를 stub하고, 이유를 바로 앞 줄에 주석으로 작성했습니다.
# 실제 DB의 행은 CHECK 제약 조건이, 미저장 변경은 with_lock이, 할당 후 값은 InventoryPosition이 거부하기 때문에,
# 유효성 검사에 실패하는 상태를 실제 DB에서는 만들 수 없다. update!을 update로 변경하는 변이를 찾기 위해, 여기만 valid?를 stub한다.
allow(product).to receive(:valid?).and_return(false)
또 다른 예외는 외부 결제 API입니다. spec에서 실제로 호출하면, 결과가 통신이나 상대방 서비스의 상태에 좌우되기 때문에, 대역(mock)으로 대체하여 호출 인자만 확인하고 있습니다. stub은 빠르게 실패를 재현할 수 있기 때문에, 사람이 쓰든 AI에게 시키든 늘어나는 경향이 있습니다. 이유가 적힌 stub은, 실제 DB에서 다시 쓸 수 있는지 먼저 생각합니다.
AI가 작성한 후에 인간이 볼 곳
설계로 정한 계약과 완료 조건은, AI가 읽는 AGENTS.md에 정리했습니다. Claude Code용 CLAUDE.md에는 @AGENTS.md의 한 줄만 적고 같은 지시를 불러오게 했습니다.
## 완료 조건
- `bin/rails quality`가 성공할 때까지 완료로 하지 않는다.
- `pre-commit run --all-files`가 성공할 때까지 완료로 하지 않는다. `--no-verify`로 훅을 건너뛰지 않는다.
...
bin/rails quality는, 계약 ID 대조, RSpec, evilution, CRAP 값 순서로 실행하며, 어느 하나가 실패하면 중단됩니다. 최종 버전에서는, evilution이 1,093개의 변이 중 1,088개를 killed 했고, 살아남은 것은 0개였습니다.
첫 번째 대조에서 확인하는 것은, spec 이름에 계약표의 ID가 있는지와, 표의 어느 행에도 그 ID를 가진 spec이 있는지를 확인하는 것입니다. 다만, 이 대조는 이름 문자열만 볼 수 있습니다. SHIP-03이라고 적은 spec이 실제로는 다른 움직임을 확인하고 있어도, 대조에서는 구별할 수 없습니다. 대조를 통과하기 위해서만 그럴듯한 ID를 붙이는 것도 가능하므로, spec 내용이 해당 행의 계약을 확인하는지는 계속해서 인간이 리뷰로 읽기로 했습니다.
완료 조건에는 게이트 외에, AI의 조사 방법과 확인 방법도 적어 놓았습니다. 이 역시 이번 기사 작업에서 실제로 필요했던 규칙입니다.
1차 정보를 읽고 나서 작성하게 하기
AI는 학습한 지식만을 바탕으로 답변을 구성하기 때문에, 버전이 변경된 API나 새로운 도구에 대해서는 오류를 포함할 수 있습니다. 일반적인 이론이나 추측으로 작성하게 하지 않고, 사용 중인 버전의 1차 정보를 읽은 후에 작성하도록 합니다. mutant 라이선스 조건은 README에서, evilution의 등가 판정 오류는 판정 소스에서 처음 확인했습니다. 보고서에는 근거로 삼은 문서의 URL 또는 소스의 파일과 줄을 작성하게 합니다.
실제로 실행하여 검증시키기
SimpleCov가 커버리지 결과를 덮어쓰는 경우나, with_lock을 제거했을 때 두 건 모두 포착되는 경우도 직접 실행해 보기 전까지는 알 수 없었습니다. 보고서에는 실행한 명령어와 출력을 작성하게 합니다.
E2E 블랙박스 테스트를 작성하게 하기
모델과 서비스의 spec만으로는 라우팅, 입력 변환, JSON 형태, 상태 코드를 검증할 수 없습니다. 이 리포지토리에서는 HTTP API의 계약도 표로 만들고, 실제로 구동된 서버에 외부에서 요청을 보내는 spec으로 검증하고 있습니다. 외부에서 검증하는 형태로 하면 테스트가 구현 내용에 맞춰 작성되는 것을 방지하기 쉽습니다. API를 변경할 때는 E2E spec을 먼저 수정하여 실패를 확인한 후에 구현하는 규칙을 세웠습니다.
사람이 리뷰로 읽어야 할 부분
완료 조건을 충족했는지 여부는 기계가 판정할 수 있습니다. 따라서 사람이 리뷰에서 읽어야 할 곳은 다음으로 좁혀집니다.
- spec 내용이 이름으로 지정한 계약의 줄을 검증하고 있는지 (이름의 ID 누락은 대조 작업이 막아줍니다)
- evilution이 생존자로 언급한 변이가 정말로 spec 부족에 의한 것인지
# evilution:disable를 붙인 줄의 이유가 등가라는 설명이 되어 있는지 - ActiveRecord를 stub한 spec에 실제 DB에서는 알 수 없는 이유가 작성되어 있는지
- 계약표를 변경하지 않은 수정으로 기존 spec의 예시/입력/기대값이 사라지거나 바뀌지는 않았는지
- 수정한 spec에 사람의 승인과, 계약표의 어느 줄에 맞추었는지 설명이 있는지
equivalent에 들어간 변이와 CRAP 값이 높은 순서로 나열된 상위 메서드
AI 보고서에 근거 출처와 실행 결과가 작성되어 있지 않으면, 그 부분은 검증되지 않은 것으로 간주합니다. AI가 AGENTS.md의 완료 조건이나 .evilution.yml을 수정하지 않았는지도 차이점(diff)으로 확인합니다. 게이트 임계값을 낮추는 변경은 테스트를 강화하지 않으면서 게이트를 통과시키는 가장 쉬운 방법이기 때문입니다. 실패한 spec의 기대값을 구현에 맞춰 수정하는 변경 역시 같은 이유로 막습니다. 기존 spec을 수정할 수 있는 경우는, 계약표를 변경하여 기능을 개수할 때와 spec의 기대값이 표와 불일치한다고 사람이 인정한 경우뿐입니다.
수정을 일률적으로 금지하면, 처음부터 잘못된 spec을 고칠 수 없고, AI가 올바른 구현을 잘못된 spec에 맞춰 망가뜨릴 위험이 있습니다. 게다가, spec의 오류는 evilution이나 줄 커버리지로는 발견되지 않습니다. spec과 구현이 같은 오류를 가지고 있다면, 테스트는 통과하고 변이도 killed되기 때문입니다. 그래서 spec이 실패하면 구현을 고치기 전에 계약표와 대조하게 합니다. 구현이 표와 다르면 구현을 고치고, spec이 표와 다르면 수정하지 않고, 계약 ID와 spec의 줄을 보고하게 합니다. 승인을 얻은 spec 수정을 구현 변경과 별도의 커밋으로 할 규칙입니다.
규모가 커졌을 때의 지침
대상 코드가 늘어날수록 evilution 실행 시간은 길어집니다. 필자의 구성에서는 app/models와 app/services 371개 검증에, 병렬 1로 69.4초, 4병렬로 16.5초가 걸렸습니다. 병렬 수는 --jobs로 지정합니다. evilution은 각 워커에 TEST_ENV_NUMBER를 설정하므로, database.yml의 DB명에 이 값을 채워 넣으면 워커별로 DB를 나눌 수 있습니다.
앱 전체를 매번 검증하는 것보다 대상을 선택하는 것이 현실적입니다. 금액 계산, 권한 부여(認可), 상태 전이, 재고와 같이 오류가 발생하면 손해가 큰 도메인 로직부터 시작합니다. 생성된 코드나 단순 CRUD의 컨트롤러는 나중으로 미뤄도 괜찮습니다.
풀 리퀘스트 단위에서는 변경한 파일만 전달하는 운영이 적절합니다. evilution은 lib/foo.rb:15-30와 같은 줄 범위 지정과, 변경되지 않은 파일 결과를 재사용하는 --incremental
에 대응하고 있습니다. 전체 검증은 야간 CI 등 시간이 여유로운 장소에서 돌리는 것이 좋을 것입니다.
동시성(Concurrency)의 계약은 단일 테스트(spec)만으로는 확인할 수 없습니다. DB를 사용하는 통합 테스트를 별도로 준비하여, DB 종류나 트랜잭션 분리 레벨을 변경했을 때는 반드시 재실행해야 합니다.
요약
AI에게 맡기는 코드가 늘어나도, 사람이 읽어야 하는 것은 계약표와 검증 도구가 지적한 부분만으로 충분합니다. 상황별 선택 방법은 다음과 같습니다:
| 상황 | 선택할 것 |
|---|
| 계약표를 만들 때 | 구현을 작성하기 전에 업무 분석부터 결정하고, 사람이 리뷰한다 |
| ... | with_lock과 병렬 실행 통합 테스트를 사용한다 |
| HTTP 진입점의 버그가 걱정될 때 | 구동된 서버에 외부에서 요청을 보내는 E2E spec을 작성하게 한다 |
| AI 설명이 맞는지 판단할 수 없을 때 | 1차 정보 출처와, 실행한 명령어의 출력을 보고하게 한다 |
| 검증 시간이 길 때 | 대상을 도메인 로직으로 좁히고, 변경된 코드만 검증한다 |
뮤테이션 테스트(Mutation Test) 점수가 100%여도, 사양 자체가 올바른 것은 아닙니다. 어떤 계약을 지켜야 할지는 앞으로도 사람이 설계로 결정할 일입니다. AI에게는 그 표를 지키게 하는 구현과 테스트를 작성하게 합시다.
이 글이 AI로 구현 속도를 높일 수 있었지만, 대량으로 생성된 소스 코드의 품질 보증을 어떻게 해야 할지 고민하는 분들께 도움이 되기를 바랍니다.
토론