서론
평소에는 AI에게 Jest로 유닛 테스트를 작성하게 하고 있습니다. 테스트는 늘어나고 CI도 통과합니다. 하지만, 통과하고 있는 테스트가 무엇을 보장하는지 제 자신의 언어로 설명할 수 없다는 느낌이 들었습니다.
그러던 중 ababup1192님의 'AI 개발 시대이기 때문에 비로소 테스트의 역할을 재조명하다'라는 글을 읽게 되었습니다. 이 글에서는 원문의 핵심 요점을 정리하고, 이것을 타입이 거의 number인 업무 시스템 프론트엔드에 어떻게 적용할 수 있을지 고민해 봅니다.
참고로, 후반부의 적용 방안은 아직 실제 프로덕트에 반영하지 않은 것입니다. 도입 계획과 그 판단 이유를 작성한 내용입니다.
원문 기사의 핵심 요점
테스트 종류별로 감지할 수 있는 결함이 다르다
원문 기사는 테스트의 종류를 '어떤 것이 우월한가'가 아니라 '어떤 종류의 결함을 감지할 수 있는가'라는 관점에서 비교하고 있습니다. 원문의 표를 제가 이해한 바에 따라 간략화하면 다음과 같습니다.
| 테스트 종류 | 주로 담당하는 결함 |
|---|
| 타입/lint | 값의 오용, 팀 규칙 위반 |
| ... | |
E2E 테스트에서도 계산 오류를 발견할 때가 있습니다. 하지만 경계값 조합을 수십 가지나 E2E로 확인하는 것은 효율적이지 않습니다. 반대로 유닛 테스트를 아무리 두껍게 작성해도, 화면 전환 과정에서 데이터가 인계되지 못하는 오류는 찾아내기 어렵습니다.
AI의 수정 루프는 빠른 테스트로 돌려야 한다
원문 기사는 'AI에게 구현시키고, 테스트가 pass할 때까지 수정시키는' 진행 방식을 전제로 하고 있습니다. AI는 '수정 → 실행 → 결과 읽기'를 반복하기 때문에, 1회당 대기 시간과 몇 번 만에 고쳐지는지가 생산성을 좌우합니다.
E2E 테스트는 느릴 뿐만 아니라, 실패하더라도 원인 위치가 파악하기 어렵습니다. 그만큼 AI가 추측으로 수정하게 되면서 루프 횟수도 늘어나기 쉽습니다. 따라서 원문 기사는 타입/lint/유닛 테스트/PBT를 AI의 피드백에 사용하고, integration 테스트나 E2E 테스트는 결합 및 실환경 확인에 집중하도록 정리했습니다.
E2E를 줄일 때는 먼저 빠른 테스트가 실패하는지 확인한다
E2E 테스트를 줄이려면, 해당 테스트의 주요 assert마다 대응되는 결함을 일부러 구현하여 빠른 테스트가 fail하는지 확인합니다. 만약 fail하지 않는다면, 먼저 빠른 테스트를 추가한 후 삭제합니다. 이 확인 작업은 AI에게 맡길 수도 있다고 언급되어 있습니다.
셀프 힐링(Self-healing)과 브라우저 에이전트는 합격/불합격 판정에 사용하지 않는다
AI에 의한 셀프 힐링은 깨진 selector를 수정하는 데는 유용합니다. 하지만 버튼 문구 변경이 의도된 것인지, 아니면 회귀(regression)인지는 테스트 측에서는 판단할 수 없습니다. 자연어 시나리오로 동작하는 브라우저 에이전트 역시 결과가 모델의 판단에 좌우됩니다. 원문 기사는 합격/불합격 판정에는 결정적인 E2E 테스트를 사용하고, 에이전트는 탐색적 테스트(exploratory test)에 사용하는 것이 좋다고 합니다.
타입으로 '테스트해야 할 범위'를 좁히기
원문 기사에서 가장 실무적으로 유용하다고 느낀 부분은 AI가 작성한 유닛 테스트를 어떻게 리뷰할 것인가 하는 장입니다.
함수가 number를 그대로 받으면, 입력값은 사실상 무한합니다. -500도 3.14도 NaN도 금액 위치에 세율의 0.1을 전달할 수 있습니다. AI는 그중에서 떠오른 케이스를 골라 테스트를 작성하지만, 그 선택 방식이 충분한지는 모든 테스트를 읽어봐야 판단할 수 있습니다. 게다가 많은 케이스가 원래 존재해서는 안 되는 값 확인에 사용됩니다.
원문 기사의 답은 테스트의 작성 방식이 아니라, 테스트되는 쪽의 설계에서 범위를 좁히는 것입니다.
class Price {
private constructor(readonly value: number) {}
...
}
생성자(constructor)를 private으로 만들면, Price를 만드는 입구는 Price.of()밖에 없습니다. 부적절한 값은 애초에 만들 수 없게 됩니다.
const p = Price.of(1000);
const bad = Price.of(-500);
new Price(1000);
이렇게 하면 테스트와 리뷰의 대상이 분리됩니다.
- 부적절한 값에 대한 테스트는
Price.of()의 경계값(-1, 0, 소수점)에 모이고 -
calcTotal(price: Price, qty: Quantity) 같은 함수의 테스트는 계산이 올바른지만 보면 됩니다. 사람이 리뷰하는 것은 개별적인 테스트가 아니라, Price.of()의 조건이 사양대로인지 여부입니다.
입구에서 한 번 체크하면 안은 안전하다는 공항 보안 검색과 유사한 생각입니다. AI에게 테스트 작성을 맡기는 전제라면, 사람이 읽어야 하는 양을 줄일 수 있다는 점이 그대로 효과를 발휘합니다.
업무 시스템 프론트엔드에 적용하는 방안 고려하기
여기서는 아직 도입하지 않은 계획에 대한 이야기입니다.
업무 시스템의 프론트엔드에서는 금액, 날짜, 구분값 등이 거의 number로 처리되며, 타입이 느슨한 경우가 많습니다. 갑자기 모든 것을 값 객체(Value Object)로 만드는 것은 현실적이지 않습니다. 변경 범위가 너무 넓어 리뷰나 마이그레이션 자체가 멈출 수 있습니다.
사고 시 피해가 큰 값은 하나만 타입으로 지정하기
처음에는 잘못되었을 때의 영향이 큰 값 딱 하나를 선택합니다. 예를 들어 청구서 마감일 같은 경우입니다.
type ClosingDayValue = number | 'endOfMonth';
class ClosingDay {
private constructor(readonly value: ClosingDayValue) {}
...
128 또는 '월말'만 허용합니다. 2931일을 허용하면, 월에 따라 존재하지 않는 날을 어떻게 처리할지에 대한 분기가 테스트에 여기저기 흩어지게 됩니다. 이것을 입구에서 막는 것이 목표입니다. 여기서 정한 조건이 사양(Specification)으로서 올바른지는 실제 업무 규칙에 비추어 확인이 필요합니다.
브랜드 타입이라는 가벼운 선택지
또 다른 후보가 바로 브랜드 타입(Brand Type)입니다. 원문에는 나오지 않은 작성 방식이며, 적용을 검토하는 과정에서 나온 아이디어입니다.
type Price = number & { readonly __brand: 'Price' };
기존 코드가 number를 전제로 작성되어 있다면, 클래스보다 도입 부담이 적어 보입니다.
개인적인 생각
클래스와 브랜드 타입의 트레이드오프(Trade-off)
현재 이해로는 다음과 같이 정리하고 있습니다.
- 클래스: private 생성자로 입구를 강제할 수 있다는 장점이 있지만,
.value를 거쳐야 하므로 기존 number 전제 코드에는 수정해야 할 부분이 늘어납니다.
- 브랜드 타입: 실행 시점에는
number 그대로이므로 기존 코드에 익숙하게 적용하기 쉽습니다. 하지만 as Price와 같이 작성하면 어디서든 만들 수 있기 때문에, 입구를 지키기 위해서는 별도의 규칙(규약)이나 린트(lint)가 필요할 것 같습니다.
'입구를 하나로 만든다'는 원문의 목표를 우선한다면 클래스, 마이그레이션 비용을 우선한다면 브랜드 타입이라는 판단이 될 것이라고 생각합니다. 이 부분은 실제로 시도해 보고 확인해 볼 계획입니다.
AI 지시 및 리뷰 관점
t 타입을 적용한 후에는 AI에 대한 지시와 리뷰의 관점도 바꿀 수 있다고 생각합니다.
- 생성 함수 (
of())의 테스트에는 경계값(Boundary Value)을 반드시 포함하도록 지시한다.
- 타입을 받는 함수의 테스트에는 부적절한 값 케이스를 작성하지 않도록 한다.
- 리뷰에서는 테스트 1건씩이 아니라, 생성 조건 자체가 사양과 일치하는지를 본다.
원문의 '그래도 남는 확인 사항'으로 언급된 내용처럼, 구현을 일부러 망가뜨려서 테스트가 실패하는지 확인하거나, 현재 시간이나 난수(Random Number)에 대한 의존성을 인수로 받아 결정적으로 만드는 것도 거론되었습니다. 이 역시 리뷰 관점에 포함하고 싶은 항목입니다.
향후 검증 계획
다음으로 깊이 파고들어 검증하고 싶은 것은 다음 3가지입니다.
- 판별 가능한 유니온(Discriminated Union): 화면의 로딩 상태 등을 유한한 타입으로 표현하여, 테스트의 커버리지를 건수로 판단할 수 있는지 여부.
- PBT (Property-Based Testing): 값의 범위가 넓은 타입에 대해 '총 금액은 0 이상'과 같은 속성(Property)으로 검증하는 방법.
- 브랜드 타입의 본격 도입: 클래스와의 비교를 실제 코드로 확인해 보는 것.
요약
- 테스트 종류마다 탐지할 수 있는 결함과 비용이 다르다.
- AI 수정 루프는 빠른 테스트로 돌리고, E2E(End-to-End)는 화면을 넘나드는 흐름과 실제 환경에 국한한다.
- 타입으로 부적절한 값을 만들 수 없게 하면, AI가 작성하는 테스트의 범위와 사람이 리뷰하는 범위 모두가 줄어든다.
- 타입이 느슨한 기존 코드에는, 사고 시 피해가 큰 값 딱 하나부터 적용하는 것이 현실적이라고 생각한다.
참고
- ababup1192『AI 개발 시대라서야말로, 테스트의 역할을 되돌아보다』(Zenn, 2026/09/29 공개)
Discussion