동치 분할과 경계값
같은 규칙으로 처리될 입력끼리 묶는 동치 분할과, 영역이 바뀌는 끝값과 그 바로 바깥을 시험하는 경계값 분석으로 정원, 아이디, 참가자 수의 사례를 고릅니다. 조건의 조합은 결정 테이블로 정리하고, 고른 사례를 core.mjs로 실행해 확인합니다.
정원이 1부터 100까지라면 100가지를 모두 시험해 볼 수도 있지만, 정원의 상한이 1,000,000으로 바뀌면 같은 방법을 쓰기 어렵습니다. 그래서 입력을 규칙에 따라 몇 개의 영역으로 나누고, 그중에서도 실수가 잦은 곳을 골라 시험합니다. 이 글에서는 입력을 영역으로 나누는 동치 분할과 영역이 바뀌는 지점을 시험하는 경계값 분석으로 정원, 아이디, 참가자 수의 사례를 고르고, 조건이 여러 개 얽힌 경우는 결정 테이블로 정리합니다.
입력을 영역으로 나누고 끝값을 고른다
동치 분할(equivalence partitioning)은 프로그램이 같은 규칙으로 처리하리라고 예상되는 입력끼리 묶어 영역으로 나누는 방법입니다. 한 영역에서는 대표 값 하나를 골라 시험하는데, 이는 같은 영역의 값이라면 결과도 같으리라는 예상에 기댄 것이므로 대표 값 하나가 영역 안의 모든 결함을 찾아 준다고 할 수는 없습니다.
경계값 분석(boundary value analysis)은 유효한 영역과 유효하지 않은 영역이 만나는 곳에서, 영역의 끝값(boundary value)과 그 바로 바깥의 값을 고르는 방법입니다. <와 <=를 헷갈리거나 범위를 하나 어긋나게 적는 실수는 대개 이 지점에서만 드러나기 때문입니다. 두 방법은 함께 쓰며, 영역을 나눈 뒤 각 영역의 대표 값과 영역 사이의 끝값을 사례로 고릅니다.
정원 입력의 영역
이 교재의 정원은 1 이상 100 이하의 정수입니다. 그러므로 유효한 정수, 1보다 작은 정수, 100보다 큰 정수, 정수가 아닌 숫자, 숫자가 아닌 값을 서로 다른 영역으로 봅니다.
| 영역과 끝값 | 사례 | 기대하는 결과 |
|---|---|---|
| 하한 바로 아래와 하한 | 0, 1 | 거절, 허용 |
| 상한과 상한 바로 위 | 100, 101 | 허용, 거절 |
| 정수가 아닌 숫자 | 1.5 | 거절 |
| 숫자가 아닌 값 | "2", null | 거절 |
정원이라는 입력의 끝값과 참가자 수의 끝값은 다른 것입니다. 정원이 2인 모임에 참가자가 1명일 때는 새 신청을 받고 2명일 때는 거절하는지도 따로 시험해야 하며, 정원 0을 거절하는 테스트가 정원이 찬 모임의 신청 규칙까지 확인해 주지는 않습니다. 아래 그림은 정원 2인 모임에 참가자가 한 명씩 늘어 정원에 이르는 과정을 따라갑니다.
정원 입력의 범위와 신청 상태의 경계를 구분한다
모집은 열려 있고 참가자는 min뿐이다. jun은 새 ID지만 정원이 찼다. 각 단계의 값과 전환 이유를 비교합니다.
SOFTWARE ENGINEERING · READING CLUB
정원 입력의 범위와 신청 상태의 경계를 구분한다
생성 입력을 검증하는 숫자 영역과 실행 중 참가자 수가 바뀌는 상태 전이는 서로 다른 경계입니다.
상단의 1~100은 정원 입력 정책입니다. 하단의 1→2는 신청 상태 변화입니다. 같은 만석 상태라도 기존 ID의 중복 거절은 우선순위를 확인하는 별도 사례입니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 한 자리 남음부터 새 신청 거절까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 관계 설명 | 상단은 정원 입력 0 거절, 1부터 100까지 허용, 101 거절의 숫자 범위입니다. 하단은 정원 2의 열린 모임에서 참가자 1명에 hana를 등록해 2명이 된 뒤 새 jun 신청을 FULL로 거절하는 흐름입니다. 기존 min 신청은 중복으로 거절합니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 한 자리 남음 모집은 열려 있고 참가자는 min뿐이다. |
|
|
| 정원 도달 신규 유효 ID hana를 추가한다. |
|
|
| 새 신청 거절 jun은 새 ID지만 정원이 찼다. |
|
|
정원 입력의 범위와 신청 상태의 경계를 구분한다
모집은 열려 있고 참가자는 min뿐이다. jun은 새 ID지만 정원이 찼다. 각 단계의 값과 전환 이유를 비교합니다.
SOFTWARE ENGINEERING · READING CLUB
정원 입력의 범위와 신청 상태의 경계를 구분한다
생성 입력을 검증하는 숫자 영역과 실행 중 참가자 수가 바뀌는 상태 전이는 서로 다른 경계입니다.
상단의 1~100은 정원 입력 정책입니다. 하단의 1→2는 신청 상태 변화입니다. 같은 만석 상태라도 기존 ID의 중복 거절은 우선순위를 확인하는 별도 사례입니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 한 자리 남음부터 새 신청 거절까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 관계 설명 | 상단은 정원 입력 0 거절, 1부터 100까지 허용, 101 거절의 숫자 범위입니다. 하단은 정원 2의 열린 모임에서 참가자 1명에 hana를 등록해 2명이 된 뒤 새 jun 신청을 FULL로 거절하는 흐름입니다. 기존 min 신청은 중복으로 거절합니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 한 자리 남음 모집은 열려 있고 참가자는 min뿐이다. |
|
|
| 정원 도달 신규 유효 ID hana를 추가한다. |
|
|
| 새 신청 거절 jun은 새 ID지만 정원이 찼다. |
|
|
더 깊이: 끝값마다 두 개를 고를까, 세 개를 고를까
위의 표처럼 끝값 하나마다 그 값과 바로 옆 영역의 가장 가까운 값을 고르는 방식을 2값 경계값 분석이라고 부릅니다. 국제 소프트웨어 테스팅 자격 위원회(ISTQB)의 Foundation Level 강의 계획서 v4.0.1은 이와 함께, 끝값 하나마다 그 값과 양옆의 이웃까지 세 개를 고르는 3값 경계값 분석을 소개합니다. 정원의 상한이라면 2값 방식은 100과 101을, 3값 방식은 99, 100, 101을 고릅니다.
강의 계획서는 3값 방식이 2값 방식이 놓치는 결함을 찾을 수 있다고 예를 듭니다. x <= 10이어야 할 조건을 x == 10으로 잘못 구현했다면, 2값 방식이 고르는 10과 11에서는 올바른 구현과 결과가 같아 결함이 드러나지 않습니다. 3값 방식이 더하는 9에서야 올바른 구현은 참, 잘못된 구현은 거짓이 되어 차이가 납니다. 정원 검사처럼 하한과 상한의 두 비교를 묶은 조건이라면, 끝값 안쪽의 2와 99를 더해 이런 종류의 잘못까지 걸러 낼 수 있습니다. 강의 계획서는 두 방식 모두 커버리지를 고른 사례가 다룬 항목의 비율로 잰다고 정리합니다.
아이디 형식의 영역
아이디는 길이, 첫 글자, 나머지 글자를 각각 나누어 봅니다. 길이는 허용되는 끝값인 1자와 24자, 바로 바깥인 25자를 고르고, 첫 글자는 소문자, 숫자, 대문자로, 나머지 글자는 허용되는 하이픈과 허용되지 않는 대문자, 공백, 한글로 나눕니다.
이때 유효하지 않은 사례는 한 번에 한 가지 규칙만 어기도록 만듭니다. "1 Min"처럼 여러 규칙을 한꺼번에 어기는 값은 어느 검사에 걸려 거절되었는지 알 수 없고, 앞의 검사가 거절해 버리면 뒤의 검사에 결함이 있어도 드러나지 않습니다. 이 문제는 연습 문제 3에서 다시 봅니다.
조건의 조합은 결정 테이블로 정리한다
모집 상태, 이미 신청했는지, 남은 자리가 있는지처럼 여러 조건이 함께 결과를 정할 때는 값의 끝만 보아서는 사례를 고르기 어렵습니다. 이럴 때는 조건의 조합마다 결과를 적은 결정 테이블(decision table)을 만듭니다. 아이디가 유효하다고 가정하면 다음과 같습니다.
| 모집 상태 | 이미 신청함 | 남은 자리 | 결과 |
|---|---|---|---|
| CLOSED | 상관없음 | 상관없음 | CLOSED |
| OPEN | 예 | 상관없음 | ALREADY_REGISTERED |
| OPEN | 아니요 | 있음 | REGISTERED |
| OPEN | 아니요 | 없음 | FULL |
"상관없음"은 그 조건의 값과 관계없이 결과가 같다는 뜻입니다. 둘째 행의 남은 자리를 "없음"으로 둔 사례, 곧 정원이 찬 열린 모임에 이미 신청한 아이디로 다시 신청하는 사례는 두 거절 조건이 동시에 성립하므로, 어느 쪽이 먼저인지를 확인하는 중요한 사례가 됩니다.
고른 사례를 실행해 확인한다
다음 코드를 reading-lab/examples/ch5-2.mjs로 저장하고 node examples/ch5-2.mjs로 실행합니다. 정원, 아이디, 참가자 수의 세 부분으로 되어 있으며, 각 사례의 결과는 단언으로 확인한 뒤 출력합니다.
import assert from "node:assert/strict";
import { createSession, register } from "../core.mjs";
for (const capacity of [0, 1, 100, 101, 1.5, "2", null]) {
let actual;
try {
createSession({ id: "capacity", title: "정원 확인", capacity });
actual = "ACCEPT";
} catch (error) {
actual = error.name;
}
const expected = capacity === 1 || capacity === 100 ? "ACCEPT" : "RangeError";
assert.equal(actual, expected);
console.log("capacity", JSON.stringify(capacity), actual);
}
const idCases = [
["1자", "a", "REGISTERED"],
["24자", "a".repeat(24), "REGISTERED"],
["25자", "a".repeat(25), "INVALID_MEMBER"],
["숫자로 시작", "1min", "INVALID_MEMBER"],
["대문자 포함", "mIn", "INVALID_MEMBER"],
["하이픈 포함", "min-1", "REGISTERED"],
["공백 포함", "min 1", "INVALID_MEMBER"],
["한글", "min민", "INVALID_MEMBER"],
];
const roomy = createSession({ id: "ids", title: "아이디 확인", capacity: 100 });
for (const [label, id, expected] of idCases) {
const { code } = register(roomy, id);
assert.equal(code, expected);
console.log("id", label, code);
}
let two = createSession({ id: "two-seats", title: "두 자리", capacity: 2 });
two = register(two, "min").state;
const hana = register(two, "hana");
const jun = register(hana.state, "jun");
const minAgain = register(hana.state, "min");
console.log("count", hana.code, jun.code, minAgain.code);capacity 0 RangeError
capacity 1 ACCEPT
capacity 100 ACCEPT
capacity 101 RangeError
capacity 1.5 RangeError
capacity "2" RangeError
capacity null RangeError
id 1자 REGISTERED
id 24자 REGISTERED
id 25자 INVALID_MEMBER
id 숫자로 시작 INVALID_MEMBER
id 대문자 포함 INVALID_MEMBER
id 하이픈 포함 REGISTERED
id 공백 포함 INVALID_MEMBER
id 한글 INVALID_MEMBER
count REGISTERED FULL ALREADY_REGISTERED정원은 끝값인 1과 100만 허용되고 바로 바깥인 0과 101, 정수가 아닌 값은 모두 RangeError로 거절되었습니다. 문자열 "2"는 숫자처럼 보이지만 Number.isInteger("2")가 거짓이라 거절되며, 화면에서 받은 값을 숫자로 바꾸는 일은 이 함수가 아니라 입력을 받는 쪽에서 해야 합니다. 아이디는 1자와 24자가 허용되고 25자가 거절되어 길이의 끝값이 정확한 것을 확인했습니다.
마지막 줄은 그림의 과정과 같습니다. 참가자가 1명인 모임에 hana가 등록되어 정원 2에 이르고, 그 뒤 새 아이디 jun은 FULL을, 이미 신청한 min은 ALREADY_REGISTERED를 받았습니다. 같은 만석 상태에서 두 사람이 서로 다른 이유로 거절된 것이며, 이 결과를 형식이 잘못된 아이디가 중복 검사나 정원 검사에 이르기도 전에 거절된 경우와 헷갈리지 않아야 합니다.
정리
- 동치 분할은 같은 규칙으로 처리될 입력끼리 영역을 나누는 방법이며, 영역의 대표 값 하나가 모든 결함을 찾아 주지는 않습니다.
- 경계값 분석은 영역의 끝값과 그 바로 바깥의 값을 시험하며, 비교 연산자나 범위를 잘못 적는 실수를 찾는 데 쓸모가 있습니다.
- 정원이라는 입력의 끝값과 참가자 수의 끝값처럼, 서로 다른 값의 끝값은 따로 시험해야 합니다.
- 유효하지 않은 사례는 한 번에 한 가지 규칙만 어기도록 만들어야 어느 검사가 동작했는지 알 수 있습니다.
- 조건이 여러 개 얽혀 있으면 결정 테이블로 조합마다 결과를 정하고, 두 조건이 동시에 성립하는 사례로 우선순위를 확인합니다.
연습 문제
1. 취소 마감 시각의 끝값
다음 버전의 취소 정책은 "모임 시작 30분 전까지 취소할 수 있다"입니다. 시간을 계산하는 함수는 따로 있다고 할 때, 시험할 끝값과 아직 정해지지 않은 정책 질문을 적으세요.
해설 보기
시작까지 정확히 30분 남은 때와 그 바로 앞뒤, 예를 들어 30분 1초 전과 29분 59초 전을 고릅니다. 그런데 정확히 30분 전에 취소할 수 있는지, 초 단위로 따지는지 분 단위로 반올림하는지, 서버의 시계를 기준으로 하는지 사용자의 시계를 기준으로 하는지는 아직 정해지지 않았으므로 먼저 확인해야 합니다. 모임이 이미 시작된 뒤의 취소와 잘못된 시각 입력도 별도의 영역으로 시험합니다. 정책이 정해지지 않은 부분의 기대 결과를 지금의 구현에서 읽어 와 정답으로 삼아서는 안 됩니다.
2. 나이 조건의 사례
청소년 독서 모임은 14세 이상 19세 이하만 신청할 수 있고, 나이는 정수로 입력받습니다. 동치 분할과 경계값 분석으로 시험할 사례와 기대 결과를 적으세요.
해설 보기
유효한 영역은 14부터 19까지이고, 유효하지 않은 영역은 14보다 작은 정수, 19보다 큰 정수, 정수가 아닌 값입니다. 끝값과 그 바로 바깥인 13은 거절, 14는 허용, 19는 허용, 20은 거절이며, 영역 안의 대표 값으로 16을 더할 수 있습니다. 정수가 아닌 값으로는 15.5와 문자열 "15"를 넣어 거절되는지 확인합니다. 이렇게 일곱 사례 정도로, 나이를 하나씩 모두 넣어 보지 않고도 실수가 잦은 곳을 시험할 수 있습니다.
3. 한꺼번에 여러 규칙을 어긴 사례
누군가 아이디 검사를 /^[a-z].{0,23}$/로 잘못 바꾸어, 첫 글자만 소문자인지 보고 나머지 글자는 무엇이든 받아들이게 되었습니다. 유효하지 않은 사례로 "1 Min" 하나만 시험하고 있었다면 이 결함을 찾을 수 있을까요?
해설 보기
찾을 수 없습니다. "1 Min"은 첫 글자가 숫자라 잘못된 정규식에서도 거절되므로 테스트는 계속 통과합니다. 하지만 "min 1"이나 "mIn"처럼 첫 글자는 맞고 나머지 글자에 공백이나 대문자가 있는 아이디는 잘못된 정규식이 받아들입니다. 본문의 예제처럼 공백 포함, 대문자 포함 사례를 하나씩 따로 두었다면 이 결함이 바로 드러났을 것입니다.
끝값을 시험했다고 해서 영역 안의 모든 값이 안전하다고 말할 수는 없습니다. 다음 글 문장·분기·조건 검증에서는 입력이 아니라 코드의 구조를 보고, 테스트가 어떤 문장과 분기와 조건을 실행했는지 따져 봅니다.