입사 후 마주한 실망스러운 사실

회사에 입사한 뒤 여러 프로그램의 코드를 살펴보다 테스트가 아예 없다는 사실을 알게 됐다.
기능이 동작하는지는 개발자가 직접 화면을 눌러 확인했고, 수정 이후 기존 기능이 그대로 동작하는지는 사람의 기억과 수작업에 의존했다.
품질이 보장되지 않는 프로그램을 클라이언트에게 납품해 왔다는 사실이 실망스러웠다.

이미 이루어진 과거의 선택을 바꿀 수는 없지만, 지금 맡은 프로젝트에서는 같은 상태를 그대로 두고 싶지 않았다.
결함이 하나도 없는 프로그램은 사실상 어렵다는 걸 알기에, 중요하다고 합의한 로직과 빌드 과정은 변경할 때마다 같은 기준으로 검증됐다고 말할 수 있는 최소한의 품질을 만들고 싶었다.

테스트를 개인 습관이 아닌 병합 조건으로 만들기

CI/CD 파이프라인을 처음 구축할 때 서비스 계층부터 유닛 테스트를 붙였다.
데이터 접근 함수는 mock으로 격리해 DB 없이 빠르게 실행했고, 이후에는 PGlite 기반 데이터 접근 테스트도 추가했다.

수동 검증은 일정 압박과 반복 작업 속에서 누락되기 쉽고, 내 컴퓨터에서 통과한 결과는 다른 사람이 확인하기 어렵다.
그래서 GitLab Runner가 MR과 main의 변경을 받을 때마다 같은 명령을 실행하게 했다.

현재 흐름은 이렇다.

MR 생성·갱신
  ├─ 서비스 유닛 테스트
  ├─ PGlite 데이터 접근 테스트
  └─ 서비스 변경 대비 테스트 누락 검사(경고)
          ↓
        main
          ↓
  앱 빌드·Playwright 렌더 스모크
       (배포 시 수동, 주 1회 자동)
          ↓
       개발 서버 배포

MR에서는 빠른 테스트로 병합 전에 피드백을 받는다.
main에서는 배포할 때 수동으로, 주 1회는 자동으로 실제 앱을 빌드하고 띄운 뒤 핵심 화면을 확인한다.
배포 잡은 이 검증을 통과해야만 실행된다.
테스트 결과는 JUnit 리포트로 남겨 실패한 테스트와 메시지를 GitLab에서도 바로 확인하게 했다.

서비스 파일이 바뀌었는데 대응하는 테스트 파일이 함께 바뀌지 않으면 알려주는 잡도 두었다.
현재는 오탐 가능성을 고려해 파이프라인을 막지 않는 경고다.
그래도 리뷰에서 “왜 서비스는 바뀌었는데 테스트는 그대로인가?”라는 질문을 하는 역할은 충분히 했다.

빨간 파이프라인이 보여준 것

파이프라인을 도입하고 한 달여가 지나는 동안, 효과는 통과일 때보다 실패했을 때 더 분명하게 드러났다.
그 기간 동안 실제로 어떤 문제를 막았는지 당시 로그를 기준으로 정리해 본다.

conflict는 해결됐지만 테스트 준비가 빠졌다

기능 브랜치에 최신 main을 합치면서 conflict를 해결한 직후였다.
생성 로직은 단순 등록 함수 대신 연관 데이터까지 함께 만드는 새 함수를 호출하도록 바뀌어 있었다.
하지만 기존 유닛 테스트의 DB mock에는 이 함수가 없어 서비스 테스트가 곧바로 실패했다.

같은 파이프라인의 누락 검사도 관련 서비스 파일 두 곳이 바뀌었는데 대응 테스트 변경이 없다고 알렸다.

Git이 conflict marker를 모두 없앴다고 작업이 끝나지는 않았다.
각 코드 조각은 존재해도 새 호출 관계에 맞춘 mock과 검증이 빠질 수 있다.
이 실패 덕분에 생성 로직과 연관 데이터에 대한 테스트를 보완한 뒤 병합했다.

파이프라인이 없었다면 conflict를 해결한 파일만 훑어보고 넘어갔을 것이다.
파이프라인은 개발자가 기억하지 못한 주변부까지 다시 실행해 줬다.

화면 수정이 기존 조회 규칙을 건드렸다

시각화 화면의 토글 기능을 수정하면서 조회 서비스 로직까지 바꿨을 때는 더 직접적인 회귀가 잡혔다.
요청한 대상과 관계없는 데이터가 조회 결과에 섞인 것이다. 해당 서비스에 이미 작성해 둔 테스트가 이 변화를 잡았다.

실패는 세 번의 파이프라인에서 반복됐다.
로그를 확인한 뒤 조회 결과를 요청 범위로 제한하고 계층 구조를 만들도록 서비스 로직을 수정했다. 그제야 파이프라인이 통과했다.

테스트는 단순히 함수가 실행되는지만 보지 않았다.
기존에 지키기로 한 데이터 경계를 변경 이후에도 그대로 지키는지 확인했다.
“수정 사항이 기존 로직을 건드렸는가?”라는 막연한 걱정이 실패한 테스트 한 건과 구체적인 입력·출력 차이로 바뀌었다.
유닛 테스트의 가치를 이때 가장 크게 느꼈다.

유닛 테스트 밖의 오류는 빌드가 잡았다

유닛 테스트를 모두 통과해도 애플리케이션이 빌드된다는 보장은 없다.
실제로 main의 배포 전 E2E 잡에서 Next.js 앱을 올리던 중 Turbopack 빌드가 실패했다.

원인은 import 방식이었다.
컴포넌트는 named export를 제공하는데 두 화면이 여전히 default import를 썼다.

두 import를 바로잡은 뒤 빌드와 E2E가 통과했다.
유닛 테스트가 보지 않는 모듈 연결 문제를 실제 빌드가 잡았고 E2E를 통과해야만 실행되는 배포 잡도 함께 멈췄다.
깨진 앱이 개발 서버에 반영되는 일은 없었다.

다만 이 오류는 MR이 아니라 main의 수동 E2E에서 발견됐다.
배포 전에는 막았지만 병합 전에는 막지 못했다.
가벼운 npm run build 검사를 MR 단계로 당겨야 한다는 개선점이 남았다.

도입하고 달라진 점

테스트가 없던 상태에서는 개발자가 수동으로 일일이 확인하는 것에 의존할 수밖에 없었다.
지금은 어떤 테스트가 다시 실행됐고 어디까지 통과했는지 기록으로 확인한다.

conflict를 해결한 뒤에는 눈에 보이는 충돌 파일만 확인하지 않아도 된다.
새 함수 호출과 mock의 연결이 끊겼는지 테스트가 알려준다.
화면을 수정한 뒤에는 관련 없어 보이는 서비스의 반환 규칙이 달라졌는지 기존 테스트가 확인한다.
컴포넌트의 export 방식을 바꾸고 사용처 하나를 놓치면 빌드가 멈춘다.

리뷰에서는 “문제없어 보입니다” 대신 실패한 테스트 이름, 기대값과 실제값, 테스트가 빠진 서비스 파일을 놓고 이야기한다.
실패를 고친 과정도 파이프라인 이력에 남으니 같은 문제가 다시 생겼을 때 추측부터 시작하지 않아도 된다.

CI/CD가 품질을 저절로 만들어 주지는 않는다.
테스트로 표현하지 않은 요구사항은 검사할 수 없고 잘못 작성한 테스트는 초록색 거짓말을 한다.
지금의 테스트 누락 검사도 경고에 머물러 있고 빌드 검사는 더 앞당길 여지가 있다.

이러한 테스트 파이프라인을 도입하여 최소한의 선은 생겼다.
코드를 병합하고 배포하는 과정에서 반복해서 확인해야 할 항목을 자동화하여 반복 확인에 드는 비용도 줄였다.

마무리

테스트가 없다는 사실을 발견하고 최소한의 품질선부터 만들기로 한 지 한 달여가 지났다.
그 기간 동안 파이프라인은 여러 번 빨간색이 됐다.
conflict 해결 뒤 빠진 mock, 기존 조회 규칙을 깨뜨린 변경, 잘못된 import 때문에 실패한 빌드가 실제로 있었다.
그때마다 문제를 발견하고 배포되기 전에 수정할 수 있었다.

클라이언트에게 완벽한 애플리케이션을 약속할 수는 없다.
대신 최소한 우리가 정의한 품질 기준을 매 변경마다 통과한 애플리케이션을 전달하겠다고는 말할 수 있게 됐다.
이번에 만든 테스트와 파이프라인이 그 약속을 사람의 주의력에서 실행 가능한 절차로 옮겼다.

댓글남기기