단위 테스트로 로직 오류를 개발 단계에서 차단하는 법

단위 테스트의 격리·결정론성, 테스트 더블, CI 품질 게이트와 커버리지·변이 테스트 운영 방식을 정리한다.

2026-08-14 · 최초 발행 2025-12-17

작은 로직을 빠르게 검증하는 테스트

단위 테스트는 함수, 메서드, 클래스처럼 가장 작은 실행 단위를 독립적으로 검증하는 자동화 테스트다. 네트워크, 파일, DB, 시간, 난수처럼 결과를 흔들 수 있는 요소를 분리해 같은 입력에서 같은 결과가 나오도록 만든다. 로직 오류를 개발 단계에서 발견하고 변경 비용을 낮추기 위한 개발자 중심의 품질 활동이다.

테스트 대상의 범위는 언어와 아키텍처에 따라 달라진다. 함수형 코드에서는 함수, 객체지향 코드에서는 메서드나 클래스, 스크립트 기반 구조에서는 모듈까지 단위가 될 수 있다. 기준은 크기가 아니라 작고 격리된 상태에서 검증할 수 있는가에 있다.

외부 의존성은 mock, stub, fake, spy 같은 테스트 더블로 바꾼다. 부작용을 줄여 순수 함수에 가깝게 만들고 의존성 주입(DI)을 적용하면 테스트하기 쉬운 구조를 만들 수 있다.

테스트가 신뢰를 얻는 조건

외부 I/O, 시스템 시간, 글로벌 상태에 직접 의존하지 않아야 한다. 그래야 실행할 때마다 동일한 입력에 동일한 출력을 기대할 수 있다. 수 ms~수십 ms/테스트 수준의 빠른 실행 시간은 개발 루프 안에서 상시 피드백을 받는 기반이 된다.

테스트 본문은 Arrange-Act-Assert(AAA) 또는 Given-When-Then 방식으로 읽히게 구성한다. 하나의 테스트에는 하나의 행위와 하나의 주장을 두는 편이 실패 원인을 좁히기 좋다. 메서드명_시나리오_기대결과처럼 의도를 드러내는 이름은 실패 메시지만으로도 문제를 파악하게 해 준다.

테스트 더블은 역할이 다르다. Stub은 고정된 응답을 주고, Mock은 행위를 검증하며, Fake는 단순화한 구현을 제공하고, Spy는 호출 기록을 남긴다. 외부 계약을 명시적으로 확인할 수 있지만, 모킹이 과해지면 리팩터링에 취약해질 수 있다. 도메인 핵심 로직은 순수화해 모킹 자체를 줄이는 방향이 낫다.

경계값과 예외를 우선 검증하고, 데이터는 필요한 만큼만 준비한다. 테스트는 서로 독립적으로 실행되어야 하며 실행 순서에 결과가 좌우되어서는 안 된다. 조합이 많은 비즈니스 규칙은 표 기반 또는 파라미터화 테스트로 다뤄 조합 폭발을 억제한다.

개발 파이프라인 안에서의 위치

변경한 코드와 테스트 코드는 로컬에서 먼저 실행하고, 커밋과 PR 이후 CI에서 빌드·테스트·커버리지·선택적 변이 테스트를 수행한다. 품질 기준에는 커버리지 임계치와 변이 점수 목표가 포함될 수 있다. 실패하면 원인, 로그, 리포트를 제공하고, 플레이키 테스트가 감지되면 격리나 재시도 정책을 적용한다.

성공실패실패성공임계치 미달임계치 충족실패(취약 테스트)성공개발자 '코드/테스트' 변경로컬 '단위 테스트' 실행커밋/PR 생성수정 재실행CI '빌드/테스트' 단계'유닛 테스트' 실행'빌드 중단' 알림'커버리지 측정' 임계치 확인'변이 테스트(선택)' 실행'아티팩트 생성/병합' '피드백'게시

라인·분기 커버리지, 변이 점수, 테스트 실행 시간, 실패율과 플레이키(flaky) 비율은 함께 봐야 한다. 커버리지만 높아도 의미 없는 테스트가 늘어날 수 있고, 변이 테스트만으로는 빌드 시간이 길어질 수 있다.

검증 범위가 다른 단위 테스트와 통합 테스트

단위 테스트와 통합 테스트는 대체 관계가 아니다. 단위 테스트를 테스트 피라미드의 저변에 충분히 확보하고, 상위 레벨 테스트로 통합 위험을 보강하는 구조가 적합하다.

지표 단위 테스트 통합 테스트
성능(속도) 매우 빠름(밀리초 단위) 느림(초~분 단위)
확장성(병렬화) 높음(수천 케이스 병렬 가능) 중간(공유 자원 충돌 관리 필요)
일관성(결정론) 매우 높음(격리 전제) 중간(환경/데이터 영향)
안정성 높음(플레이키 낮음) 낮음~중간(환경 변화 민감)
운영 편의 높음(개발 루프 내 상시 실행) 중간(환경 프로비저닝 필요)

단위 테스트가 필요한 코드 경계

할인·세율·요율·요금제 계산, 승인 규칙, 정책 엔진처럼 도메인 규칙이 밀집한 코드는 단위 테스트의 대표적인 대상이다. 빈 컬렉션, null/None, 음수와 최대치, 시간대와 로캘, 파싱 실패 같은 경계·예외 처리도 빠뜨리기 쉽다.

레거시 코드에서는 특성화(Characterization) 테스트로 현재 동작을 먼저 고정한 뒤 단계적으로 리팩터링할 수 있다. 데이터 접근 레이어는 리포지토리 인터페이스를 모킹해 트랜잭션, 락, DB 상태에 대한 의존성을 제거한다.

Python과 Java에서의 예시

전제조건:

  • Python 3.11+, pytest 7.x+
  • Java 17+, JUnit 5.10+, Gradle/Maven

Python/pytest

# src/discount.py
def calc_discount(price: int, tier: str) -> int:
    if price < 0:
        raise ValueError("negative price")
    rates = {"NONE": 0.0, "SILVER": 0.05, "GOLD": 0.1}
    rate = rates.get(tier, 0.0)
    discounted = int(round(price * (1 - rate)))
    return max(discounted, 0)
# tests/test_discount.py
import pytest
from src.discount import calc_discount

def test_gold_tier_applies_10_percent():
    assert calc_discount(10000, "GOLD") == 9000

@pytest.mark.parametrize("price,tier,expected", [
    (0, "NONE", 0),
    (10001, "SILVER", 9501),
    (5000, "UNKNOWN", 5000),
])
def test_various_tiers(price, tier, expected):
    assert calc_discount(price, tier) == expected

def test_negative_price_raises():
    with pytest.raises(ValueError):
        calc_discount(-1, "GOLD")

Java/JUnit5

// src/main/java/com/example/Tax.java
package com.example;
public final class Tax {
    private Tax() {}
    public static int addVat(int net, double rate) {
        if (net < 0 || rate < 0) throw new IllegalArgumentException();
        return (int)Math.round(net * (1.0 + rate));
    }
}
// src/test/java/com/example/TaxTest.java
package com.example;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class TaxTest {
    @Test
    void addVat_rounds_properly() {
        assertEquals(110, Tax.addVat(100, 0.1));
        assertEquals(123, Tax.addVat(100, 0.23));
    }
    @Test
    void addVat_throws_on_invalid_input() {
        assertThrows(IllegalArgumentException.class, () -> Tax.addVat(-1, 0.1));
    }
}

로컬 실행은 < 2초, CI 유닛 단계는 < 3분을 목표로 둘 수 있다. 커버리지 임계치(라인 7080%, 분기 5070%)와 변이 점수(≥60%)는 점진적으로 높인다. 반복 실행, Seed 고정, 시간 고정(Clock abstraction)으로 플레이키를 탐지하고, 불안정한 테스트는 격리·수정한다.

과도한 모킹에는 도메인 순수화와 계약 기반 테스트를 병행한다. 커버리지 지표에만 매달려 의미 없는 테스트가 늘어나는 문제는 위험 기반 선택과 변이 테스트로 점검한다. 변이 테스트로 빌드 시간이 증가하면 변경 영향 범위만 증분 실행하거나 야간 빌드로 분리할 수 있다.

품질 가드레일이 만드는 변화

상위 레벨 결함은 30~60% 감소할 수 있으며, 재현과 분석 시간도 대폭 단축된다. 요구 단계에서 운영 단계로 갈수록 10x 증가하는 결함 수정 비용을 초기에 차단할 수 있다. 월 5건 회귀 결함 × 4시간/건 × 50% 저감은 ≈ 10시간/월 절감에 해당한다.

피드백 루프가 짧아지면 PR 체류 시간은 10~30% 감소할 수 있고, 빌드 신뢰도는 플레이키 < 0.5%를 목표로 관리할 수 있다. 커버리지와 변이 점수는 대규모 변경 시 리팩터링 위험을 줄이는 품질 가드레일로 작동한다.

단위 테스트소프트웨어 품질테스트 더블변이 테스트CI