경계값분석으로 입력 검증 테스트 설계하기
경계값분석의 설계 원리와 Single Fault·Robustness·Worst Case 선택 기준, 입력 검증 자동화 방법을 정리한다.
2026-08-14 · 최초 발행 2025-12-15
결함이 몰리는 입력 경계를 먼저 모델링한다
경계값분석(Boundary Value Analysis)은 블랙박스·명세기반 테스트에서 입력 도메인의 경계 부근에 결함이 집중된다는 가정을 활용한다. 등가분할(Equivalence Partitioning)로 나눈 클래스의 경계를 기준으로, 적은 케이스로 Off-by-one 오류나 부등식 처리 오류를 겨냥하는 방식이다.
정수 범위가 [L, U]라면 일반적으로 L, L+1, U-1, U와 중간값(M)을 후보로 둔다. 강건성까지 확인하려면 L-1, U+1도 추가한다.
경계는 숫자 범위에만 있지 않다. <, ≤, ≥, >의 포함 여부, 길이 제한, 날짜와 시간의 포맷·타임존·서머타임도 경계 정의에 영향을 준다. 실수나 금액은 반올림·내림, 부동소수 정밀도, 최소 단위(예: 0.01)를 명세에 맞춰 분명히 해야 한다.
이 기법은 입력 차원이 서로 독립적이거나 상호작용이 약하다는 전제에서 특히 효율적이다. 시작일 ≤ 종료일처럼 변수 간 제약이 강하면 Worst Case나 조합·상태 기반 기법을 보완적으로 적용해야 한다.
테스트 설계는 명세에서 입력 도메인과 등가분할을 찾고, 경계 후보를 뽑은 뒤 전략을 고르는 순서로 진행된다. 이후 케이스와 오라클을 구성해 실행하고, 실패가 집중된 지점을 다시 경계 정의에 반영한다.
경계 집합과 오라클을 함께 설계한다
유효 경계에는 최소·최대값과 각각의 인접값, 전형값인 중간값이 들어간다. Robustness Testing은 여기에 최소-1, 최대+1처럼 명세를 벗어난 입력을 더해 강건성과 오류 처리를 확인한다.
테스트 케이스만 만들어서는 충분하지 않다. 비교 연산자의 의미와 함께 에러 코드, 메시지, HTTP 상태처럼 결과를 판정할 기준을 구체화해야 한다. 날짜·시간, 통화 단위, 로케일처럼 외부 조건에 따라 달라지는 경계도 명시 대상이다.
반복 사용되는 검증 규칙은 파라미터화 테스트나 데이터 드리븐 방식으로 구성할 수 있다. 경계 집합 생성기와 조합기 모듈을 분리하면 속성기반 테스트와도 결합하기 쉽다.
변수 상호작용에 맞춰 전략을 고른다
Single Fault Based Testing은 한 번에 하나의 변수만 경계값으로 바꾸고 나머지는 전형값에 고정한다. Robustness Testing은 이 방식에 범위 밖 값을 더한다. 모든 변수의 후보를 데카르트 곱으로 조합하는 방식은 Worst Case이며, 무효 경계까지 포함하면 Robust Worst Case가 된다.
| 기법 | 결함 검출력(경계) | 테스트 케이스 수(확장성) | 무효 값 포함(강건성) | 일관성/명세 의존성 | 운영 편의(작성 난이도) |
|---|---|---|---|---|---|
| Single Fault Based | 중 | 4n+1 수준(전형적) | 아니오 | 높음 | 높음(용이) |
| Robustness | 중~상 | 6n+1 수준(전형적) | 예 | 높음 | 중 |
| Worst Case | 상 | 5^n 수준(전형적) | 아니오 | 높음 | 중~하 |
| Robust Worst Case | 최상 | 7^n 수준(전형적) | 예 | 높음 | 하(복잡) |
n은 입력 변수 개수다. 표의 “전형적” 산정식은 각 변수의 경계 후보 수를 {유효: 4+전형값(=5), Robust: 6+전형값(=7)}로 가정한 경험적 값이며, 실제 케이스 수는 명세·타입·검증 규칙에 따라 달라진다.
고위험 변수에는 Worst Case 또는 Robust Worst Case를, 위험도가 낮은 변수에는 Single Fault 또는 Robustness를 적용하는 식으로 균형을 잡을 수 있다.
입력 검증에서 자주 만나는 경계
폼과 API에서는 나이 [0..120], 페이지네이션 page[1..], size[1..200], 금액 [0..1,000,000]처럼 범위가 직접 드러나는 필드가 대표적이다. Robustness로 음수, 0, 허용 최대 초과, 비정상 포맷을 넣으면 정상 경로뿐 아니라 오류 처리도 검증할 수 있다.
금융·정산 로직에서는 수수료 면제 임계치와 위험 한도 초과 판단의 =, >, ≥ 차이가 결함으로 이어질 수 있다. IoT·펌웨어에서는 센서 캘리브레이션 오차와 ADC 라운딩 경계에서 비정상 스위칭이나 알람을 점검한다.
날짜·기간 연산은 월말, 윤년, 서머타임, 포함·배제 경계에 따라 합계와 기간 계산이 바뀐다. 데이터 파이프라인도 문자열 길이, 정규식, nullability, 최대 레코드 크기를 경계값으로 다뤄야 한다.
단일 변수 기준으로는 전수 테스트 대신 4~7개 수준의 케이스로 줄이면서 Off-by-one·부등식 오류를 찾을 수 있다. 다변수에서는 Single Fault가 약 4n(+전형 1)이고, Worst Case는 5^n(또는 7^n)이므로 커버리지와 비용의 트레이드오프를 선택하게 된다. 경계 생성기와 조합기를 템플릿화하면 테스트 데이터 재사용과 자동화 파이프라인 통합도 수월해진다.
상태와 제약이 강한 경우의 보완 방법
경계값분석만으로는 단계형 워크플로우, 상태 전이, 세션 의존성이 있는 흐름을 충분히 다루기 어렵다. 입력 간 독립성 가정도 시작일 ≤ 종료일 같은 강한 상호 제약이 있는 경우에는 커버리지를 떨어뜨릴 수 있다.
다변수 상호작용은 Pairwise나 Orthogonal Array로 샘플링하고, 상태 변화는 State Transition·Decision Table·BDD 시나리오로 보강한다. 한도 상향·하향처럼 규정이 바뀌는 시점에는 해당 경계를 다시 검증해 회귀 결함을 일찍 찾는다.
경계 케이스 생성 코드
전제조건: Python 3.9+
아래 코드는 단일·다변수 경계값 집합을 만들고 Single Fault와 Worst Case를 구성한다.
from itertools import product
from typing import List, Tuple, Dict, Any
def boundary_set_int(low: int, high: int, nominal: int = None, robust: bool = False) -> List[int]:
assert low <= high
if nominal is None:
nominal = (low + high) // 2
values = {low, min(low + 1, high), max(high - 1, low), high, nominal}
if robust:
values.update({low - 1, high + 1})
# 정렬 후 반환
return sorted(values)
def single_fault_cases(specs: Dict[str, Tuple[int, int]], robust: bool = False) -> List[Dict[str, Any]]:
# 전형값(모든 변수 nominal) 1건 + 각 변수별 경계 교체
nominal_row = {}
boundary_map = {}
for k, (low, high) in specs.items():
nominal = (low + high) // 2
nominal_row[k] = nominal
boundary_map[k] = boundary_set_int(low, high, nominal, robust)
cases = [nominal_row.copy()]
for k in specs:
for v in boundary_map[k]:
if v == nominal_row[k]:
continue
row = nominal_row.copy()
row[k] = v
cases.append(row)
return cases
def worst_case_cases(specs: Dict[str, Tuple[int, int]], robust: bool = False) -> List[Dict[str, Any]]:
sets = []
keys = list(specs.keys())
for k in keys:
low, high = specs[k]
nominal = (low + high) // 2
sets.append(boundary_set_int(low, high, nominal, robust))
cases = []
for combo in product(*sets):
cases.append({k: v for k, v in zip(keys, combo)})
return cases
if __name__ == "__main__":
specs = {
"age": (0, 120),
"size": (1, 200),
}
sf = single_fault_cases(specs, robust=True)
wc = worst_case_cases(specs, robust=False)
print(f"Single Fault(robust=True) count = {len(sf)}")
print(sf[:8], "...")
print(f"Worst Case(robust=False) count = {len(wc)}")
print(wc[:3], "...")
Single Fault(robust=True)는 대략 6n+1 케이스(변수별 6개 경계 + 전형 1개)를 생성한다. Worst Case(robust=False)는 변수별 5개 경계의 데카르트 곱으로 5^n 케이스를 만든다.