돌아가는 코드와 믿을 수 있는 코드, 웹개발 테스트의 함정
배포 버튼을 누르기 전 테스트가 모두 초록색이었는데, 실제 서비스에서는 회원가입이 멈추고 결제 금액이 다르게 표시됩니다. 이런 사고는 테스트가 부족해서만 생기지 않습니다. 틀린 방식으로 작성된 테스트가 개발자에게 거짓 확신을 주기 때문에 더 크게 번지는 경우가 많습니다.
코딩에서 중요한 것은 단순히 실행되는 코드를 만드는 데 그치지 않습니다. 입력과 상태가 바뀌어도 의도한 결과를 내는지 확인해야 합니다. 코딩의 기본 개념을 이해하는 단계에서 한 걸음 더 나아가려면, 실패를 발견하는 테스트와 실패를 숨기는 테스트를 구별할 수 있어야 합니다.
통과하는 테스트와 버그를 찾는 테스트는 다릅니다
구현 내용을 그대로 복사한 테스트
할인 금액을 계산하는 함수가 있다고 가정해 보겠습니다. 본문 코드에서 상품 가격에 0.9를 곱하고, 테스트 코드에서도 같은 방식으로 기대값을 계산했다면 어떨까요? 할인율을 잘못 이해해 양쪽에 똑같이 0.9를 적어도 테스트는 통과합니다. 구현과 테스트가 같은 실수를 공유하면 초록색 결과는 정확성의 증거가 아닙니다.
좋은 테스트는 내부 계산식을 따라 쓰기보다 사용자가 관찰할 결과를 고정합니다. 10만 원 상품에 10% 할인을 적용하면 최종 금액은 9만 원이어야 한다는 식입니다. 세금, 쿠폰 중복, 소수점 처리처럼 정책이 얽힌 경우에는 기획 문서나 승인된 예시를 기준값으로 삼아야 합니다. 개발자가 방금 작성한 알고리즘을 정답지로 다시 사용하는 실수는 피하세요.
테스트 이름도 결과를 읽을 수 있게 작성해야 합니다. calculateDiscount test처럼 함수 이름만 반복하면 실패 원인을 파악하기 어렵습니다. ‘정액 쿠폰과 비율 쿠폰이 함께 있으면 정액 쿠폰을 먼저 차감한다’처럼 조건과 기대 결과가 드러나면, 코드를 처음 보는 팀원도 규칙을 검토할 수 있습니다.
- 하지 말아야 할 일: 실제 함수의 계산식을 테스트에 그대로 복사하기
- 확인할 기준: 사용자 화면, API 응답, 저장된 데이터처럼 외부에서 관찰 가능한 결과
- 좋은 테스트 이름: 조건과 행동, 기대 결과가 한 문장에 드러나는 이름
- 실패 사례: 잘못된 할인율이 구현 코드와 기대값 계산 코드에 동시에 들어가 테스트가 통과한 경우
테스트가 통과했다는 사실보다 중요한 질문은 ‘이 테스트는 어떤 잘못을 발견할 수 있는가?’입니다.
테스트 개수와 테스트 가치가 비례하지 않는 이유
코드 커버리지 숫자만 올리는 실수
커버리지 90%를 달성하면 서비스가 안전할 것처럼 느껴집니다. 그러나 코드 한 줄이 실행됐다는 사실은 그 결과를 제대로 검증했다는 뜻이 아닙니다. 반환값을 확인하지 않고 함수를 호출하기만 해도 일부 도구에서는 해당 줄이 실행된 것으로 기록됩니다. 높은 커버리지와 강한 검증은 서로 다른 개념입니다.
특히 웹개발 프로젝트에서는 정상 요청만 반복해 커버리지를 채우기 쉽습니다. 로그인 성공, 게시글 조회 성공, 주문 생성 성공만 검사하고 만료된 토큰이나 중복 요청, 권한 부족, 네트워크 지연은 건너뜁니다. 실제 장애는 이런 경계에서 발생하는데도 보고서에는 보기 좋은 숫자만 남습니다. 여러분의 테스트 목록에는 ‘실패해야 성공인 경우’가 얼마나 포함돼 있나요?
의미 없는 단위 테스트의 징후
단순한 getter나 프레임워크가 이미 보장하는 기능을 수십 개 검사하면서 핵심 비즈니스 규칙은 테스트하지 않는 프로젝트도 많습니다. 테스트 수가 늘어날수록 실행 시간과 유지 비용은 증가하므로, 위험도가 낮은 코드를 기계적으로 검사하는 방식은 생산성을 떨어뜨립니다. 결함이 발생할 가능성과 발생했을 때의 피해를 함께 고려해 우선순위를 정해야 합니다.
| 테스트 대상 | 겉보기 성과 | 실제 가치 | 우선순위 |
|---|---|---|---|
| 단순 속성 반환 | 커버리지 상승 | 낮음 | 낮음 |
| 권한별 데이터 노출 | 작성 비용 큼 | 매우 높음 | 높음 |
| 결제 중복 요청 | 구현 복잡 | 매우 높음 | 높음 |
| 프레임워크 기본 동작 | 테스트 수 증가 | 낮음 | 낮음 |
- 매출, 개인정보, 권한에 영향을 주는 기능을 먼저 표시합니다.
- 정상 입력보다 빈 값, 최댓값, 중복 입력과 잘못된 형식을 우선 추가합니다.
- 커버되지 않은 줄을 무조건 채우지 말고 그 줄의 실패 위험을 평가합니다.
- 삭제해도 결함 탐지력이 달라지지 않는 테스트는 통합하거나 제거 후보로 기록합니다.
모킹의 편리함과 실제 환경의 거리는 생각보다 큽니다
모든 의존성을 가짜로 바꾼 단위 테스트
데이터베이스, 외부 API, 현재 시간, 파일 시스템을 모두 모킹하면 테스트는 빠르고 안정적으로 실행됩니다. 문제는 가짜 객체가 실제 시스템과 다른 행동을 할 때입니다. 데이터베이스 제약 조건을 모킹이 무시하거나, 실제 결제 API가 문자열로 보내는 값을 가짜 응답에서는 숫자로 반환하면 테스트 환경에서만 완벽한 코드가 만들어집니다.
한 쇼핑몰 팀이 재고 차감 로직을 테스트하면서 저장소 객체를 전부 가짜로 대체했다고 가정해 보겠습니다. 각 테스트에서는 재고가 정확히 1개씩 줄었지만, 운영 데이터베이스에서는 두 요청이 동시에 같은 재고를 읽어 품절 상품이 중복 판매됐습니다. 모킹은 호출 관계를 확인하는 도구이지 동시성, 스키마, 네트워크 계약까지 증명하는 장치가 아닙니다.
그렇다고 모든 테스트에서 실제 외부 서비스를 호출하는 것도 좋은 선택은 아닙니다. 속도가 느려지고 사용량 비용이 발생하며 상대 서비스 장애에 따라 결과가 흔들릴 수 있습니다. 단위 테스트에는 통제된 가짜 객체를 쓰되, 데이터 형식과 저장 동작은 통합 테스트에서 실제 환경과 최대한 가깝게 확인하는 층별 전략이 필요합니다. 프로그래밍이 문제 해결 절차를 명령으로 구성하는 작업이라는 점은 프로그래밍 용어 설명에서도 살펴볼 수 있으며, 테스트 역시 그 절차의 전제와 결과를 검증하는 코드입니다.
- 단위 테스트: 계산 규칙과 조건 분기를 빠르게 검증합니다.
- 통합 테스트: 데이터베이스 쿼리, 직렬화, 트랜잭션과 설정을 확인합니다.
- 계약 테스트: 서비스 사이의 요청과 응답 형식이 합의와 일치하는지 검사합니다.
- 종단 간 테스트: 사용자의 핵심 흐름이 브라우저부터 서버까지 이어지는지 확인합니다.
- 하지 말아야 할 일: 모킹된 메서드가 호출됐다는 사실만으로 기능 전체가 검증됐다고 판단하기
호출 횟수에 과도하게 묶인 테스트
내부 메서드가 정확히 두 번 호출되는지, 어떤 순서로 실행되는지를 모든 테스트에서 검사하면 작은 리팩터링에도 테스트가 무너집니다. 사용자 결과는 그대로인데 구현 방식이 바뀌었다는 이유만으로 실패하는 테스트는 개발 속도를 떨어뜨립니다. 호출 횟수는 메일 중복 발송이나 결제 승인처럼 횟수 자체가 요구사항일 때만 엄격하게 검증하세요.
가짜 객체가 많아질수록 테스트 속도는 빨라질 수 있지만, 현실과의 거리를 별도의 통합 테스트로 메우지 않으면 확신의 품질은 낮아집니다.
무작위 실패와 재실행 통과를 정상으로 취급하지 마세요
시간과 실행 순서에 의존하는 테스트
같은 코드를 바꾸지 않았는데 테스트가 성공과 실패를 반복한다면 흔히 ‘플레이키 테스트’라고 부릅니다. 개발팀은 급한 마음에 재실행 버튼을 누르고 통과한 결과만 채택하기 쉽습니다. 하지만 이런 습관이 반복되면 진짜 회귀 오류가 발생해도 구성원들이 또 일시적인 문제라고 생각하게 됩니다. 신뢰를 잃은 테스트 묶음은 없는 테스트보다 위험할 수 있습니다.
대표적인 원인은 현재 시각을 직접 읽는 코드입니다. 자정 직전과 직후에 날짜가 달라지고, 실행 서버의 시간대가 다르며, 만료 시점을 초 단위로 비교하다 경계값에서 실패합니다. 테스트에 고정된 시계를 주입하고 시간대를 명시하면 재현성이 높아집니다. 단순히 대기 시간을 1초에서 3초로 늘리는 방식은 느린 환경에서 다시 실패할 뿐 근본적인 해결이 아닙니다.
공유 데이터도 자주 문제를 일으킵니다. 앞 테스트가 만든 사용자나 설정을 지우지 않아 다음 테스트의 조건이 달라지고, 병렬 실행에서는 같은 이메일 주소나 포트를 사용한 테스트끼리 충돌합니다. 각 테스트는 독립된 데이터를 만들고 종료 후 정리해야 하며, 실행 순서를 바꿔도 결과가 같아야 합니다. 전체 실행에서는 성공하지만 단독 실행에서 실패하는 경우도 숨은 의존성을 의심해야 합니다.
- 현재 시간을 코드 안에서 직접 가져오지 말고 테스트가 제어할 수 있는 시계 객체를 사용합니다.
- 고정된 포트, 공용 파일명, 동일한 계정 ID 대신 테스트별 고유 값을 생성합니다.
- 비동기 처리는 임의의 시간만큼 기다리지 말고 완료 조건이 충족될 때까지 제한 시간 안에서 확인합니다.
- 실패한 테스트의 이름, 시드 값, 환경 정보와 로그를 보존해 같은 조건을 재현합니다.
- 재실행으로 통과했더라도 실패 기록을 닫지 말고 원인과 담당자를 남깁니다.
테스트 순서가 숨기는 상태 오염
첫 번째 테스트가 관리자 권한을 활성화하고 원래 상태로 돌려놓지 않으면 뒤의 권한 테스트가 우연히 통과할 수 있습니다. 이를 발견하려면 테스트 실행 순서를 무작위로 바꾸거나 일부 테스트만 골라 반복 실행해 보는 방법이 효과적입니다. 실패 순서를 기록할 수 있도록 무작위 시드도 함께 출력해야 같은 조합을 다시 만들 수 있습니다.
테스트 격리를 위해 매번 전체 데이터베이스를 새로 만드는 방식은 정확하지만 시간이 많이 들 수 있습니다. 트랜잭션 롤백, 테스트 전용 스키마, 컨테이너 재사용 등 프로젝트 규모에 맞는 절충안을 선택하세요. 중요한 것은 속도만 높이는 것이 아니라 어떤 상태가 테스트 사이에 공유되는지 팀이 명확히 알고 통제하는 것입니다.
오늘 실패하는 테스트 하나를 먼저 설계해 보세요
버그를 고치기 전에 재현 코드부터 남기는 방법
개발 중 발견한 버그를 곧바로 수정하고 싶겠지만, 먼저 그 버그를 재현하는 테스트를 작성해 보세요. 수정 전에는 빨간색으로 실패하고 수정 후에는 초록색으로 바뀌어야 합니다. 이 순서를 지키면 원인을 정확히 건드렸는지 확인할 수 있고, 몇 달 뒤 같은 문제가 되살아나는 것도 막을 수 있습니다. 처음부터 통과하는 회귀 테스트는 문제 상황을 제대로 재현하지 못했을 가능성이 있습니다.
예를 들어 회원가입 폼에서 이름 앞뒤의 공백 때문에 중복 계정이 생겼다면 ‘공백이 있는 이메일과 없는 이메일을 동일하게 취급한다’는 테스트를 먼저 추가합니다. 이어 입력 정규화 로직을 수정하고, 대소문자와 유니코드 문자까지 경계 사례를 확장합니다. 이 과정은 단순 코딩을 넘어 요구사항을 실행 가능한 문서로 바꾸는 작업입니다. 관련 배경을 넓히고 싶다면 프로그래밍 개념 자료도 함께 참고할 수 있습니다.
처음부터 거대한 테스트 체계를 만들 필요는 없습니다. 지금 작업 중인 저장소에서 최근 수정된 버그 하나를 고르고, 입력과 기대 결과를 한 줄씩 적은 뒤 가장 가까운 테스트 파일에 재현 사례를 추가하면 됩니다. 테스트가 너무 어렵다면 코드가 데이터베이스, 시간, 네트워크에 과도하게 결합돼 있다는 설계 신호일 수도 있습니다.
- 재현: 최근 버그의 실제 입력값을 개인정보 없이 테스트 데이터로 바꿉니다.
- 실패 확인: 수정 전 테스트가 예상한 이유로 실패하는지 오류 메시지를 읽습니다.
- 최소 수정: 테스트를 통과시키는 데 필요한 범위만 변경합니다.
- 경계 확장: 빈 값, 중복 요청, 최댓값 등 인접한 실패 조건을 한두 개 추가합니다.
- 전체 실행: 관련 모듈과 전체 테스트를 실행해 다른 기능의 회귀 여부를 확인합니다.
10분 안에 실행할 한 가지 행동
지금 코드 편집기를 열고 최근 장애나 버그 티켓 하나를 선택하세요. 수정 코드를 다시 만지는 대신, 그 문제가 되살아나면 반드시 실패할 테스트 하나를 작성해 실행하십시오. 빨간색 실패 화면과 그 이유를 확인하는 것까지가 오늘의 행동입니다. 이 작은 습관이 ‘그냥 돌아가는 웹개발 코드’를 ‘변경해도 믿을 수 있는 코드’로 바꾸는 가장 현실적인 출발점입니다.
- 대상은 최근에 실제로 발생한 버그 하나로 제한합니다.
- 테스트 이름에는 당시 조건과 잘못된 결과를 구체적으로 적습니다.
- 수정 전 실패를 확인하지 못했다면 테스트 조건부터 다시 검토합니다.
- 재현 테스트가 준비된 뒤에만 구현 코드를 고칩니다.

- 이전글AI 코딩 도구로 웹개발 한 달 해봤더니 반복된 실수들 26.08.29
- 다음글도커 개발환경, 팀 프로젝트에서 직접 써본 장단점 26.08.27
등록된 댓글이 없습니다.
