테스트를 많이 쓸수록 웹개발 버그가 늘어나는 이유

profile_image
작성자 품질 엔지니어 차유온
댓글 0건 조회 38회

테스트 코드가 500개나 통과했는데 배포 직후 결제 버튼이 멈췄다면 무엇이 잘못된 걸까요? 많은 개발팀이 테스트 개수를 품질 점수처럼 관리하지만, 잘못 작성한 테스트는 버그를 막기는커녕 잘못된 확신을 키웁니다. 특히 일정에 쫓기는 웹개발 프로젝트에서는 실행 결과가 초록색이라는 사실만 확인하고 테스트가 실제 사용자 행동을 검증하는지는 놓치기 쉽습니다.

코딩의 기본 개념은 명령을 컴퓨터가 처리할 수 있는 형태로 표현하는 데서 출발합니다. 테스트 역시 입력과 기대 결과를 코드로 표현하는 작업이므로, 무엇을 기대하는지 잘못 정의하면 정확하게 실행되어도 쓸모없는 결과를 냅니다. 아래에서는 테스트가 실패한 사례보다 더 위험한 통과했지만 아무것도 보장하지 못한 사례를 중심으로 살펴봅니다.

통과 숫자에 취하면 사용자 시나리오가 사라집니다

구현을 그대로 복사한 테스트는 하지 마세요

할인 금액을 계산하는 함수가 있다고 가정해 보겠습니다. 운영 코드는 상품 가격에 할인율을 곱하고, 테스트 코드도 같은 수식을 한 번 더 적어 예상값을 만듭니다. 두 코드에 동일한 반올림 오류가 들어가면 테스트는 통과하지만 고객에게는 1원 단위 오차가 계속 노출됩니다. 테스트는 구현 방식을 복제하는 문서가 아니라 외부에서 관찰할 수 있는 요구사항을 검증해야 합니다.

실무에서 자주 벌어지는 실패는 내부 함수 호출 횟수나 비공개 메서드의 반환값을 지나치게 확인하는 것입니다. 리팩터링으로 함수 이름만 바꾸어도 수십 개의 테스트가 깨지지만, 정작 ‘쿠폰 적용 후 최종 결제액이 맞는가’라는 핵심 행동은 검사하지 않습니다. 이런 테스트는 변경 비용을 높여 개발자가 리팩터링을 피하게 만들고, 오래된 구조가 남아 새로운 버그의 토양이 됩니다.

  • 피해야 할 실수: 운영 코드의 계산식을 테스트에 그대로 복사합니다.
  • 피해야 할 실수: 화면에 드러나지 않는 내부 구현 순서까지 고정합니다.
  • 바꿔야 할 기준: 입력, 사용자 행동, 최종 출력처럼 공개된 계약을 검증합니다.
  • 좋은 질문: 이 테스트가 실패하면 어떤 사용자 피해를 발견한 것인지 한 문장으로 설명할 수 있습니까?

커버리지 100%가 안전을 뜻한다고 믿지 마세요

코드 커버리지는 실행된 줄을 보여줄 뿐, 결과가 올바른지 알려주지 않습니다. 로그인 함수의 모든 줄을 실행하면서도 잘못된 비밀번호가 거절되는지 단 한 번도 단언하지 않을 수 있습니다. 숫자를 올리기 위해 의미 없는 호출을 추가하면 대시보드는 좋아 보이지만 회귀 버그 탐지력은 거의 늘지 않습니다.

나쁜 측정 기준발생하는 착시대신 확인할 기준
전체 커버리지 비율모든 기능이 검증됐다고 오해결제·인증 등 위험 경로의 분기 검증
테스트 개수개수가 많으면 촘촘하다고 판단요구사항별 정상·실패 사례 존재 여부
실행 성공률항상 통과하면 안정적이라고 판단의도적으로 코드를 망가뜨렸을 때 실패하는지 확인
실무 팁: 테스트의 가치는 평소에 얼마나 자주 통과하는지가 아니라, 잘못된 변경을 얼마나 정확하고 빠르게 멈추는지로 평가해야 합니다.

가짜 환경을 늘리면 실제 장애 원인이 숨습니다

모든 의존성을 모킹하는 습관을 버리세요

외부 API, 데이터베이스, 시간, 파일 시스템을 전부 모킹하면 단위 테스트는 빠르고 안정적으로 보입니다. 문제는 모의 객체가 실제 시스템과 다른 응답을 돌려줄 때 시작됩니다. 배송 API는 운영 환경에서 상태 코드를 문자열로 보내는데 테스트용 객체는 숫자로 반환하거나, 데이터베이스는 제약 조건 위반을 일으키는데 저장소 모형은 어떤 값이든 받아들이는 식입니다. 테스트 속도를 얻은 대신 통합 지점의 위험을 통째로 삭제한 셈입니다.

한 쇼핑몰 팀은 주문 서비스 테스트에서 결제 모듈이 언제나 성공하도록 설정했습니다. 이후 실제 결제사가 응답 제한 시간을 초과하자 재시도 요청이 중복 승인으로 이어졌지만, 테스트에는 실패·지연·응답 유실 시나리오가 없었습니다. 호출 여부만 검사했기 때문에 금액과 주문 식별자가 재시도 과정에서도 동일하게 유지되는지도 확인하지 못했습니다. 이 사례의 교훈은 모킹을 없애라는 것이 아니라, 계약이 깨질 가능성이 큰 경계에는 실제 형식과 제약을 반영한 통합 테스트가 필요하다는 것입니다.

  1. 순수 계산이나 문자열 변환은 빠른 단위 테스트로 검증합니다.
  2. 데이터베이스 쿼리는 실제 엔진과 같은 종류의 테스트 환경에서 제약 조건까지 확인합니다.
  3. 외부 API에는 계약 테스트를 두고 필수 필드, 자료형, 오류 응답을 검증합니다.
  4. 브라우저 자동화는 회원가입, 로그인, 구매처럼 사업에 중요한 흐름에 한정합니다.
  5. 네트워크 지연과 429·500 응답, 연결 중단을 별도 실패 사례로 추가합니다.

시간과 비동기 처리를 운에 맡기지 마세요

테스트가 가끔 실패하면 개발자는 원인을 찾기보다 재실행 버튼부터 누르게 됩니다. 고정되지 않은 현재 시각, 실행 순서에 의존하는 공유 상태, 무조건 2초를 기다리는 코드가 대표적인 원인입니다. 이런 불안정한 테스트가 쌓이면 실제 결함도 ‘또 테스트가 흔들렸겠지’라고 넘기게 됩니다. 결국 경보 장치가 너무 자주 오작동해 화재 경보까지 무시하는 상황과 같습니다.

프로그래밍의 의미와 처리 과정을 보면 프로그램은 정해진 절차와 조건을 기반으로 동작합니다. 테스트도 같은 입력에는 같은 결과를 내도록 통제해야 합니다. 현재 시각은 주입 가능한 시계 객체로 바꾸고, 무작정 기다리는 대신 특정 상태가 될 때까지 제한 시간 안에서 관찰해야 합니다. 테스트마다 독립된 데이터를 만들고 종료 후 정리하면 실행 순서를 바꿔도 같은 결과를 얻을 수 있습니다.

  • 하지 말아야 할 것: 비동기 처리가 끝날 것이라 추측해 고정 시간만큼 대기합니다.
  • 하지 말아야 할 것: 여러 테스트가 같은 사용자 계정과 장바구니를 공유합니다.
  • 권장 방식: 완료 이벤트나 화면 상태를 조건으로 기다리고 최대 대기 시간을 둡니다.
  • 권장 방식: 날짜, 난수, 네트워크 응답을 테스트에서 명시적으로 제어합니다.
  • 격리 확인: 전체 실행뿐 아니라 각 테스트를 단독 실행하고 순서를 무작위로 바꿔 봅니다.
한 번 재실행해서 통과한 테스트를 그대로 두지 마세요. 재현 빈도가 낮다는 것은 문제가 없다는 뜻이 아니라, 원인을 관찰하기 더 어렵다는 뜻입니다.

장바구니 할인 오류를 막아낸 한 번의 테스트 개편

실패한 배포를 사용자 행동부터 다시 추적했습니다

의류 쇼핑몰의 장바구니에 ‘5만원 이상 구매하면 배송비 무료’ 기능이 추가됐다고 해보겠습니다. 개발자는 배송비 계산 함수에 단위 테스트 18개를 작성했고 모두 통과했습니다. 하지만 배포 당일, 6만원짜리 상품에 20% 쿠폰을 적용한 고객에게 무료 배송이 표시됐다가 결제 화면에서 배송비 3천원이 다시 붙었습니다. 장바구니 화면은 할인 전 금액을 기준으로 판단했고 결제 서버는 할인 후 금액인 4만8천원을 기준으로 판단한 것이 원인이었습니다.

기존 테스트는 배송비 함수에 숫자를 직접 넣었을 뿐, 상품 추가부터 쿠폰 적용과 결제 요청까지 이어지는 흐름을 검증하지 않았습니다. 더구나 프런트엔드 테스트는 서버 응답을 항상 무료 배송으로 모킹했습니다. 함수 하나하나는 요구받은 값을 계산했지만 시스템 전체에는 ‘무료 배송 판단 기준이 할인 전 금액인가, 할인 후 금액인가’라는 공통 계약이 없었습니다. 테스트 18개가 실패하지 않은 이유는 코드가 옳아서가 아니라 잘못된 경계를 한 번도 건너지 않았기 때문입니다.

  1. 팀은 먼저 정책 담당자와 무료 배송 기준을 할인 후 상품 금액으로 확정했습니다.
  2. 4만9천원, 정확히 5만원, 5만1천원이라는 경계값을 테스트 데이터로 만들었습니다.
  3. 6만원 상품에 쿠폰을 적용해 4만8천원이 되는 회귀 사례를 별도로 고정했습니다.
  4. 프런트엔드 계산을 제거하고 서버가 반환한 배송비와 기준 금액을 표시하도록 책임을 한곳에 모았습니다.
  5. 실제 데이터베이스를 사용하는 통합 테스트에서 쿠폰, 주문, 배송비 레코드가 함께 저장되는지 검증했습니다.
  6. 브라우저 테스트로 상품 추가, 쿠폰 적용, 무료 배송 문구 변경, 최종 결제 금액까지 따라갔습니다.

개수 대신 실패 이유가 선명한 테스트를 남겼습니다

개편 과정에서 테스트 수는 18개에서 12개로 줄었습니다. 내부 함수 호출 횟수를 확인하던 테스트와 동일한 계산을 반복하던 테스트를 제거했기 때문입니다. 대신 정책 경계 테스트 5개, 데이터베이스 통합 테스트 3개, 핵심 구매 흐름 테스트 1개가 실제 위험을 담당했습니다. 테스트 이름도 ‘calculateShipping 호출’에서 ‘쿠폰 적용 후 금액이 5만원 미만이면 배송비가 부과된다’처럼 실패 원인이 바로 드러나도록 바꿨습니다.

팀은 배포 전 마지막 검증으로 배송비 기준을 고의로 할인 전 금액으로 되돌려 보았습니다. 새 회귀 테스트와 브라우저 테스트가 즉시 실패했고, 다른 정책 테스트는 그대로 통과했습니다. 잘못된 변경을 정확히 가리키면서 과도한 연쇄 실패도 만들지 않은 것입니다. 코딩 문법 자체를 더 살펴보고 싶다면 코딩 관련 지식백과 설명도 참고할 수 있지만, 이 사례에서 품질을 바꾼 핵심은 새로운 문법이 아니었습니다. 사용자가 실제로 밟는 경로와 사업 규칙의 경계를 테스트 대상으로 다시 선택한 판단이었습니다.

  • 테스트 이름만 읽어도 보호하려는 사용자 행동이 보여야 합니다.
  • 장애가 발생하면 같은 입력을 재현하는 회귀 테스트를 먼저 추가합니다.
  • 정책 기준값의 바로 아래, 정확한 값, 바로 위를 모두 검증합니다.
  • 중복 계산보다 프런트엔드와 서버 사이의 데이터 계약을 확인합니다.
  • 테스트를 고의로 실패시켜 실제로 결함을 탐지하는지 확인합니다.

테스트를 많이 쓸수록 웹개발 버그가 늘어나는 이유

댓글목록

등록된 댓글이 없습니다.