경계값 분석으로 입력 검증 결함 줄이기

경계값 분석(BVA)의 경계 식별, 다변량 조합, 오라클 설계와 자동화 방법을 입력 검증·API·시간 조건 사례로 정리한다.

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

오류가 몰리는 입력의 끝점

경계값 분석(Boundary Value Analysis, BVA)은 입력 도메인의 경계 지점을 중심으로 테스트 케이스를 구성하는 기법이다. 오프바이원(off-by-one) 오류와 유효성 검증 결함을 적은 비용으로 찾아내기 위해 쓰며, 등가 분할(Equivalence Partitioning)과 함께 테스트 설계의 표준 기법으로 활용된다.

입력 범위의 최소값과 최대값 부근에서 오류가 자주 발생한다는 경험 법칙이 출발점이다. 각 등가 클래스에서 경계값을 골라, 적은 케이스로 많은 결함을 검출하는 것이 목적이다.

경계는 숫자 범위에만 존재하지 않는다. 문자열 길이와 바이트 길이, 날짜와 시간, 컬렉션 크기와 페이지 경계, 타임아웃·쿼터 같은 외부 시스템의 제한도 모두 테스트 대상이 된다.

표준 BVA는 유효값인 [min, min+Δ, nominal, max-Δ, max]를 중심으로 검증한다. 강건(robust) BVA는 여기에 무효값 [min-Δ, max+Δ]를 더한다. 여러 입력값이 함께 작동하는 경우에는 Each-Choice, Pairwise, Worst-Case 방식으로 경계를 조합해 상호작용 결함을 찾는다.

명세와 구현 사이에서 경계를 찾는다

경계는 명세만 보고 결정하면 부족하다. 자료형, DB 제약, 외부 API 제한처럼 구현에서 생기는 경계도 함께 확인해야 한다. 폐구간인지 개구간인지, 단위와 스텝(증분)이 무엇인지도 분명히 정해야 한다.

정수와 실수의 차이, 문자열 길이와 바이트 길이의 차이, 시간대와 캘린더 전환이 만드는 해상도 역시 경계 설계에 영향을 준다. 테스트 값의 간격인 Δ는 최소 증분으로 정의한다.

각 테스트에는 유효·무효 입력의 기대 결과를 연결한다. 오류 코드와 메시지, HTTP 상태, 예외 타입뿐 아니라 실패 시 시스템 상태가 보존되는지, 롤백되는지, 메시지 국제화와 로깅 레벨·민감정보 마스킹 정책이 지켜지는지도 오라클에 포함한다. 경계를 통과하거나 차단할 때의 상태 전이와 감사 로그까지 검증 범위로 본다.

비기능 조건도 같은 방식으로 다룬다. 파일 크기와 메모리 한계, API 레이트 리밋, 데이터베이스 커넥션 풀의 경계, 타임아웃과 재시도 정책의 전환점을 테스트한다.

입력 형태별로 달라지는 경계

폼과 입력 검증에서는 나이 [0..120], 비밀번호 길이 [8..64], 통화 금액의 [10원 단위] 같은 범위를 확인한다. UTF-8 멀티바이트 문자열의 바이트 길이 제한과 DB 컬럼 길이가 어긋나는 경우도 경계 결함으로 이어질 수 있다.

API와 파일 처리에서는 업로드 파일 크기 [<= 10MB], Content-Length, 청크 업로드의 마지막 청크가 주요 경계다. 페이지네이션의 limit·offset, rate limit 잔여 횟수가 1→0으로 바뀌는 지점도 확인한다.

날짜와 시간은 월말, 윤년, 서머타임 전환 시각에서 경계가 생긴다. 시간대 변환 과정에서 경계가 손실되지 않는지도 검증해야 한다. 임베디드·IoT 환경에서는 센서 트리거의 하향·상향 임계값과 ADC 해상도에 따른 Δ 정의가 중요하다.

경계 테스트 세트를 만드는 흐름

테스트 설계는 명세·요구사항, 데이터 스키마와 제약, 외부 API 한계, 운영 지표(SLO/쿼터)에서 시작한다. 변수와 도메인을 식별하고 범위와 스텝을 정한 뒤, min, min+Δ, nominal, max-Δ, max와 필요할 경우 min-Δ, max+Δ를 후보로 뽑는다.

그 다음 폐·개구간, 반올림 규칙, 지역화와 단위 변환을 정하고 조합 전략을 선택한다. 기대 결과, 상태 전이, 오류 코드와 메시지를 오라클로 정의한 뒤 자동화 스크립트와 데이터 픽스처를 실행한다. 결과를 분석해 경계를 다시 조정하고, 테스트 세트·결함 목록·커버리지 리포트·경계 카탈로그로 남긴다.

아니오입력 도메인 수집(명세, 스키마,외부 한계)변수/범위/스텝정의(폐/개구간, 단위)경계 후보 도출(min, min+Δ,nominal, max-Δ, max,min-Δ, max+Δ)조합 전략 선택(Each-Choice,Pairwise, Worst-Case)오라클/에러 처리 정의 정확성검증?테스트 데이터/스크립트 생성 실행결과 분석 경계 재보정리포팅 경계 카탈로그업데이트

경계 케이스를 자동으로 생성하는 코드

환경은 Python 3.10+이며 표준 라이브러리만 사용한다. 정수·실수 범위와 증분 Δ, 포함 여부(inclusive)를 전제로 한다.

from dataclasses import dataclass
from itertools import product
from typing import List, Tuple, Optional, Union

Number = Union[int, float]

@dataclass
class Domain:
    name: str
    min_val: Number
    max_val: Number
    delta: Number = 1
    inclusive: Tuple[bool, bool] = (True, True)
    robust: bool = True
    nominal: Optional[Number] = None  # None이면 중앙값 계산

    def boundaries(self) -> List[Number]:
        mn, mx, d = self.min_val, self.max_val, self.delta
        incl_min, incl_max = self.inclusive

        def add(x: Number) -> Number:
            return type(d)(x + d)

        def sub(x: Number) -> Number:
            return type(d)(x - d)

        values = []
        if self.robust and incl_min:
            values.append(sub(mn))        # min - Δ
        if incl_min:
            values.append(mn)             # min
            values.append(add(mn))        # min + Δ
        else:
            values.append(add(mn))        # (min, min+Δ] 등
        # nominal
        if self.nominal is not None:
            values.append(self.nominal)
        else:
            # 중앙값 근사
            values.append(type(d)((mn + mx) / 2))
        # 상한
        if self.inclusive[1]:
            values.append(sub(mx))        # max - Δ
            values.append(mx)             # max
        else:
            values.append(sub(mx))        # [max-Δ, max) 등
        if self.robust and self.inclusive[1]:
            values.append(add(mx))        # max + Δ
        # 중복 제거 + 안정 정렬
        seen, out = set(), []
        for v in values:
            if v not in seen:
                seen.add(v)
                out.append(v)
        return out

def each_choice(domains: List[Domain]) -> List[dict]:
    # 각 도메인에서 첫 번째 경계값을 선택해 한 번씩 모두 포함
    max_len = max(len(d.boundaries()) for d in domains)
    result = []
    for i in range(max_len):
        case = {}
        for d in domains:
            vals = d.boundaries()
            case[d.name] = vals[min(i, len(vals)-1)]
        result.append(case)
    return result

def worst_case(domains: List[Domain]) -> List[dict]:
    lists = [d.boundaries() for d in domains]
    names = [d.name for d in domains]
    return [dict(zip(names, combo)) for combo in product(*lists)]

# 예시 사용
if __name__ == "__main__":
    age = Domain(name="age", min_val=0, max_val=120, delta=1, inclusive=(True, True), robust=True, nominal=30)
    amount = Domain(name="amount", min_val=0, max_val=100000, delta=100, inclusive=(True, True), robust=True, nominal=50000)

    print("== Each-Choice ==")
    for c in each_choice([age, amount]):
        print(c)

    print("\n== Worst-Case (부분만 출력) ==")
    wc = worst_case([age, amount])
    for c in wc[:10]:
        print(c)
    print(f"... 총 케이스 수: {len(wc)}")

Pairwise 조합에는 pairwise 라이브러리나 IPOG 알고리즘 사용을 권장한다. 입력→API 호출→응답/DB 상태 검증을 함수로 래핑하면 오라클 평가도 자동화할 수 있다.

검출 범위와 테스트 비용의 균형

경계값 분석은 입력 검증·경계 조건 결함의 7090% 범주를 포착할 수 있다. 무작위·임의 조합과 비교하면 케이스 수를 3060% 절감할 수 있으며, 경계 카탈로그를 재사용하면 변경 영향 분석 속도와 회귀 테스트의 신뢰성을 높일 수 있다. 초기 결함을 일찍 발견해 수정 비용 곡선을 완화하는 효과도 기대할 수 있다.

다만 모든 입력 경계를 조합하는 방식은 빠르게 커진다. n변수×k경계는 k^n으로 증가하므로 실행 비용이 높아지며, Pairwise가 현실적인 절충안이 될 수 있다. 강건 BVA 역시 무효값을 추가하는 만큼 케이스가 늘어나므로 위험도를 기준으로 적용 범위를 정한다.

자료형의 32-bit/64-bit 한계, DB 컬럼 길이, 외부 API의 쿼터·타임아웃처럼 숨은 경계를 포함해야 한다. 부동소수점 오차와 반올림 규칙, 시간대 변환과 DST 조건도 명시한다. 문자 수와 바이트 수의 불일치는 UI·로케일별 케이스를 복잡하게 만들기 때문에 환경 매트릭스 관리가 필요하다.

기법 케이스 수(상대) 경계 결함 탐지 성향 일관성/재현성 운영 편의
표준 BVA 낮음 중간 높음 높음
강건 BVA 중간 높음 높음 중간
Pairwise BVA 중간 중~높음(쌍 상호작용) 중간 중간
Worst-Case BVA 높음 매우 높음 높음 낮음

정확한 경계 식별과 Δ 정의, 유효·무효 입력의 오라클 명세가 BVA의 기반이다. 다변량 입력에서는 위험도에 따라 조합 전략을 선택하고, Pairwise를 활용해 비용과 탐지력을 조율한다. 자동화 스크립트와 경계 카탈로그는 테스트 자산을 반복해서 사용할 수 있게 한다.

경계값 분석소프트웨어 테스트입력 검증테스트 설계조합 테스트