본문 바로가기

경계값으로 조건문 점검하기

결과가 바뀌는 기준값과 그 바로 양옆의 값으로 조건문을 실행해 보고, 학점·환전·회원 나이 예제에서 비교 연산자를 잘못 쓴 곳을 찾아내는 방법을 배웁니다.

조건문을 한 번 실행해서 원하는 결과가 나왔다고 해서 모든 입력에서 맞게 동작한다고 할 수는 없습니다. 조건문의 오류는 대개 결과가 바뀌는 기준값 바로 옆에 숨어 있습니다. 예를 들어 '90점 이상이면 A'라는 조건을 score > 90으로 잘못 썼다면, 91이나 95를 넣어서는 A가 제대로 나오므로 아무 문제가 없어 보입니다. 이 실수는 정확히 90을 넣었을 때만 드러납니다.

그래서 조건문을 점검할 때는 아무 값이나 넣지 않고, 결과가 바뀌는 기준값과 그 바로 양옆의 값을 골라 실행합니다. 90점 기준이라면 89, 90, 91이고, 가입 가능한 나이가 14–120세라면 13, 14, 120, 121입니다. 이렇게 고른 값을 경계값(boundary value)이라고 합니다. 이 글에서는 요구사항에서 경계값을 찾고, 각 값의 기대 결과를 미리 적은 뒤, 실제 실행 결과와 비교하는 과정을 연습합니다.

90점이 B로 나오는 이유

점수가 0–100점이고, 90점 이상은 A, 80점 이상은 B, 나머지는 C, 범위를 벗어난 점수는 invalid로 판정해야 한다고 합시다. 다음 코드는 이 요구사항을 구현하려고 쓴 것입니다.

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

90점은 A여야 하는데 B가 나왔습니다. 90은 score > 90을 만족하지 않으므로 다음 조건 score > 80으로 넘어가 B가 된 것입니다. 요구사항의 '90점 이상'은 90을 포함하므로 조건은 score >= 90이어야 하고, 같은 이유로 B의 조건도 score >= 80이어야 80점이 B에 들어갑니다.

이 실수가 찾기 어려운 이유는 score > 90과 score >= 90이 정수 가운데 90 하나에서만 결과가 다르기 때문입니다. 91 이상에서는 둘 다 참이고 89 이하에서는 둘 다 거짓이니, 90을 넣어 보지 않는 한 어떤 실행으로도 둘을 구별할 수 없습니다. <와 <=, >와 >=를 바꿔 쓰는 실수는 모두 이렇게 기준값 하나에서만 드러납니다. 경계값을 골라 실행하는 이유가 바로 이것입니다.

요구사항을 겹치지 않는 구간으로 나누기

코드를 고치기 전에 요구사항부터 표로 정리하겠습니다. 가능한 점수 전체를 결과가 같은 구간으로 나누고, 구간마다 조건식과 확인할 값을 적습니다. 조건식은 if-else if 사슬에 쓸 순서대로 적었습니다.

구간조건식확인할 경계값
유효하지 않음score < 0 || score > 100-1, 101
A (90–100)score >= 9090, 100
B (80–89)앞 조건이 거짓이고 score >= 8080, 89
C (0–79)앞 조건이 모두 거짓0, 79

표를 만들었으면 모든 정수가 정확히 한 행에만 속하는지 확인합니다. 어느 행에도 속하지 않는 값이 있으면 그 값의 결과가 정해지지 않은 것이고, 두 행에 동시에 속하는 값이 있으면 요구사항이 모순된 것입니다. 이 표는 100 초과와 0 미만을 첫 행이 받고, 나머지 0–100을 세 행이 빈틈없이 나누어 받습니다.

A의 조건식에 상한 score <= 100이 없는 것은 첫 행이 100 초과를 이미 걸러 냈기 때문입니다. if와 else로 조건 분기하기에서 본 것처럼 else if에는 '앞 조건이 거짓'이라는 전제가 붙습니다. 그래도 이 전제가 한눈에 들어오지 않는다면 score >= 90 && score <= 100처럼 상한을 다시 써도 결과는 같습니다.

이제 표의 '확인할 경계값' 열에 있는 값마다 기대 결과를 적습니다. 실행하기 전에 기대 결과를 먼저 적어 두어야, 실행 결과를 보고 '그럴듯하다'며 넘어가는 일을 막을 수 있습니다. 비교를 위해 GradeBoundaryBug의 score를 바꿔 가며 실행한 결과도 함께 적었습니다.

score기대 결과GradeBoundaryBug의 결과
-1invalidC
0CC
79CC
80BC
89BB
90AB
100AA
101invalidA

여덟 값 가운데 -1, 80, 90, 101 네 곳에서 기대 결과와 다릅니다. 80과 90은 >를 >=로 써야 할 자리에 쓴 실수이고, -1과 101은 범위 검사를 빠뜨린 실수입니다. 반면 85나 95처럼 구간 한가운데에 있는 값만 넣었다면 이 코드도 B와 A를 내놓으므로, 네 가지 오류를 하나도 찾지 못했을 것입니다.

표대로 고친 코드는 다음과 같습니다. 여러 값으로 실행하기 쉽도록 점수를 실행 인자로 받습니다.

src/GradeBoundary.java
public final class GradeBoundary {
    public static void main(String[] args) {
        int score = Integer.parseInt(args[0]);
        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);
    }
}

배열과 반복문을 아직 배우지 않았으므로, 한 번 컴파일한 뒤 값을 바꿔 가며 여러 번 실행합니다. 아래 화면에서 >로 시작하는 줄은 입력한 명령이고, 바로 다음 줄이 그 실행의 출력입니다. 인자는 기대 결과표와 같은 순서로 넣었으니, 출력의 등급을 표의 '기대 결과' 열과 한 줄씩 맞춰 보세요.

> java -cp out GradeBoundary -1
-1=invalid
> java -cp out GradeBoundary 0
0=C
> java -cp out GradeBoundary 79
79=C
> java -cp out GradeBoundary 80
80=B
> java -cp out GradeBoundary 89
89=B
> java -cp out GradeBoundary 90
90=A
> java -cp out GradeBoundary 100
100=A
> java -cp out GradeBoundary 101
101=invalid

여덟 결과가 모두 기대 결과표와 같습니다.

0을 어디에 넣을지는 요구사항이 정한다

기준값을 어느 쪽 결과에 넣을지는 문법이 아니라 요구사항이 정합니다. 달러를 원화로 바꿔 주는 프로그램을 예로 들어 보겠습니다. 환율은 예제를 위해 1달러에 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 하나만 받는 dollars == 0을 별도의 분기로 두었습니다. 만약 '0달러도 그냥 0원으로 환산한다'고 정했다면 마지막 블록이 0을 받게 하면 되고, '0달러는 오류'라고 정했다면 첫 조건을 dollars <= 0으로 바꾸면 됩니다. 어느 쪽이든 문법상으로는 올바른 코드입니다. 그러니 요구사항에서 기준값이 어디에 속하는지를 먼저 정해야 <와 <= 가운데 무엇을 쓸지 고를 수 있습니다.

환산 금액을 계산할 때 dollars를 long으로 바꾼 것은 타입이 담을 수 있는 값에도 한계가 있기 때문입니다. int끼리 곱하면 결과도 int인데, int는 2,147,483,647까지만 담을 수 있습니다. 그래서 큰 금액을 넣으면 곱셈 결과가 이 범위를 넘어 틀린 값이 됩니다. 한 피연산자를 long으로 바꾸면 곱셈 자체가 long으로 계산되므로 이 문제가 생기지 않습니다. 어떤 금액부터 결과가 틀어지는지는 연습 문제 3에서 직접 찾아봅니다. 형변환과 값의 넘침은 변수 스코프와 숫자 형변환에서 자세히 다룹니다.

회원 나이 분류를 경계값으로 점검하기

이제 회원가입의 나이 분류를 같은 방법으로 점검하겠습니다. if와 else로 조건 분기하기에서는 1–120세를 받았지만, 여기서는 가입 정책을 '14세 이상 120세 이하'로 바꿉니다. 가입할 수 있는 나이 안에서는 17세까지 underage, 18–64세는 adult, 65세부터는 senior로 분류합니다. 정책이 바뀌면 기대 결과도 달라지므로, 표도 새 정책으로 다시 만듭니다.

이 정책에서 결과가 바뀌는 기준값은 가입 범위의 양 끝인 14와 120, 그리고 분류가 바뀌는 18과 65입니다. 분류 기준인 18과 65는 기준값 b의 b−1, b, b+1을 모두 검사합니다. 가입 범위의 양 끝에서는 최솟값 14와 최댓값 120만 넣어 보기 쉬운데, 그 바로 바깥인 13과 121도 넣어야 거절해야 할 값이 실제로 거절되는지 확인할 수 있습니다. 범위 안쪽의 이웃인 15와 119는 14, 120과 같은 결과가 나와야 하는 값이라 표를 짧게 하려고 생략했습니다. 이렇게 고른 값의 기대 결과를 먼저 적으면 다음과 같습니다.

입력기대 결과확인하는 기준값
13invalid14 (가입 하한)
14underage14 (가입 하한)
17underage18
18adult18
19adult18
64adult65
65senior65
66senior65
120senior120 (가입 상한)
121invalid120 (가입 상한)

다음 프로그램은 실행 인자로 받은 나이 하나를 분류합니다. 인자 개수가 맞지 않으면 사용법을 출력하고 끝납니다.

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 19
19=adult
> java -cp out MemberAgeBoundary 64
64=adult
> java -cp out MemberAgeBoundary 65
65=senior
> java -cp out MemberAgeBoundary 66
66=senior
> java -cp out MemberAgeBoundary 120
120=senior
> java -cp out MemberAgeBoundary 121
121=invalid

열 개의 결과가 모두 기대 결과표와 같습니다. 만약 어느 하나가 달랐다면 처음 어긋난 입력부터 살펴봅니다. 비교 연산자를 잘못 골랐는지, 조건의 순서가 틀렸는지를 확인하고, 기대 결과표를 잘못 적었을 가능성도 함께 따져 본 뒤 고쳐서 다시 실행합니다.

다만 이 열 개가 모두 맞았다고 해서 모든 입력에서 맞다고 증명된 것은 아닙니다. 구간이 모든 정수를 빈틈없이 덮는지는 조건식을 읽어서 확인하고, 실행으로는 고른 값들이 기대대로 나오는지만 확인한 것입니다. 두 가지를 함께 해야 조건문을 믿을 수 있습니다. 아래 그림은 지금까지의 과정, 곧 요구사항을 구간으로 나누고 경계값의 기대 결과를 적은 뒤 실제 출력과 비교하는 흐름을 정리한 것입니다.

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

FLOWCHART · BOUNDARY ORACLE

요구 구간과 경계 실행 결과를 함께 확인하기

먼저 요구를 빈틈과 중복 없는 구간으로 나눕니다. 그 다음 경계의 예상 결과를 적고 Java의 실제 출력과 비교합니다.

요구 구간과 경계 실행 결과를 함께 확인하기 전체요구구간→경계입력→예상값→실제출력→비교; 일치사례기록/불일치조사재실행; score0–100/age14–120조건유지 구간에서 예상값을 도출 score: 0–79 C · 80–89 B · 90–100 A · 밖 invalid age: 14–17 underage · 18–64 adult · 65–120 senior · 밖 invalid 각 경계 b의 b−1 / b / b+1을 독립 사례로 확인 입력 하나 선택 예: age=18 예상값 먼저 기록 18 → adult 같은 입력으로 실행 실제 출력 확보 예상과 실제가 일치하는가? 예 확인한 사례 기록 전체 입력 증명은 아님 아니오 차이 조사·수정 연산자 · 분기 · 예상표 재실행
요구 구간과 경계 실행 결과를 함께 확인하기 전체요구구간→경계입력→예상값→실제출력→비교; 일치사례기록/불일치조사재실행; score0–100/age14–120조건유지 요구 구간 score 0–100 0–79:C · 80–89:B 90–100:A · 밖:invalid age 14–120 14–17:underage 18–64:adult 65–120:senior 경계 입력 선택 예: age = 18 17·18·19도 각각 확인 예상값 먼저 적기 18 → adult 같은 입력으로 실행 실제 출력 확보 예상과 실제가 일치하는가? 예 확인한 사례 기록 전체 입력의 증명은 아님 아니오 차이 조사·수정 비교 연산자 · 분기 순서 예상 표의 오류도 조사 재실행
  1. 유효 범위와 전환점을 구간으로 나눕니다

    점수 예제는 전체 정수 입력을 아래 네 구간으로 나눕니다. else-if의 앞 조건이 거짓인 범위를 다음 조건이 이어받습니다.

    점수 요구의 전체 구간
    score 구간예상 라벨
    0 미만 / 100 초과invalid
    0–79C
    80–89B
    90–100A
    0 미만 / 100 초과
    invalid
    0–79
    C
    80–89
    B
    90–100
    A

    빠진 구간이나 중복 분류가 있으면 실행 전에 조건과 순서를 고칩니다.

  2. 결과가 바뀌는 경계의 예상값을 먼저 적습니다

    회원 나이는 14–120을 허용하고, 18과 65에서 내부 라벨이 바뀝니다. 각 경계 b의 b−1, b, b+1을 검토합니다.

    본문 회원 나이 예제의 경계 결과
    age 입력 순서예상 라벨 순서
    13 → 14invalid → underage
    17 → 18 → 19underage → adult → adult
    64 → 65 → 66adult → senior → senior
    120 → 121senior → invalid
    13 → 14
    invalid → underage
    17 → 18 → 19
    underage → adult → adult
    64 → 65 → 66
    adult → senior → senior
    120 → 121
    senior → invalid
  3. 같은 입력을 Java로 실행하고 실제 출력을 비교합니다

    일치하면 확인한 경계 사례를 기록합니다. 다르면 최초 불일치의 비교 연산자, 분기 순서, 예상 표를 조사하여 다시 실행합니다.

구간이 전체 입력을 덮는지는 조건식으로 확인하고, 선택한 경계 사례는 실행으로 확인합니다. 일부 테스트의 통과만으로 모든 입력의 정확성이 증명되지는 않습니다.

이 기대 결과표는 코드를 고칠 때마다 다시 쓸 수 있습니다. 나중에 이 분류를 메서드로 옮기거나 다른 모양의 조건으로 바꾸더라도, 같은 입력에는 같은 결과가 나와야 하기 때문입니다.

요구사항을 조건문으로 옮기기 전에 확인할 네 가지

지금까지 한 일을 정리하면, 조건문을 쓰기 전에 다음 네 가지 질문에 답한 것입니다.

  1. 입력값의 유효 범위는 어디부터 어디까지인가?
  2. 결과가 바뀌는 기준값은 정확히 무엇인가?
  3. 기준값 자체는 바로 아래 구간과 바로 위 구간 중 어느 쪽 결과에 속하는가?
  4. 어느 결과에도 속하지 않거나, 두 결과에 동시에 속하는 값이 있는가?

배송비를 예로 들어 보겠습니다. 요구사항이 '거리 1km 이하는 1,000원, 10km 이하는 2,000원, 그 이상은 3,000원'이라고 적혀 있다고 합시다. 거리는 정수 km로 받는다고 가정합니다. 2번 질문의 답인 기준값은 1과 10입니다. 3번 질문에 답하면 1은 '1km 이하'이므로 1,000원 쪽에, 10은 '10km 이하'이므로 2,000원 쪽에 속합니다.

그런데 4번 질문에서 문제가 드러납니다. '이상'은 기준값을 포함하는 말이라서, '10km 이하는 2,000원, 그 이상은 3,000원'이라고 쓰면 10km가 두 구간에 동시에 속합니다. 이럴 때는 코드를 쓰기 전에 요구사항을 '10km를 넘으면 3,000원'처럼 바로잡아야 합니다. 1번 질문도 남아 있습니다. 거리가 0이나 음수인 경우는 요구사항에 없으므로, 받지 않기로 정했다면 분류보다 먼저 거절해야 합니다.

네 질문에 답한 결과를 코드로 옮기면 다음과 같습니다. 유효하지 않은 거리를 먼저 거르고, distance <= 1, 그 조건이 거짓일 때의 distance <= 10, 마지막 else 순서로 배치합니다.

src/ShippingFee.java
public final class ShippingFee {
    public static void main(String[] args) {
        int distance = Integer.parseInt(args[0]);

        if (distance <= 0) {
            System.out.println("invalid distance");
        } else if (distance <= 1) {
            System.out.println(distance + "km=1000");
        } else if (distance <= 10) {
            System.out.println(distance + "km=2000");
        } else {
            System.out.println(distance + "km=3000");
        }
    }
}

경계값은 기준값 1의 이웃인 0, 1, 2와 기준값 10의 이웃인 9, 10, 11입니다. 0은 유효 범위의 바로 바깥이기도 합니다.

> java -cp out ShippingFee 0
invalid distance
> java -cp out ShippingFee 1
1km=1000
> java -cp out ShippingFee 2
2km=2000
> java -cp out ShippingFee 9
9km=2000
> java -cp out ShippingFee 10
10km=2000
> java -cp out ShippingFee 11
11km=3000

10km가 2,000원으로 나오는 것은 3번 질문에서 정한 대로입니다. 바로잡은 요구사항과 이 실행 결과를 함께 남겨 두면, 나중에 누군가 '10km는 왜 2,000원인가'라고 물을 때도 답할 수 있습니다.

더 깊이: 동등 분할과 경계값 분석

이 글에서 한 일은 소프트웨어 테스트에서 동등 분할(equivalence partitioning)과 경계값 분석(boundary value analysis)이라고 부르는 기법입니다. 동등 분할은 입력을 '프로그램이 똑같이 처리해야 하는 값들'의 묶음으로 나누는 것으로, 앞의 구간표가 바로 그것입니다. 같은 묶음 안의 값은 원칙적으로 하나만 검사해도 됩니다. 하지만 비교 연산자를 잘못 고르는 실수는 묶음의 양 끝에만 영향을 주므로, 경계값 분석은 그 끝의 값들을 따로 골라 검사합니다. 동등 분할을 소프트웨어공학 교재에서는 동치 분할이라고 부르며, 두 기법은 동치 분할과 경계값에서 테스트 설계 관점으로 더 자세히 다룹니다.

b−1, b, b+1을 고르는 방식은 입력이 정수일 때의 이야기입니다. 거리를 double로 받는다면 1과 2 사이에도 1.5 같은 값이 있으므로, 기준값 1.0 자체와 그보다 조금 크거나 작은 값을 골라야 합니다. 실무에서는 이렇게 만든 기대 결과표를 JUnit 같은 테스트 도구의 테스트 코드로 옮겨 둡니다. 그러면 코드를 고칠 때마다 표 전체를 자동으로 다시 실행해, 고친 곳 때문에 다른 결과가 틀어지지 않았는지 바로 확인할 수 있습니다.

정리

  • <와 <=처럼 비슷한 비교 연산자는 기준값 하나에서만 결과가 다릅니다. 그래서 기준값과 그 바로 양옆의 값으로 실행해야 이런 실수를 찾을 수 있습니다.
  • 요구사항을 겹치지 않는 구간으로 나눈 표를 먼저 만들고, 모든 값이 정확히 한 구간에 속하는지 확인합니다.
  • 실행하기 전에 각 경계값의 기대 결과를 적고, 실제 출력과 비교합니다. 다르면 연산자, 조건 순서, 기대 결과표를 차례로 점검합니다.
  • 기준값을 어느 쪽 결과에 넣을지는 요구사항이 정합니다. '이상', '이하', '초과', '미만'이 모호하면 코드보다 요구사항을 먼저 바로잡습니다.
  • 경계값 검사가 모두 통과해도 모든 입력이 증명된 것은 아닙니다. 구간이 빈틈없는지는 조건식을 읽어서 확인합니다.

지금은 경계값마다 프로그램을 다시 실행했지만, 같은 일을 되풀이하는 반복문을 쓰면 실행 인자 여러 개를 한 번에 처리할 수 있습니다. 다음 글에서는 while과 break, continue로 반복을 제어하는 방법을 배웁니다.

연습 문제

문제 1: 캠페인 달성률 분류하기

회원가입 캠페인의 달성률(%)을 다음과 같이 분류하는 SignupRateBoundary를 작성하세요. 그리고 -1, 0, 1, 79, 80, 99, 100, 101을 모두 넣어 실행한 결과가 기대와 같은지 확인하세요.

  • 0 미만 또는 100 초과: invalid
  • 0: not-started
  • 1–79: in-progress
  • 80–99: almost
  • 100: complete
해설 보기
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);
    }
}

여덟 값을 차례로 넣어 실행한 결과를 이어 적으면 다음과 같습니다.

-1=invalid
0=not-started
1=in-progress
79=in-progress
80=almost
99=almost
100=complete
101=invalid

0과 100은 각각 그 값 하나만 받는 구간이라서 따로 다뤘습니다. 0은 rate == 0으로 직접 검사했고, 100은 앞의 조건들이 0–99를 모두 걸러 낸 뒤에 남는 마지막 else가 받습니다. 80 이상을 먼저 검사하는 식으로 순서를 바꿔도 되지만, 그때도 각 조건이 앞 조건에서 걸러지고 남은 값만 받는다는 점을 설명할 수 있어야 합니다.

문제 2: 경계값으로 버그 찾기

비밀번호 길이는 '8자 이상 20자 이하'여야 한다는 요구사항을 다음 코드로 구현했습니다. 길이 12나 15를 넣으면 true가 나와 맞는 것처럼 보입니다. 경계값을 골라 기대 결과와 실제 결과를 비교하고, 틀린 곳을 고치세요.

boolean validLength = length > 8 && length < 20;
해설 보기

기준값은 8과 20이므로 7, 8, 9와 19, 20, 21을 검사합니다.

length기대 결과실제 결과
7falsefalse
8truefalse
9truetrue
19truetrue
20truefalse
21falsefalse

8과 20에서 기대와 다릅니다. '이상'과 '이하'는 기준값을 포함하므로 두 비교 모두 기준값을 포함하는 연산자로 바꿔야 합니다.

boolean validLength = length >= 8 && length <= 20;

12나 15 같은 값은 두 코드에서 결과가 같으므로 이 버그를 찾는 데 아무 도움이 되지 않습니다. 버그는 정확히 기준값인 8과 20에서만 드러납니다.

문제 3: 타입의 최댓값도 경계값이 된다

ExchangeBoundary의 환산 줄을 int won = dollars * wonPerDollar;로 바꾸면, 결과가 틀리기 시작하는 가장 작은 달러 금액은 얼마인가요? int의 최댓값이 2,147,483,647이라는 것을 이용해 계산하고, 그 금액과 바로 아래 금액을 넣어 확인하세요.

해설 보기

2,147,483,647을 1,350으로 나눈 몫은 1,590,728입니다. 1,590,728 × 1,350 = 2,147,482,800은 int에 담기지만, 1,590,729 × 1,350 = 2,147,484,150은 최댓값을 넘습니다. 그래서 바꾼 코드를 java -cp out ExchangeBoundary 1590728로 실행하면 won=2147482800이 제대로 출력되지만, 인자를 1590729로 바꾸면 won=-2147483146처럼 음수가 출력됩니다. 실행 인자에는 쉼표를 넣지 않습니다. Integer.parseInt는 쉼표가 들어간 문자열을 정수로 바꾸지 못합니다. 결과가 틀리기 시작하는 금액은 1,590,729달러입니다.

이 경계는 요구사항이 아니라 int라는 타입이 만든 것입니다. 원래 코드처럼 (long) dollars * wonPerDollar로 곱셈 전에 형변환하면 이 경계가 사라집니다. 반면 (long) (dollars * wonPerDollar)라고 쓰면 int로 곱해서 이미 틀린 값을 long으로 바꾸는 것이라 소용이 없습니다. 형변환을 어디에 두느냐가 결과를 바꾸는 이유는 변수 스코프와 숫자 형변환에서 다시 다룹니다.