tsma — 레거시 코드의 회귀 방어선

테스트 없는 코드를 어떻게 리팩토링하는가?

10만 줄짜리 레거시 코드를 물려받았다. 테스트가 없다. 리팩토링하고 싶지만 건드리면 뭐가 깨질지 모른다. 테스트를 짜려면 코드를 이해해야 하고, 코드를 이해하려면 문서가 있어야 하는데 문서도 없다.

아무도 안 건드린다. 더 썩는다.

전 세계 모든 레거시 코드가 이 교착 상태에 있다. Fortune 500 기업의 IT 예산 중 60~80%가 레거시 유지에 쓰인다. 개발자 시간의 42%가 기술 부채 처리에 소비된다.

LLM이 테스트를 대신 짜줄 수 있다면?


LLM에게 테스트를 맡기면 생기는 문제

LLM에게 “이 함수의 테스트를 작성해줘"라고 하면 뭔가 나오긴 한다. 문제는 세 가지다.

첫째, 어디부터 시작해야 하는지 모른다. 함수가 527개일 때 1번부터 순서대로? 가장 중요한 것부터? 기준이 없다.

둘째, 테스트의 질을 검증할 수 없다. LLM이 작성한 테스트가 pass했다. 그런데 이 테스트가 실제로 함수의 동작을 검증하고 있는가, 아니면 그냥 호출만 하고 assert가 없는 빈껍데기인가? 사람이 일일이 읽어봐야 안다.

셋째, 피드백이 없으면 LLM의 테스트는 60~70%에서 멈춘다. “이 함수를 테스트해줘"만으로는 분기 커버리지 100%에 도달하지 못한다. 어떤 분기가 빠졌는지 알려줘야 나머지를 채운다.

LLM이 테스트를 못 짜는 게 아니다. LLM에게 뭘 짜야 하는지, 얼마나 잘 짰는지 알려주는 구조가 없는 것이 문제다.


tsma: 명령어 하나로 돌아가는 테스트 레일

tsma는 프로젝트의 모든 함수를 인덱싱하고, 테스트 유무를 감지하고, 커버리지를 측정하고, LLM 에이전트에게 정확한 피드백을 주는 CLI 도구다.

에이전트가 알아야 할 명령어는 하나다.

$ tsma next

이 명령어 하나가 전체 루프를 구동한다:

$ tsma next          # 테스트가 없는 다음 함수를 보여준다
  → 테스트를 작성한다
$ tsma next          # 새 테스트를 감지하고, 실행하고, 커버리지를 측정한다
  → 100%? PASS, 다음 함수로
  → <100%? 미커버 분기를 라인 번호와 함께 알려준다
$ tsma next          # 수정된 테스트를 재측정한다
  → 개선되든 아니든, DONE으로 표시하고 다음으로

“All functions complete!“가 나올 때까지 반복한다.


527개 함수에서 검증했다

실제 Go 프로젝트(527개 함수)에 tsma를 적용했다.

결과비율
PASS (100% 분기 커버리지)24646.7%
DONE (best-effort)28153.3%
TODO (미처리)00%

246개 함수는 분기 커버리지 100%에 도달했다. 나머지 281개는 100%에 도달하지 못했지만, 가능한 범위까지 테스트가 작성되었다.

왜 100%에 도달하지 못하는 함수가 있는가?


100%에 도달하는 함수와 못하는 함수

함수가 100% 분기 커버리지에 도달할 수 있는지는 의존성을 어떻게 받는지에 달려 있다.

인터페이스(mockable) — 100% 달성 가능:

type Handler struct {
    svc AuthSvc              // interface — mock으로 교체 가능
}

테스트에서 mock을 주입하면 모든 경로를 제어할 수 있다:

svc := mocks.NewMockAuthSvc(ctrl)
svc.EXPECT().Login(...).Return(result, nil)   // 성공 경로
svc.EXPECT().Login(...).Return(nil, err)      // 실패 경로

구체 타입(not mockable) — 100% 불가:

type Handler struct {
    svc *service.SMSImportService    // struct 포인터 — 교체 불가
}

실제 구현이 DB, 외부 API 등 내부 의존성을 가지고 실행된다. 특정 에러를 발생시키거나 특정 결과를 반환하게 만들 수 없다. 그 결과에 의존하는 분기는 단위 테스트로 도달할 수 없다.

tsma의 대응: 미커버 분기 피드백 후 한 번 더 시도한다. 그래도 도달하지 못하면 DONE으로 수용한다. 이건 도구의 한계가 아니라 코드의 테스트 가능성을 반영한다. 인터페이스를 도입하면(DI) 100%가 가능해지지만, 그건 원본 코드를 수정하는 일이다.


피드백이 LLM의 테스트를 극적으로 바꾼다

tsma의 핵심 가치는 인덱싱도, 커버리지 측정도 아니다. 미커버 분기를 라인 번호로 정확히 알려주는 것이다.

피드백 없이:

"ListContracts 함수의 테스트를 작성해줘"
→ LLM이 happy path만 테스트
→ 커버리지 60~70%

피드백과 함께:

"ListContracts 함수의 테스트를 작성해줘"
→ 커버리지 65% (11/17)
→ UNCOVERED:
    line 41 — if params.Status != nil
    line 44 — if params.BuildingId != nil
    line 70 — if err != nil (CountSummary)
→ LLM이 정확히 그 분기를 커버하는 테스트 추가
→ 커버리지 100%

같은 LLM이다. 차이는 피드백의 유무뿐이다. 라인 번호 세 줄이 60%와 100%를 가른다.


에이전트가 죽어도 진행은 보존된다

LLM 에이전트는 뻗는다. 토큰 한도, 네트워크 에러, 세션 끊김. 527개 함수를 한 세션에 다 처리할 수 없다.

tsma는 진행 상태를 .tsma/session.json에 영속 저장한다.

$ tsma status

527 functions
PASS:  246 (46.7%)
DONE:  281 (53.3%)
TODO:    0 (0.0%)

에이전트가 200번째 함수에서 죽으면? 새 에이전트가 tsma next를 치면 201번째부터 이어간다. session.json이 체크포인트다.

여러 에이전트가 교대로 작업해도 충돌이 없다. 함수 단위로 원자적이다.


세션은 캐시고, 소스 파일이 진실이다

tsma의 설계 원칙 중 하나: session은 캐시이고 소스 파일이 source of truth다.

테스트 파일을 삭제하면 session.json에 PASS로 기록되어 있더라도 해당 함수는 TODO로 되돌아간다. 세션이 현실과 괴리되지 않는다.

원칙:
  session.json이 "PASS"라고 해도
  테스트 파일이 없으면 → TODO
  소스 파일이 바뀌었으면 → 재측정 대상

LLM 에이전트에게 주는 지시

에이전트에게 필요한 지시는 6줄이다:

1. tsma next 실행
2. TODO면 — 함수를 읽고 테스트 작성
3. 테스트 실패면 — 에러를 읽고 테스트 수정
4. 미커버 분기가 표시되면 — 그 분기를 커버하는 테스트 추가
5. PASS/DONE이면 — 다음 함수가 자동으로 표시됨
6. "All functions complete!"가 나올 때까지 반복

에이전트가 알아야 할 명령어는 tsma next 하나뿐이다. 나머지는 CLI가 제약한다.


기차와 선로

바이브 코딩은 기차다. 빠르다. 하지만 선로가 없으면 탈선한다.

AI 코딩 도구들은 전부 기차를 더 빠르게 만드는 데 집중하고 있다. 더 큰 모델, 더 똑똑한 에이전트, 더 나은 프롬프트. 그런데 기차가 빨라질수록 탈선의 피해도 커진다.

tsma는 선로다. LLM이 테스트를 생성하고(Neural), CLI가 “여기까지만"을 정의한다(Symbolic Constraint). LLM의 창의성은 그대로 두되, 결과의 질은 기계가 강제한다.

기존tsma
테스트 작성사람 (느림) 또는 LLM (무질서)LLM이 작성, CLI가 검증
어디부터?사람이 판단CLI가 순서 결정
품질 확인사람이 리뷰CLI가 커버리지 측정
피드백없음미커버 분기 라인 번호
진행 추적없음session.json 자동

LLM은 자유롭게 생성한다. 하지만 tsma next라는 선로 위에서만 달린다.


언어 지원

언어인덱서테스트 러너커버리지
Gogo/astgo testgo test -coverprofile
TypeScriptregexnpx vitest / npx jestc8 / istanbul
Pythonregexpytestcoverage.py

Go는 AST 파서로 정확한 함수 추출. TypeScript와 Python은 정규식 기반.

생성 파일(*_gen.go, *.pb.go), 테스트 파일, vendor/node_modules는 인덱싱에서 자동 제외된다.


설치와 실행

make install
cd your-legacy-project
tsma next

그게 전부다.

MIT License. github.com/park-jun-woo/tsma