버그를 하나 고쳤는데 멀쩡하던 다른 기능이 깨진 경험이 있을 것이다. 아니면 몇 달 전에 짠 함수를 다시 열었는데, 이 함수가 빈 리스트를 받으면 어떻게 동작해야 하는지 나 자신도 기억나지 않아 코드를 처음부터 다시 읽은 경험도 있을 것이다. 소프트웨어 테스트는 정확히 이 두 상황을 겨냥한다. 테스트(test) 란 “코드가 의도대로 동작하는지 확인하는 또 다른 코드”이고, 한 번 짜 두면 사람 대신 기계가 몇 초 만에 수백 번이고 그 확인을 반복해 준다.
이 글은 소프트웨어 테스팅 시리즈의 첫 편이자 길잡이다. 테스트가 왜 필요한지 — 특히 “테스트는 시간 낭비”라는 흔한 인식이 왜 틀렸는지 — 를 정리하고, 시리즈 전체의 지도를 그린다.
테스트의 세 가지 역할
테스트가 주는 가치는 “버그를 잡는다” 한마디로 뭉뚱그리기엔 결이 다른 세 가지로 나뉜다.
1. Regression 방지 — 고장 나면 즉시 알람이 울리는 안전망
regression 이란 잘 동작하던 기능이 이후의 코드 변경 때문에 다시 망가지는 현상을 말한다(“회귀 버그”라고도 부른다). 소프트웨어에서 버그의 상당수는 새 기능을 처음 만들 때가 아니라 기존 코드를 고칠 때 생긴다. 코드는 서로 얽혀 있어서, A를 고치면 A를 쓰던 B가 소리 없이 깨질 수 있기 때문이다.
간단한 예로, 작업(lot) 목록을 우선순위 규칙으로 정렬하는 dispatching 함수가 있다고 하자.
def sort_by_priority(lots):
"""납기(due_date)가 빠른 순, 같으면 대기시간이 긴 순."""
return sorted(lots, key=lambda l: (l.due_date, -l.waiting_time))
def test_earlier_due_date_comes_first():
a = Lot(due_date=3, waiting_time=10)
b = Lot(due_date=1, waiting_time=0)
assert sort_by_priority([a, b]) == [b, a]
def test_tie_broken_by_longer_waiting_time():
a = Lot(due_date=3, waiting_time=10)
b = Lot(due_date=3, waiting_time=99)
assert sort_by_priority([a, b]) == [b, a]몇 달 뒤 누군가 “긴급 lot 우선” 조건을 추가하다가 실수로 대기시간 부호를 빼먹으면, 두 번째 테스트가 그 자리에서 빨간불을 켠다. 테스트가 없다면? 이 버그는 코드 리뷰를 통과하고, 배포되고, 몇 주 뒤 현장에서 “왜 오래 기다린 lot이 계속 밀리죠?”라는 문의로 돌아온다. 버그는 발견이 늦어질수록 고치는 비용이 가파르게 커진다 — 원인 추적에 드는 시간, 이미 잘못 처리된 데이터, 잃어버린 신뢰까지. regression 테스트는 그 발견 시점을 “몇 주 뒤 현장”에서 “저장 직후 몇 초”로 당겨 준다.
2. 설계 피드백 — 테스트하기 어려운 코드는 나쁜 설계라는 신호
덜 알려져 있지만 실무에서 체감이 큰 역할이다. 어떤 함수에 테스트를 붙이려는데 유난히 힘들다면 — DB를 띄워야 하고, 전역 변수를 세팅해야 하고, 현재 시각에 따라 결과가 달라진다면 — 그것은 테스트 도구의 문제가 아니라 그 함수가 너무 많은 것에 암묵적으로 의존하고 있다는 설계 신호다.
예컨대 calculate_deadline()이 내부에서 datetime.now()를 직접 호출하면, 이 함수는 실행하는 순간의 시각과 결합되어 같은 입력에도 매번 다른 답을 낸다. 테스트를 붙이려는 순간 이 결합이 드러나고, “현재 시각을 인자로 받게 바꾸자”는 개선으로 이어진다. 그 결과 함수는 테스트하기 쉬워질 뿐 아니라 재사용하기도, 이해하기도 쉬워진다. 테스트를 쓰는 행위 자체가 설계를 검사하는 리트머스 시험지 역할을 하는 것이다. 이 관계는 4편(행동 vs 구현)과 6편(테스트 가능한 설계)에서 본격적으로 다룬다.
3. 실행 가능한 문서 — 거짓말하지 않는 스펙
주석과 문서의 고질병은 코드가 바뀌어도 문서는 안 바뀐다는 것이다. 낡은 문서는 없느니만 못하다. 반면 테스트는 executable documentation, 즉 실행되는 문서다. 코드의 실제 동작과 어긋나는 순간 실패하므로, 통과하는 테스트는 항상 현재 동작에 대한 참인 서술이다.
def test_empty_lot_list_returns_empty_schedule():
assert build_schedule([]) == Schedule.empty()
def test_lot_exceeding_machine_capacity_is_rejected():
lot = Lot(size=999)
machine = Machine(capacity=100)
with pytest.raises(CapacityExceededError):
assign(lot, machine)이 두 테스트는 그 자체로 “빈 입력이면 빈 스케줄을 반환한다”, “용량 초과 lot은 예외를 던진다”라는 스펙 문장이다. 새로 합류한 동료(혹은 몇 달 뒤의 나)는 구현 코드를 파헤치는 대신 테스트 목록을 훑는 것만으로 이 모듈이 지켜야 할 계약을 파악할 수 있다.
”테스트는 비용”이라는 오해
그럼에도 현장에서 가장 자주 듣는 반론은 이것이다. “테스트 짤 시간에 기능을 하나 더 만드는 게 낫지 않나?” 이 말은 소프트웨어를 한 번 만들고 끝나는 것으로 볼 때만 성립한다. 일회성 스크립트라면 맞는 말이다. 하지만 살아 있는 소프트웨어는 수명의 대부분을 변경되면서 보낸다 — 요구사항이 바뀌고, 버그가 고쳐지고, 기능이 얹힌다.
테스트가 없는 코드베이스에서 변경 비용이 어떻게 불어나는지 보자.
[테스트 없음] [테스트 있음]
코드 수정 (10분) 코드 수정 (10분)
↓ ↓
영향 범위를 눈으로 추적 (1시간) 테스트 실행 (1분)
↓ ↓
관련 기능 수동 확인 (1시간) 빨간불 난 곳만 확인
↓ ↓
"아마 괜찮겠지" 하고 배포 초록불 확인 후 배포
↓
운 나쁘면 장애 → 원인 추적 (며칠)
핵심은 왼쪽 경로의 비용이 코드베이스가 커질수록 점점 나빠진다는 점이다. 코드가 얽힐수록 “이 수정이 어디까지 영향을 주나”를 사람이 추적하는 시간은 늘어나고, 어느 순간 개발자들은 깨질까 봐 무서워서 코드를 안 건드리기 시작한다. 개선이 멈추고, 낡은 코드 위에 우회 코드가 쌓이고, 코드베이스는 더 무서워진다 — 악순환이다. 반대로 테스트가 있으면 변경 후 확인 비용이 코드베이스 크기와 거의 무관하게 “테스트 한 번 돌리는 시간”으로 고정된다. 그래서 테스트는 지금 지출하는 비용이 아니라 미래의 모든 변경 비용을 낮추는 투자다. 테스트 작성에 드는 시간은 첫 변경 몇 번 만에 회수된다.
시뮬레이션 분야의 verification & validation(모델이 의도대로 구현되었는지 / 현실을 잘 반영하는지 검증)과도 정신이 닿아 있다 — “만들었다”와 “믿을 수 있다”는 별개의 문제이며, 믿음은 체계적 검증 절차에서 나온다는 것. 시뮬레이션 쪽 관점은 12-verification-and-validation에서 다룬 바 있다.
AI 코딩 에이전트 시대: 테스트의 지위가 달라진다
이 시리즈를 지금 쓰는 이유이기도 하다. Claude Code 같은 코딩 에이전트가 코드를 대신 짜는 비중이 커지면서, 사람이 코드를 한 줄씩 읽고 검증하는 비중은 필연적으로 줄어든다. 그때 테스트는 두 가지 의미에서 지위가 격상된다.
- 사람 → 에이전트 방향의 스펙: “이 테스트들을 통과하게 만들어라”는 자연어 지시보다 훨씬 모호함이 적은 요구사항 전달 수단이다.
- 에이전트 → 사람 방향의 안전망: 사람이 생성된 코드를 전부 읽지 않는다면, 그 코드가 맞다는 근거는 사실상 테스트 통과 여부뿐이다. 에이전트 스스로도 테스트를 돌려 자기 결과를 검증하는 피드백 루프로 쓴다.
물론 여기엔 “에이전트가 짠 테스트를 믿을 수 있나”, “테스트만 통과시키는 꼼수는 어떻게 막나” 같은 새로운 문제가 따라온다. 이 주제는 14편에서 본격적으로 다루고, 여기서는 동기만 기억해 두자: 코드를 덜 읽는 시대일수록, 테스트가 곧 스펙이자 마지막 안전망이 된다.
시리즈 로드맵
이 시리즈는 6부, 총 14편으로 구성된다. 특정 도구나 도메인에 매이지 않는 원칙 중심으로 가되, 예제는 Python(pytest)을 기본으로 쓴다.
1부 테스트란 무엇인가
01 왜 테스트를 쓰는가 (이 글)
02 테스트의 분류: unit / integration / E2E
2부 좋은 단위 테스트 ← 시리즈의 핵심
03 단위 테스트의 구조와 규율 (AAA, FIRST, pytest)
04 무엇을 테스트할 것인가: 행동 vs 구현
05 Test double: mock, stub, fake 구분하기
06 테스트 가능한 설계 (Design for Testability)
3부 방법론
07 TDD: 테스트가 먼저인 개발
08 Property-based testing
09 Coverage와 mutation testing: 테스트를 테스트하기
4부 시스템 수준
10 Integration·E2E 테스트 실전
11 CI에서의 테스트: 자동화된 안전망
5부 어려운 대상들
12 레거시 코드에 테스트 붙이기
13 비결정적 코드 테스트: 시간, 난수, 동시성
6부 AI 시대의 테스트
14 코딩 에이전트와 테스트: 스펙이자 검증 루프
흐름은 이렇다. 1부에서 테스트의 종류와 자리를 잡고, 2부에서 이 시리즈의 중심인 “좋은 단위 테스트란 무엇인가”를 파고든다 — 무엇을 검증하고(04), 의존성은 어떻게 끊고(05), 애초에 테스트하기 좋은 코드는 어떻게 생겼는지(06). 3부는 테스트를 언제, 얼마나, 어떻게 쓸지에 대한 방법론이고, 4부는 단위를 넘어 시스템 전체와 CI 파이프라인으로 시야를 넓힌다. 5부는 현실에서 가장 자주 부딪히는 어려운 대상 — 테스트 없는 레거시, 시간·난수에 의존하는 코드 — 를 다루고, 마지막 6부에서 처음의 동기였던 AI 에이전트 시대의 테스트로 돌아온다.
요약
- 테스트는 세 가지 가치를 준다: regression 방지(변경으로 인한 파손을 몇 초 만에 탐지), 설계 피드백(테스트하기 어려운 코드 = 결합이 심한 코드라는 신호), 실행 가능한 문서(코드와 어긋나면 실패하므로 거짓말하지 않는 스펙).
- “테스트는 비용”이라는 인식은 소프트웨어를 일회성으로 볼 때만 맞다. 살아 있는 코드베이스에서 테스트는 모든 미래 변경의 비용을 낮추는 투자이며, 테스트 없는 코드는 “무서워서 못 고치는 코드”로 굳어 간다.
- AI 코딩 에이전트 시대에 사람이 코드를 덜 읽을수록, 테스트는 의도를 전달하는 스펙이자 결과를 믿을 수 있게 하는 유일한 안전망이 된다 (상세는 14편).
- 다음 글(02)에서는 unit / integration / E2E라는 테스트의 층위와, 각 층에 얼마나 투자할지에 대한 test pyramid 논쟁을 다룬다.