안동민 개발노트

본문 시작

조건 경계 훈련

학점·환율·회원 나이 범위를 경계 표로 바꾸고 바로 아래·같음·바로 위 입력으로 조건식을 검증합니다.

조건문을 한 번 실행해 원하는 라벨이 나왔다고 해서 범위 전체가 맞는 것은 아닙니다.

오류는 대개 기준값 바로 옆에 숨어 있습니다.

90점 기준이라면 89, 90, 91을 실행하고, 허용 나이가 14–120세라면 13, 14, 120, 121을 실행해야 <와 <=의 차이가 드러납니다.


대표 경계값 누락

lab/GradeBoundaryBug.java
public final class GradeBoundaryBug {
    public static void main(String[] args) {
        int score = 90;
        String grade;

        if (score > 90) {
            grade = "A";
        } else if (score > 80) {
            grade = "B";
        } else {
            grade = "C";
        }

        System.out.println(score + "=" + grade);
    }
}
90=B

91만 실행했다면 A가 나와 오류를 놓칩니다.

요구가 “90점 이상 A”라면 조건은 score >= 90입니다.

B도 “80점 이상”이라면 score >= 80이어야 80이 포함됩니다.

src/GradeBoundary.java
public class GradeBoundary {
    public static void main(String[] args) {
        int score = 90;
        String grade;

        if (score < 0 || score > 100) {
            grade = "invalid";
        } else if (score >= 90) {
            grade = "A";
        } else if (score >= 80) {
            grade = "B";
        } else {
            grade = "C";
        }

        System.out.println(score + "=" + grade);
    }
}
90=A

먼저 정상 예제 하나를 이해한 뒤 score만 79, 80, 89, 100, 101로 바꾸어 다시 실행합니다.

아직 배우지 않은 배열과 향상된 for를 경계 검사 도구로 미리 사용하지 않습니다.

점수 유효성 0–100을 먼저 검사했으므로 A 조건은 90–100만 받습니다.


겹치지 않는 조건 구간

코드 전에 범위를 표로 만듭니다.

구간조건식대표 경계
유효하지 않음`score < 0
Ascore >= 9090
B앞 조건 거짓 + score >= 8080, 89
C앞 조건 모두 거짓0, 79

각 정수 입력이 정확히 한 행에 속하고 빈 구간이 없는지 확인합니다.

score >= 90 && score <= 100처럼 상한을 반복해도 되지만 앞의 유효성 검사와 분기 순서가 이미 상한을 보장합니다.

이 전제를 읽기 어렵다면 조건을 명시적으로 써도 됩니다.


0의 업무 의미

달러를 원화로 환산하는 요구를 생각해 봅시다.

환율 1,350원, 달러 양수만 환산, 0달러는 “교환할 금액 없음”, 음수는 오류라고 정합니다.

src/ExchangeBoundary.java
public final class ExchangeBoundary {
    public static void main(String[] args) {
        int dollars = Integer.parseInt(args[0]);
        int wonPerDollar = 1_350;

        if (dollars < 0) {
            System.out.println("invalid dollars");
        } else if (dollars == 0) {
            System.out.println("nothing to exchange");
        } else {
            long won = (long) dollars * wonPerDollar;
            System.out.println("won=" + won);
        }
    }
}
세 실행
> java -cp out ExchangeBoundary -1
invalid dollars

> java -cp out ExchangeBoundary 0
nothing to exchange

> java -cp out ExchangeBoundary 100
won=135000

0을 양수와 같은 결과로 처리할지 별도 상태로 처리할지는 문법이 정하지 않습니다.

요구사항을 먼저 정해야 > 0과 >= 0 중 하나를 선택할 수 있습니다.

곱셈은 큰 달러 입력에서 int 범위를 넘을 수 있어 한 피연산자를 long으로 바꿨습니다.


회원 나이 범위의 경계값

회원가입은 14–120세를 허용합니다.

다음 프로그램은 명령으로 받은 값 하나를 분류합니다.

아직 배열과 반복문을 배우기 전이므로 경계값마다 프로그램을 다시 실행합니다.

src/AgeBoundaryTable.java
public final class AgeBoundaryTable {
    public static void main(String[] args) {
        if (args.length != 1) {
            System.out.println("사용법: AgeBoundaryTable <age>");
            return;
        }

        int age = Integer.parseInt(args[0]);
        boolean valid = age >= 14 && age <= 120;
        String label;
        if (!valid) {
            label = "invalid";
        } else if (age < 18) {
            label = "underage";
        } else if (age < 65) {
            label = "adult";
        } else {
            label = "senior";
        }
        System.out.println(age + ":" + valid + ":" + label);
    }
}
> java -cp out AgeBoundaryTable 13
13:false:invalid
> java -cp out AgeBoundaryTable 14
14:true:underage
> java -cp out AgeBoundaryTable 120
120:true:senior
> java -cp out AgeBoundaryTable 121
121:false:invalid

최솟값과 최댓값만 보지 않고 바로 바깥 값까지 넣습니다.

내부 분류 경계 18과 65에도 17/18/19, 64/65/66을 적용해야 합니다.

경계가 여러 개라면 각 경계마다 세 값을 만들어 전체 조건표를 확인합니다.


조건 문제 해석법

조건 연습에서 바로 코드를 쓰기 전에 다음 네 질문에 답합니다.

  1. 입력 값의 전체 유효 범위는 어디까지인가?
  2. 결과가 바뀌는 정확한 기준값은 무엇인가?
  3. 기준값 자체는 왼쪽 결과와 오른쪽 결과 중 어디에 포함되는가?
  4. 어느 결과에도 속하지 않거나 두 결과에 동시에 속하는 값이 있는가?

예를 들어 “거리에 따라 운송비를 계산하되 1km 이하는 1,000원, 10km 이하는 2,000원, 그 이상은 3,000원”이라면 경계는 1과 10입니다.

distance <= 1, 그 조건이 거짓인 상태에서 distance <= 10, 마지막 else로 배치합니다.

0이나 음수를 허용하지 않는다면 이 분류보다 먼저 유효성 검사가 필요합니다.

경계 요구를 전체 구간과 실행 가능한 예상 결과표로 바꾸는 검증 흐름

FLOWCHART · BOUNDARY ORACLE

경계 요구를 배타적 구간과 실행 오라클로 바꾸기

유효 범위와 결과 전환점을 ordered guard로 옮긴 뒤, 모든 구간이 정확히 한 번 덮이는지와 경계 실행이 예상 결과와 일치하는지를 따로 검증합니다.

경계 요구를 배타적 구간과 실행 오라클로 바꾸기 유효 범위와 전환점 및 포함 규칙을 ordered guard로 옮겨 전체 입력을 배타적 구간으로 나누고, 각 경계의 아래·같음·위 예상 결과를 Java 실행과 비교해 빈틈과 중복 없는 조건 계약을 닫는 흐름입니다. READ → ORDER 아니요 예 아니요 예 요구사항 읽기 유효 범위 · 전환점 · 포함 규칙 DOMAIN · BOUNDARY · INCLUSION ordered guard → 전체 구간 앞 분기의 나머지를 배타적으로 이어받음 if → else if → else · EXHAUSTIVE 빈틈·중복 없이 전체 입력을 덮나? guard 순서 수정 누락 구간과 중복 조건 제거 REPARTITION 경계별 probe와 예상 결과 작성 SCORE · <0 invalid · 0–79 C · 80–89 B · 90–100 A · >100 invalid AGE · 13/14 · 17/18/19 · 64/65/66 · 120/121 Java 실행 오라클 각 b의 b-1 · b · b+1을 실행 EXPECTED ↔ ACTUAL 모든 Java 실행이 예상과 일치하나? 첫 불일치 조사 연산자 · 순서 · 기대표 수정 FIX FIRST DIVERGENCE 조건 계약 완료 모든 입력 한 구간 · 모든 실행 일치 PARTITION FIRST · WRITE EXPECTED RESULTS · THEN RUN JAVA

경계 검증 계약 구간을 먼저 닫고 실행으로 확인합니다.

  1. 요구를 guard로 번역

    유효 범위, 결과가 바뀌는 전환점, 경계값의 포함 규칙을 추출해 if → else if → else ordered guard로 옮깁니다.

  2. 점수 전체 구간 확인

    score < 0은 invalid, 0–79는 C, 80–89는 B, 90–100은 A, 100 초과는 invalid입니다. 모든 정수 입력은 정확히 한 구간에 속합니다.

  3. 나이 양끝과 내부 전환 확인

    13/14, 17/18/19, 64/65/66, 120/121을 사용해 유효범위 양끝과 18·65 내부 전환을 함께 검증합니다.

  4. 예상 결과를 먼저 기록

    각 경계 b마다 b-1, b, b+1의 예상 라벨을 실행 전에 적습니다.

  5. Java 실행과 비교

    빈틈이나 중복이 없고 모든 실제 출력이 예상 결과와 일치할 때만 조건 계약이 완료됩니다. 다르면 첫 불일치의 연산자와 guard 순서를 고칩니다.

  • 요구·구간·실행 검증
  • 수정이 필요한 불일치 경로
  • 배타적 구간과 실행 결과가 모두 닫힌 계약

경계값 테스트는 실행값을 많이 고르는 일이 아니라, 구간의 합집합과 교집합을 먼저 확정하고 그 경계의 예상 결과를 실행으로 증명하는 일입니다.


회원 경계 실행표

MemberAgeBoundary는 명령으로 받은 값 하나를 분류합니다.

배열과 반복문은 아직 배우지 않았으므로 같은 프로그램을 경계값마다 다시 실행해 결과표를 완성합니다.

src/MemberAgeBoundary.java
public final class MemberAgeBoundary {
    public static void main(String[] args) {
        if (args.length != 1) {
            System.out.println("사용법: MemberAgeBoundary <age>");
            return;
        }

        int age = Integer.parseInt(args[0]);
        String result;
        if (age < 14 || age > 120) {
            result = "invalid";
        } else if (age < 18) {
            result = "underage";
        } else if (age < 65) {
            result = "adult";
        } else {
            result = "senior";
        }
        System.out.println(age + "=" + result);
    }
}
> java -cp out MemberAgeBoundary 13
13=invalid
> java -cp out MemberAgeBoundary 14
14=underage
> java -cp out MemberAgeBoundary 17
17=underage
> java -cp out MemberAgeBoundary 18
18=adult
> java -cp out MemberAgeBoundary 64
64=adult
> java -cp out MemberAgeBoundary 65
65=senior
> java -cp out MemberAgeBoundary 120
120=senior
> java -cp out MemberAgeBoundary 121
121=invalid

이 출력은 이후 MemberRegistry register를 바꿀 때 유지해야 할 조건 계약입니다.

메서드 내부 구현을 switch나 다른 표현으로 바꾸더라도 같은 입력표의 결과가 같아야 합니다.


연습 문제

회원가입 캠페인 달성률을 다음처럼 분류하는 SignupRateBoundary를 작성하세요.

  • 0 미만 또는 100 초과: invalid
  • 0: not-started
  • 1–79: in-progress
  • 80–99: almost
  • 100: complete

-1, 0, 1, 79, 80, 99, 100, 101을 모두 실행해 출력합니다.

해설 보기
src/SignupRateBoundary.java
public final class SignupRateBoundary {
    public static void main(String[] args) {
        if (args.length != 1) {
            System.out.println("사용법: SignupRateBoundary <rate>");
            return;
        }

        int rate = Integer.parseInt(args[0]);
        String status;
        if (rate < 0 || rate > 100) {
            status = "invalid";
        } else if (rate == 0) {
            status = "not-started";
        } else if (rate < 80) {
            status = "in-progress";
        } else if (rate < 100) {
            status = "almost";
        } else {
            status = "complete";
        }
        System.out.println(rate + "=" + status);
    }
}

java -cp out SignupRateBoundary -1부터 문제에 제시된 여덟 값을 한 번씩 실행합니다.

0과 100은 각각 의미가 있는 단일 경계라 별도 분기로 뒀습니다.

80 이상 조건을 먼저 두는 방식도 가능하지만 각 앞 조건이 제거한 범위를 설명할 수 있어야 합니다.