본문 바로가기

입력 검증과 재입력 반복

try와 catch로 숫자 변환 실패를 붙잡고 범위를 검사해, 잘못된 입력이 들어와도 다시 묻고 계속 동작하는 메뉴형 회원가입을 만듭니다.

앞 글의 회원가입 프로그램은 나이 자리에 forty가 들어오면 NumberFormatException과 함께 그 자리에서 끝났습니다. 사람이 키보드로 입력하는 프로그램에서 오타는 드문 일이 아닙니다. 오타 하나에 프로그램이 끝나고 그때까지 입력한 내용까지 사라진다면, 사용자는 처음부터 다시 실행해야 합니다. 대화형 프로그램이라면 잘못된 입력을 만났을 때 무엇이 틀렸는지 알려 주고 다시 물어야 합니다.

이 글에서는 먼저 try와 catch로 변환 실패를 붙잡는 가장 작은 형태를 익힙니다. 그다음 숫자 형식 검사와 값 범위 검사를 나눈 재입력 반복을 만들고, 반복을 끝내는 신호를 고르는 기준과 메뉴 반복을 살펴본 뒤, 이것들을 하나의 회원가입 대화로 묶습니다.

잘못된 숫자 하나에 프로그램 전체가 끝나는 문제

다음 프로그램은 입력을 한 줄씩 읽어 나이로 바꾸고, 바꾼 값을 출력합니다. 입력의 첫 줄은 forty, 둘째 줄은 45입니다.

lab/AbortOnBadNumber.java
import java.util.Scanner;

public final class AbortOnBadNumber {
    public static void main(String[] args) {
        Scanner scanner = new Scanner("forty\n45\n");
        while (scanner.hasNextLine()) {
            int age = Integer.parseInt(scanner.nextLine());
            System.out.println("saved=" + age);
        }
    }
}
출력의 첫 줄
Exception in thread "main" java.lang.NumberFormatException: For input string: "forty"

첫 줄 forty를 변환하다 예외가 발생했고, 프로그램은 그 자리에서 끝났습니다. 둘째 줄 45는 올바른 나이인데도 읽히지조차 못했습니다. 예외는 처리하지 않으면 main 밖으로 빠져나가고, 그러면 JVM이 위와 같은 메시지를 출력한 뒤 프로그램을 끝내기 때문입니다. 한 줄의 입력 실수를 프로그램 전체의 실패로 다룬 셈입니다.

try와 catch로 예외 붙잡기

예외가 main 밖으로 빠져나가지 않게 하려면, 예외가 날 수 있는 코드를 try 블록에 넣고 예외가 났을 때 할 일을 catch 블록에 적습니다. catch 뒤 괄호에는 붙잡을 예외의 종류와, 붙잡은 예외를 가리킬 변수 이름을 씁니다. 다음 코드는 parseInt가 던지는 NumberFormatException을 error라는 이름으로 받습니다.

src/ParseOnce.java
public class ParseOnce {
    public static void main(String[] args) {
        String raw = "45";

        try {
            int age = Integer.parseInt(raw);
            System.out.println("age=" + age);
        } catch (NumberFormatException error) {
            System.out.println("숫자를 입력하세요");
        }
    }
}
age=45

raw가 "45"일 때는 try 블록이 처음부터 끝까지 실행되어 age=45가 출력되고, catch 블록은 실행되지 않습니다. raw를 "forty"로 바꾸면 결과가 달라집니다. Integer.parseInt(raw)에서 예외가 발생하는 순간 try 블록의 나머지 문장, 즉 age= 출력은 건너뛰고 곧바로 catch 블록으로 이동해 숫자를 입력하세요를 출력합니다. 어느 경우든 try/catch 문이 끝나면 그다음 문장부터 평소처럼 실행이 이어지고, 프로그램은 예외로 끝나지 않습니다.

catch 뒤의 괄호는 어떤 예외를 붙잡을지 정합니다. catch (NumberFormatException error)는 NumberFormatException만 붙잡고, 다른 종류의 예외는 그대로 통과시킵니다. error는 붙잡은 예외를 가리키는 변수입니다. 이 글의 예제에서는 쓰지 않지만, error.getMessage()를 호출하면 For input string: "forty" 같은 설명을 꺼낼 수 있습니다.

try 블록도 중괄호로 만든 블록이므로, 변수 스코프와 숫자 형변환에서 본 규칙이 그대로 적용됩니다. 위 코드의 age는 try 블록 안에서 선언했기 때문에 블록 밖에서는 쓸 수 없습니다. 이 점은 뒤에서 age를 try 바깥에 선언하는 이유가 됩니다. 예외의 종류와 계층, 여러 catch를 두는 순서는 12장의 예외 계층과 검사 예외에서, finally는 try·catch·finally 실행 순서와 정리 코드에서 자세히 다룹니다. 여기서는 사용자 입력 한 건이 잘못돼도 대화를 이어 가는 데 필요한 최소한만 씁니다.

형식이 틀려도, 범위를 벗어나도 다시 묻기

이제 try/catch를 반복문 안에 넣어, 올바른 나이가 들어올 때까지 계속 묻는 프로그램을 만들겠습니다. 회원 나이는 14 이상 120 이하만 받기로 합니다. 따라서 거절할 입력은 두 종류입니다. forty처럼 숫자로 바꿀 수 없는 입력과, 0처럼 숫자이긴 하지만 허용 범위를 벗어난 입력입니다.

src/RetryNumber.java
import java.util.Scanner;

public final class RetryNumber {
    public static void main(String[] args) {
        Scanner scanner = new Scanner("forty\n0\n45\n");

        while (scanner.hasNextLine()) {
            String raw = scanner.nextLine().strip();
            try {
                int age = Integer.parseInt(raw);
                if (age < 14 || age > 120) {
                    System.out.println("range.error=" + raw);
                    continue;
                }
                System.out.println("saved=" + age);
                break;
            } catch (NumberFormatException error) {
                System.out.println("number.error=" + raw);
            }
        }
    }
}
number.error=forty
range.error=0
saved=45

세 줄의 입력이 각각 어떤 길을 지나는지 따라가 보겠습니다.

회차입력parseInt 결과범위 검사출력다음 동작
1fortyNumberFormatException하지 않음number.error=fortycatch가 끝나고 다음 회차
200범위 밖range.error=0continue로 다음 회차
34545통과saved=45break로 반복 종료

1회차에서는 parseInt가 실패하는 순간 범위 검사와 saved= 출력을 모두 건너뛰고 catch로 이동합니다. catch 블록이 끝나면 반복문 본문도 끝나므로, 따로 continue를 쓰지 않아도 자연스럽게 다음 회차로 넘어갑니다. 2회차의 범위 검사는 try 블록 안에 있지만 parseInt 다음 줄이므로, 변환에 성공했을 때만 실행됩니다. 3회차에서 올바른 값을 받으면 break로 반복을 끝냅니다. 여기서 saved=는 출력일 뿐이고 값을 어딘가에 보관하지는 않습니다. 회원 정보를 실제로 보관하는 방법은 배열과 인덱스에서 다룹니다.

형식 오류와 범위 오류를 다른 메시지로 알리는 데는 이유가 있습니다. forty를 입력한 사용자에게는 '숫자로 입력하라'는 안내가, 0을 입력한 사용자에게는 '14부터 120까지만 된다'는 안내가 필요합니다. 두 실패를 하나의 메시지로 뭉뚱그리면 사용자는 무엇을 고쳐야 할지 알 수 없습니다.

입력 원천을 new Scanner(System.in)으로 바꾸고 같은 값을 직접 입력하면, 터미널에서는 다음과 같이 진행됩니다. forty, 0, 45는 사용자가 친 글자이고, 그 아래 줄이 프로그램의 응답입니다.

터미널 화면
forty
number.error=forty
0
range.error=0
45
saved=45

더 깊이: parseInt가 숫자로 인정하는 문자열

Integer.parseInt는 맨 앞의 + 또는 - 부호 하나와 십진 숫자로만 이루어진 문자열을 받아들입니다. 그래서 +45는 45가 되지만, 앞뒤에 공백이 붙은 " 45"나 빈 문자열 ""은 NumberFormatException을 던집니다. 입력을 strip()한 뒤에 변환하는 이유입니다. int의 범위를 넘는 2147483648도 형식 오류와 같은 NumberFormatException으로 거절됩니다. 변수 스코프와 숫자 형변환에서 본 축소 변환과 달리, 조용히 잘린 값을 돌려주지는 않습니다.

한 가지 의외의 점은 '십진 숫자'를 ASCII의 0부터 9까지로 한정하지 않는다는 것입니다. parseInt는 각 글자가 숫자인지를 Character.digit으로 판정하므로, 전각 숫자로 쓴 45도 45로 바꿉니다. 다른 문자 체계의 숫자도 마찬가지입니다. 대부분의 프로그램에서는 문제가 되지 않지만, 입력이 ASCII 숫자만으로 되어 있어야 하는 규칙이 있다면 변환 전에 글자를 따로 검사해야 합니다.

반복을 끝내는 신호, 센티널

재입력 반복은 올바른 값을 받으면 끝났습니다. 그런데 이메일을 여러 개 받는 경우처럼 '몇 개를 받을지 정해져 있지 않은' 반복은 언제 끝내야 할까요? 입력이 끝날 때까지 읽는 방법도 있지만, 키보드 앞의 사용자에게 Ctrl+D나 Ctrl+Z를 누르라고 하기는 어색합니다. 그래서 흔히 '이 값이 들어오면 그만 받는다'는 약속된 값을 정해 둡니다. 이렇게 데이터 사이에 섞여 들어와 반복의 끝을 알리는 특별한 값을 센티널(sentinel)이라고 부릅니다.

센티널은 정상 데이터가 될 수 없는 값이어야 합니다. 예를 들어 나이를 연달아 받을 때 0세를 허용하지 않는다면 0을 종료 신호로 쓸 수 있습니다. 하지만 나중에 규칙이 바뀌어 0이 올바른 데이터가 되면, 사용자가 입력한 0이 데이터인지 종료 신호인지 구분할 수 없게 됩니다. 이메일 자리에 들어올 수 없는 명령 문자열 quit처럼, 데이터와 생김새부터 다른 값을 고르는 편이 안전합니다.

src/SentinelLoop.java
import java.util.Scanner;

public final class SentinelLoop {
    public static void main(String[] args) {
        Scanner scanner = new Scanner("[email protected]\n[email protected]\nquit\n[email protected]\n");
        int count = 0;

        while (scanner.hasNextLine()) {
            String email = scanner.nextLine().strip();
            if (email.equals("quit")) {
                break;
            }
            if (email.isEmpty()) {
                continue;
            }
            count++;
            System.out.println("accepted=" + email);
        }
        System.out.println("count=" + count);
    }
}

처음 두 줄은 센티널이 아니므로 count를 하나씩 늘리고 accepted=를 출력합니다. 셋째 줄 quit을 읽으면 break로 반복을 빠져나가고, 반복 뒤의 count=2가 출력됩니다. quit 검사가 count++보다 앞에 있으므로 quit 자체는 개수에 들어가지 않습니다. 또 반복을 빠져나온 뒤에는 nextLine()을 더 호출하지 않으므로, 넷째 줄 [email protected]은 읽히지 않은 채 Scanner에 남습니다. 센티널 뒤의 입력은 처리하지 않는다는 뜻입니다.

이 반복에는 출구가 두 개 있습니다. 센티널을 만나 break하는 길과, 센티널 없이 입력이 끝나 hasNextLine()이 false가 되는 길입니다. 입력 끝을 위한 출구가 있으니 사용자가 quit을 입력하지 않고 입력을 끝내더라도 nextLine()이 NoSuchElementException을 던지는 일은 없습니다.

quit는 소비되지만 저장되지 않고 뒤 입력은 남는다

두 이메일을 읽어 count를 증가시키고 quit은 저장 전에 소비하여 종료하며 ignored 이메일을 남깁니다. 처리된 접두부와 미소비 꼬리가 단계마다 달라집니다.

SentinelLoop의 소비된 세 줄과 남은 ignored 입력

두 이메일은 count를 증가시키지만 quit는 소비한 즉시 break하며 뒤의 [email protected]은 읽히지 않는다.

INPUT · STATE BOUNDARY

quit까지 소비하고, 뒤 입력은 읽지 않습니다

두 이메일은 개수를 늘립니다. quit 줄은 읽은 즉시 break하므로 개수에 포함되지 않으며 ignored 줄은 Scanner에 남습니다.

SentinelLoop · 초기 count=0
입력 순서소비 여부처리·출력count
[email protected]읽음[email protected]1
[email protected]읽음[email protected]2
quit읽음즉시 break · accepted 출력 없음2 유지
[email protected]읽지 않음처리하지 않음2 유지
quit까지 소비하고, 뒤 입력은 읽지 않습니다 count0→Kim1→Lee2→quit2 즉시 break; ignored 미소비 영역은 제어 흐름과 연결하지 않음 소비 → 개수 반영 → quit에서 끝난다 시작 · count=0 아직 줄을 읽지 않음 첫 이메일 소비 [email protected] accepted 출력 · count=1 둘째 이메일 소비 [email protected] accepted 출력 · count=2 quit 소비 → break accepted 출력 없음 count=2 유지 읽지 않은 입력 · 처리 흐름과 분리 [email protected] 미소비 · 처리 없음 최종 count=2
quit까지 소비하고, 뒤 입력은 읽지 않습니다 count0→Kim1→Lee2→quit2 즉시 break; ignored 미소비 영역은 제어 흐름과 연결하지 않음 실제로 소비한 접두 구간 시작 · count=0 아직 줄을 읽지 않음 첫 이메일 소비 [email protected] accepted 출력 · count=1 둘째 이메일 소비 [email protected] accepted 출력 · count=2 quit 소비 → break accepted 출력 없음 count=2 유지 읽지 않은 뒤 입력 [email protected] Scanner에 그대로 남음 처리·accepted 없음 최종 count=2 break 뒤 입력으로 향하는 선 없음
  1. 시작

    count=0. 다음 줄은 [email protected]입니다.

  2. 첫 이메일 소비

    [email protected]을 출력하고 count=1입니다.

  3. 둘째 이메일 소비

    [email protected]을 출력하고 count=2입니다.

  4. quit 소비 후 즉시 종료

    break합니다. quit는 개수에 포함하지 않아 count=2이며 [email protected]\n은 읽지 않고 남깁니다.

반복 뒤 마지막 출력은 count=2입니다. 개별 이메일을 보존하는 배열은 아직 없습니다. SentinelLoop는 quit를 읽은 직후 반복을 끝내며, 그 뒤에 남은 입력을 처리하지 않습니다.

이 예의 조건

  • quit 뒤 ignored 줄은 읽지 않는다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2count 0→Kim 1→Lee 2→quit 2 즉시 break; ignored 미소비 영역은 제어 흐름과 연결하지 않음
원 도표의 설명과 주석
구분내용
설명count 0→Kim 1→Lee 2→quit 2 즉시 break; ignored 미소비 영역은 제어 흐름과 연결하지 않음
전체 읽기 정보quit는 소비되지만 저장되지 않고 뒤 입력은 남는다 SentinelLoop의 소비된 세 줄과 남은 ignored 입력 두 이메일은 count를 증가시키지만 quit는 소비한 즉시 break하며 뒤의 [email protected]은 읽히지 않는다. INPUT · STATE BOUNDARY quit까지 소비하고, 뒤 입력은 읽지 않습니다 두 이메일은 개수를 늘립니다. quit 줄은 읽은 즉시 break하므로 개수에 포함되지 않으며 ignored 줄은 Scanner에 남습니다. SentinelLoop · 초기 count=0 입력 순서 소비 여부 처리·출력 count [email protected] 읽음 [email protected] 1 [email protected] 읽음 [email protected] 2 quit 읽음 즉시 break · accepted 출력 없음 2 유지 [email protected] 읽지 않음 처리하지 않음 2 유지 시작 count=0 . 다음 줄은 [email protected] 입니다. 첫 이메일 소비 [email protected] 을 출력하고 count=1 입니다. 둘째 이메일 소비 [email protected] 을 출력하고 count=2 입니다. quit 소비 후 즉시 종료 break 합니다. quit 는 개수에 포함하지 않아 count=2 이며 [email protected]\n 은 읽지 않고 남깁니다. 반복 뒤 마지막 출력은 count=2 입니다. 개별 이메일을 보존하는 배열은 아직 없습니다. SentinelLoop 는 quit 를 읽은 직후 반복을 끝내며, 그 뒤에 남은 입력을 처리하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
입력 전

회원 개수를 0으로 시작합니다.

  • count: 0
  • 현재 입력: 아직 없음
  • 저장할 이메일만 개수에 반영한다.
첫 이메일

[email protected]을 읽고 승인합니다.

  • 센티널이 아니므로 개수를 올린다.
둘째 이메일

[email protected]을 읽고 승인합니다.

  • 두 번째 유효 이메일도 저장 대상이다.
센티널 종료

quit을 읽은 뒤 break합니다.

  • 종료 문자열을 세거나 뒤 입력을 읽지 않는다.

quit는 소비되지만 저장되지 않고 뒤 입력은 남는다

두 이메일을 읽어 count를 증가시키고 quit은 저장 전에 소비하여 종료하며 ignored 이메일을 남깁니다. 처리된 접두부와 미소비 꼬리가 단계마다 달라집니다.

SentinelLoop의 소비된 세 줄과 남은 ignored 입력

두 이메일은 count를 증가시키지만 quit는 소비한 즉시 break하며 뒤의 [email protected]은 읽히지 않는다.

INPUT · STATE BOUNDARY

quit까지 소비하고, 뒤 입력은 읽지 않습니다

두 이메일은 개수를 늘립니다. quit 줄은 읽은 즉시 break하므로 개수에 포함되지 않으며 ignored 줄은 Scanner에 남습니다.

SentinelLoop · 초기 count=0
입력 순서소비 여부처리·출력count
[email protected]읽음[email protected]1
[email protected]읽음[email protected]2
quit읽음즉시 break · accepted 출력 없음2 유지
[email protected]읽지 않음처리하지 않음2 유지
quit까지 소비하고, 뒤 입력은 읽지 않습니다 count0→Kim1→Lee2→quit2 즉시 break; ignored 미소비 영역은 제어 흐름과 연결하지 않음 소비 → 개수 반영 → quit에서 끝난다 시작 · count=0 아직 줄을 읽지 않음 첫 이메일 소비 [email protected] accepted 출력 · count=1 둘째 이메일 소비 [email protected] accepted 출력 · count=2 quit 소비 → break accepted 출력 없음 count=2 유지 읽지 않은 입력 · 처리 흐름과 분리 [email protected] 미소비 · 처리 없음 최종 count=2
quit까지 소비하고, 뒤 입력은 읽지 않습니다 count0→Kim1→Lee2→quit2 즉시 break; ignored 미소비 영역은 제어 흐름과 연결하지 않음 실제로 소비한 접두 구간 시작 · count=0 아직 줄을 읽지 않음 첫 이메일 소비 [email protected] accepted 출력 · count=1 둘째 이메일 소비 [email protected] accepted 출력 · count=2 quit 소비 → break accepted 출력 없음 count=2 유지 읽지 않은 뒤 입력 [email protected] Scanner에 그대로 남음 처리·accepted 없음 최종 count=2 break 뒤 입력으로 향하는 선 없음
  1. 시작

    count=0. 다음 줄은 [email protected]입니다.

  2. 첫 이메일 소비

    [email protected]을 출력하고 count=1입니다.

  3. 둘째 이메일 소비

    [email protected]을 출력하고 count=2입니다.

  4. quit 소비 후 즉시 종료

    break합니다. quit는 개수에 포함하지 않아 count=2이며 [email protected]\n은 읽지 않고 남깁니다.

반복 뒤 마지막 출력은 count=2입니다. 개별 이메일을 보존하는 배열은 아직 없습니다. SentinelLoop는 quit를 읽은 직후 반복을 끝내며, 그 뒤에 남은 입력을 처리하지 않습니다.

이 예의 조건

  • quit 뒤 ignored 줄은 읽지 않는다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2count 0→Kim 1→Lee 2→quit 2 즉시 break; ignored 미소비 영역은 제어 흐름과 연결하지 않음
원 도표의 설명과 주석
구분내용
설명count 0→Kim 1→Lee 2→quit 2 즉시 break; ignored 미소비 영역은 제어 흐름과 연결하지 않음
전체 읽기 정보quit는 소비되지만 저장되지 않고 뒤 입력은 남는다 SentinelLoop의 소비된 세 줄과 남은 ignored 입력 두 이메일은 count를 증가시키지만 quit는 소비한 즉시 break하며 뒤의 [email protected]은 읽히지 않는다. INPUT · STATE BOUNDARY quit까지 소비하고, 뒤 입력은 읽지 않습니다 두 이메일은 개수를 늘립니다. quit 줄은 읽은 즉시 break하므로 개수에 포함되지 않으며 ignored 줄은 Scanner에 남습니다. SentinelLoop · 초기 count=0 입력 순서 소비 여부 처리·출력 count [email protected] 읽음 [email protected] 1 [email protected] 읽음 [email protected] 2 quit 읽음 즉시 break · accepted 출력 없음 2 유지 [email protected] 읽지 않음 처리하지 않음 2 유지 시작 count=0 . 다음 줄은 [email protected] 입니다. 첫 이메일 소비 [email protected] 을 출력하고 count=1 입니다. 둘째 이메일 소비 [email protected] 을 출력하고 count=2 입니다. quit 소비 후 즉시 종료 break 합니다. quit 는 개수에 포함하지 않아 count=2 이며 [email protected]\n 은 읽지 않고 남깁니다. 반복 뒤 마지막 출력은 count=2 입니다. 개별 이메일을 보존하는 배열은 아직 없습니다. SentinelLoop 는 quit 를 읽은 직후 반복을 끝내며, 그 뒤에 남은 입력을 처리하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
입력 전

회원 개수를 0으로 시작합니다.

  • count: 0
  • 현재 입력: 아직 없음
  • 저장할 이메일만 개수에 반영한다.
첫 이메일

[email protected]을 읽고 승인합니다.

  • 센티널이 아니므로 개수를 올린다.
둘째 이메일

[email protected]을 읽고 승인합니다.

  • 두 번째 유효 이메일도 저장 대상이다.
센티널 종료

quit을 읽은 뒤 break합니다.

  • 종료 문자열을 세거나 뒤 입력을 읽지 않는다.

메뉴 번호로 기능 고르기

대화형 프로그램은 흔히 메뉴 번호를 받아 기능을 고릅니다. 다음 예제는 1이면 회원을 등록하고, 2면 등록한 회원 수를 보여 주고, 0이면 끝냅니다. 회원 등록에는 이메일과 나이가 필요하므로, 메뉴 1을 고른 경우에만 다음 두 줄을 더 읽습니다.

src/MenuConversation.java
import java.util.Scanner;

public final class MenuConversation {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(
            "1\n[email protected]\n40\n2\n9\n0\n"
        );
        int saved = 0;

        while (scanner.hasNextLine()) {
            String menu = scanner.nextLine().strip();
            switch (menu) {
                case "1" -> {
                    String email = scanner.nextLine().strip();
                    int age = Integer.parseInt(scanner.nextLine().strip());
                    saved++;
                    System.out.println("saved=" + email + ":" + age);
                }
                case "2" -> System.out.println("count=" + saved);
                case "0" -> {
                    System.out.println("bye");
                    return;
                }
                default -> System.out.println("unknown.menu=" + menu);
            }
        }
    }
}
[email protected]:40
count=1
unknown.menu=9
bye

입력은 여섯 줄이지만 반복은 네 번만 돕니다. 1회차에 메뉴 1을 읽은 뒤, 같은 회차 안에서 [email protected]과 40까지 읽기 때문입니다. 반복 한 번이 소비하는 줄 수는 고른 메뉴에 따라 달라집니다.

회차메뉴 줄같은 회차에 더 읽은 줄출력
11[email protected], 40[email protected]:40
22없음count=1
39없음unknown.menu=9
40없음bye

메뉴 0에서 break가 아니라 return을 쓴 데에도 이유가 있습니다. switch 안에서 break를 쓰면 switch만 빠져나오고 바깥의 while은 계속 돕니다. return은 main 메서드 자체를 끝내므로 switch와 while을 한꺼번에 벗어납니다. 이 차이는 연습 문제 2에서 직접 확인합니다.

이 구조에는 약점이 있습니다. 메뉴 1은 뒤에 이메일과 나이 두 줄이 반드시 온다고 가정합니다. 입력이 1\[email protected]\n에서 끝나 버리면 나이를 읽는 nextLine()이 NoSuchElementException을 던지고, 나이 자리에 forty가 오면 NumberFormatException으로 프로그램이 끝납니다. 이처럼 메뉴마다 몇 줄을 더 읽는지는 입력 형식의 일부입니다. 상세 입력을 읽기 전마다 hasNextLine()을 확인하고 변환을 try/catch로 감싸서 고칠 수도 있지만, 다음 절에서는 회원 정보 전체를 한 줄 명령으로 받는 방법을 씁니다. 한 회차가 언제나 정확히 한 줄만 소비하므로, 형식이 틀린 줄은 그 줄만 버리고 다음 줄에서 다시 시작할 수 있습니다.

한 줄 명령을 단계별로 검사하기

앞 글에서 만든 register 이메일 나이 형식에 지금까지 배운 것을 모두 적용해 보겠습니다. 한 줄을 받으면 다음 순서로 검사하고, 어느 단계에서든 실패하면 그 이유를 출력한 뒤 다음 줄로 넘어갑니다.

  1. 읽을 줄이 남아 있는가? 없으면 반복을 끝냅니다.
  2. 종료 명령 quit이나 조회 명령 list인가? 그렇다면 그 명령을 처리합니다.
  3. 공백으로 나눈 조각이 세 개이고 첫 조각이 register인가?
  4. 나이 문자열을 정수로 바꿀 수 있는가?
  5. 바꾼 나이가 14 이상 120 이하인가?
  6. 모든 검사를 통과했다면 그때 비로소 저장 상태를 바꿉니다.

순서가 중요한 이유는 앞 단계가 뒤 단계를 안전하게 만들어 주기 때문입니다. 3단계에서 조각이 세 개인지 확인했기 때문에 4단계에서 parts[2]를 읽어도 실행 오류가 나지 않습니다. 4단계에서 변환에 성공했기 때문에 5단계에서 정수로 비교할 수 있습니다. 그리고 상태를 바꾸는 일을 맨 마지막에 두었기 때문에, 중간에 거절된 입력은 상태에 아무 흔적도 남기지 않습니다. 만약 회원 수를 먼저 늘린 뒤 범위 검사에서 거절했다면, 저장되지도 않은 회원이 숫자에만 남았을 것입니다.

src/ValidatedMemberInput.java
import java.util.Scanner;

public final class ValidatedMemberInput {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(
            "register [email protected] forty\nregister [email protected] 0\nregister [email protected] 45\nlist\nquit\n"
        );
        int count = 0;
        int total = 0;

        while (scanner.hasNextLine()) {
            String line = scanner.nextLine().strip();
            if (line.equals("quit")) break;
            if (line.equals("list")) {
                System.out.println("summary=" + count + ":" + total);
                continue;
            }

            String[] parts = line.split("\\s+");
            if (parts.length != 3 || !parts[0].equals("register")) {
                System.out.println("command.error");
                continue;
            }

            int age;
            try {
                age = Integer.parseInt(parts[2]);
            } catch (NumberFormatException error) {
                System.out.println("number.error");
                continue;
            }
            if (age < 14 || age > 120) {
                System.out.println("range.error");
                continue;
            }

            count++;
            total += age;
            System.out.println("saved=" + parts[1]);
        }
    }
}
number.error
range.error
[email protected]
summary=1:45

RetryNumber와 달리 이번에는 try 블록에 parseInt 한 줄만 넣었습니다. try 블록이 길면 그 안의 어느 문장에서 NumberFormatException이 나든 같은 catch로 들어가므로, 메시지가 실제 원인과 어긋날 수 있습니다. 예를 들어 나중에 같은 블록에 다른 숫자 변환을 추가하면, 그 변환이 실패해도 number.error로 안내하게 됩니다. try를 실패할 수 있는 한 줄로 좁혀 두면, 이 catch가 어떤 실패를 처리하는지가 코드만 봐도 분명합니다.

try를 좁히자 age를 try 바깥에 선언해야 했습니다. try 안에서 선언하면 블록이 끝날 때 이름이 사라져서 범위 검사에서 쓸 수 없기 때문입니다. 그래서 int age;로 선언만 해 두고 try 안에서 값을 대입합니다. 이때 catch의 continue는 빼면 안 됩니다. continue가 없으면 변환에 실패한 경우에도 실행이 범위 검사로 내려가는데, 그 경로에서는 age에 값이 대입된 적이 없습니다. 자바 컴파일러는 이런 경로를 찾아내 variable age might not have been initialized 오류를 내고 컴파일을 거부합니다. continue가 있어야 컴파일러도 'age를 읽는 시점에는 반드시 값이 들어 있다'고 판단할 수 있습니다.

register 검증을 모두 통과한 뒤에만 누적 상태가 바뀐다

형태 검사→정수 변환→14–120 범위 검사 모두를 통과한 뒤에만 count와 total을 변경합니다. 각 거절 지점에서 상태를 보존하는 실제 입력 게이트입니다.

ValidatedMemberInput의 register 후보 검증과 다음 입력으로의 복귀

quit와 list를 먼저 처리한 뒤 남은 명령은 형태, 정수 변환, 14–120 범위를 모두 통과해야 count와 total을 바꾼다.

INPUT · STATE BOUNDARY

검증 실패는 상태를 보존하고 다음 입력으로

quit와 list를 먼저 처리한 뒤, 남은 한 줄은 명령 형태·정수 변환·나이 범위를 모두 통과해야 count와 total을 바꿉니다.

register 검증을 모두 통과한 뒤에만 누적 상태가 바뀐다register 후보의 명령 오류, 숫자 변환 오류, 범위 오류는 상태를 보존하고 다음 입력으로 복귀하며 성공한 입력만 count와 total을 갱신한다. falsetrue예외변환 성공falsetruequit·list 처리 후line.split("\\s+")명령 형태가 맞는가?parts.length == 3parts[0].equals("register")command.error상태 불변Integer.parseInt(parts[2])정수로 변환number.error상태 불변14–120 범위인가?age >= 14 && age <= 120range.error상태 불변count++ · total += agesaved=<email> 출력다음 입력 존재 검사hasNextLine()
검증 실패는 상태를 보존하고 다음 입력으로 quit/list 처리 후 형태→정수 변환→14–120 범위; 세 오류는 상태 불변으로 다음 입력, 모두 통과한 경우만 누적 quit·list 처리 후 남은 입력을 분리 split("\\s+") 3개 · 첫 항목 register ? 거짓 command.error count·total 불변 참 parseInt(parts[2]) 정수 변환 예외 number.error count·total 불변 변환 성공 14 ≤ age ≤ 120 범위 안인가? 거짓 range.error count·total 불변 참 count++ total += age saved=<email> 출력 다음 입력 존재 검사 hasNextLine() EOF면 종료

범위: quit·list를 처리한 뒤 남은 한 줄

  1. 명령 형태

    split("\\s+") 뒤 항목이 3개이고 첫 항목이 register인지 확인합니다. 아니면 command.error 후 다음 입력으로 갑니다.

  2. 숫자 변환

    Integer.parseInt(parts[2])가 NumberFormatException을 던지면 number.error 후 다음 입력으로 갑니다.

  3. 범위 검사

    14 <= age && age <= 120인지 확인합니다. 아니면 range.error 후 다음 입력으로 갑니다.

  4. 상태 반영

    count++, total += age를 실행하고 saved=<email>을 출력한 뒤 다음 입력으로 갑니다.

세 오류 경로는 count와 total을 바꾸지 않습니다. quit뿐 아니라 EOF로 hasNextLine()==false가 되어도 반복이 끝납니다.

이 예의 조건

  • quit·list 처리 뒤 register 후보 검사이며 saved는 출력입니다.
관계도의 보충 설명
원 도표의 설명과 주석
구분내용
전체 읽기 정보register 검증을 모두 통과한 뒤에만 누적 상태가 바뀐다 ValidatedMemberInput의 register 후보 검증과 다음 입력으로의 복귀 quit와 list를 먼저 처리한 뒤 남은 명령은 형태, 정수 변환, 14–120 범위를 모두 통과해야 count와 total을 바꾼다. INPUT · STATE BOUNDARY 검증 실패는 상태를 보존하고 다음 입력으로 quit와 list를 먼저 처리한 뒤, 남은 한 줄은 명령 형태·정수 변환·나이 범위를 모두 통과해야 count와 total을 바꿉니다. 범위: quit · list 를 처리한 뒤 남은 한 줄 명령 형태 split("\\s+") 뒤 항목이 3개이고 첫 항목이 register 인지 확인합니다. 아니면 command.error 후 다음 입력으로 갑니다. 숫자 변환 Integer.parseInt(parts[2]) 가 NumberFormatException 을 던지면 number.error 후 다음 입력으로 갑니다. 범위 검사 14 <= age && age <= 120 인지 확인합니다. 아니면 range.error 후 다음 입력으로 갑니다. 상태 반영 count++ , total += age 를 실행하고 saved=<email> 을 출력한 뒤 다음 입력으로 갑니다. 세 오류 경로는 count 와 total 을 바꾸지 않습니다. quit 뿐 아니라 EOF로 hasNextLine()==false 가 되어도 반복이 끝납니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
명령 형태

parts 3개와 register 이름을 검사합니다.

  • 불일치: command.error
  • 상태: count,total 유지
  • 잘못된 명령은 숫자 변환 전에 돌아갑니다.
숫자와 범위

parseInt 후 14–120 범위를 검사합니다.

  • 변환 실패: number.error, 상태 유지
  • 범위 실패: range.error, 상태 유지
  • 모든 검증 통과 전에는 누적값을 바꾸지 않습니다.
성공 갱신

검증을 통과한 age에 대해서만 count++와 total+=age를 수행합니다.

  • 출력: saved=email
  • 다음: hasNextLine 검사
  • 실패 경로도 성공 경로도 다음 입력 검사로 복귀합니다.

register 검증을 모두 통과한 뒤에만 누적 상태가 바뀐다

형태 검사→정수 변환→14–120 범위 검사 모두를 통과한 뒤에만 count와 total을 변경합니다. 각 거절 지점에서 상태를 보존하는 실제 입력 게이트입니다.

ValidatedMemberInput의 register 후보 검증과 다음 입력으로의 복귀

quit와 list를 먼저 처리한 뒤 남은 명령은 형태, 정수 변환, 14–120 범위를 모두 통과해야 count와 total을 바꾼다.

INPUT · STATE BOUNDARY

검증 실패는 상태를 보존하고 다음 입력으로

quit와 list를 먼저 처리한 뒤, 남은 한 줄은 명령 형태·정수 변환·나이 범위를 모두 통과해야 count와 total을 바꿉니다.

register 검증을 모두 통과한 뒤에만 누적 상태가 바뀐다register 후보의 명령 오류, 숫자 변환 오류, 범위 오류는 상태를 보존하고 다음 입력으로 복귀하며 성공한 입력만 count와 total을 갱신한다. falsetrue예외변환 성공falsetruequit·list 처리 후line.split("\\s+")명령 형태가 맞는가?parts.length == 3parts[0].equals("register")command.error상태 불변Integer.parseInt(parts[2])정수로 변환number.error상태 불변14–120 범위인가?age >= 14 && age <= 120range.error상태 불변count++ · total += agesaved=<email> 출력다음 입력 존재 검사hasNextLine()
검증 실패는 상태를 보존하고 다음 입력으로 quit/list 처리 후 형태→정수 변환→14–120 범위; 세 오류는 상태 불변으로 다음 입력, 모두 통과한 경우만 누적 quit·list 처리 후 남은 입력을 분리 split("\\s+") 3개 · 첫 항목 register ? 거짓 command.error count·total 불변 참 parseInt(parts[2]) 정수 변환 예외 number.error count·total 불변 변환 성공 14 ≤ age ≤ 120 범위 안인가? 거짓 range.error count·total 불변 참 count++ total += age saved=<email> 출력 다음 입력 존재 검사 hasNextLine() EOF면 종료

범위: quit·list를 처리한 뒤 남은 한 줄

  1. 명령 형태

    split("\\s+") 뒤 항목이 3개이고 첫 항목이 register인지 확인합니다. 아니면 command.error 후 다음 입력으로 갑니다.

  2. 숫자 변환

    Integer.parseInt(parts[2])가 NumberFormatException을 던지면 number.error 후 다음 입력으로 갑니다.

  3. 범위 검사

    14 <= age && age <= 120인지 확인합니다. 아니면 range.error 후 다음 입력으로 갑니다.

  4. 상태 반영

    count++, total += age를 실행하고 saved=<email>을 출력한 뒤 다음 입력으로 갑니다.

세 오류 경로는 count와 total을 바꾸지 않습니다. quit뿐 아니라 EOF로 hasNextLine()==false가 되어도 반복이 끝납니다.

이 예의 조건

  • quit·list 처리 뒤 register 후보 검사이며 saved는 출력입니다.

관계도의 보충 설명

원 도표의 설명과 주석
구분내용
전체 읽기 정보register 검증을 모두 통과한 뒤에만 누적 상태가 바뀐다 ValidatedMemberInput의 register 후보 검증과 다음 입력으로의 복귀 quit와 list를 먼저 처리한 뒤 남은 명령은 형태, 정수 변환, 14–120 범위를 모두 통과해야 count와 total을 바꾼다. INPUT · STATE BOUNDARY 검증 실패는 상태를 보존하고 다음 입력으로 quit와 list를 먼저 처리한 뒤, 남은 한 줄은 명령 형태·정수 변환·나이 범위를 모두 통과해야 count와 total을 바꿉니다. 범위: quit · list 를 처리한 뒤 남은 한 줄 명령 형태 split("\\s+") 뒤 항목이 3개이고 첫 항목이 register 인지 확인합니다. 아니면 command.error 후 다음 입력으로 갑니다. 숫자 변환 Integer.parseInt(parts[2]) 가 NumberFormatException 을 던지면 number.error 후 다음 입력으로 갑니다. 범위 검사 14 <= age && age <= 120 인지 확인합니다. 아니면 range.error 후 다음 입력으로 갑니다. 상태 반영 count++ , total += age 를 실행하고 saved=<email> 을 출력한 뒤 다음 입력으로 갑니다. 세 오류 경로는 count 와 total 을 바꾸지 않습니다. quit 뿐 아니라 EOF로 hasNextLine()==false 가 되어도 반복이 끝납니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
명령 형태

parts 3개와 register 이름을 검사합니다.

  • 불일치: command.error
  • 상태: count,total 유지
  • 잘못된 명령은 숫자 변환 전에 돌아갑니다.
숫자와 범위

parseInt 후 14–120 범위를 검사합니다.

  • 변환 실패: number.error, 상태 유지
  • 범위 실패: range.error, 상태 유지
  • 모든 검증 통과 전에는 누적값을 바꾸지 않습니다.
성공 갱신

검증을 통과한 age에 대해서만 count++와 total+=age를 수행합니다.

  • 출력: saved=email
  • 다음: hasNextLine 검사
  • 실패 경로도 성공 경로도 다음 입력 검사로 복귀합니다.

다섯 줄의 입력을 처리하는 동안 count와 total이 어떻게 변하는지 표로 보면, 거절된 입력이 상태를 전혀 바꾸지 않는다는 것을 확인할 수 있습니다.

입력출력처리 뒤 count처리 뒤 total
register [email protected] fortynumber.error00
register [email protected] 0range.error00
register [email protected] 45[email protected]145
listsummary=1:45145
quit없음, 반복 종료145

실패한 입력은 count와 total을 그대로 남긴다

숫자 오류와 범위 오류 뒤 count/total은 0을 유지하고 정상 입력에서 1/45가 되며 list와 quit은 바꾸지 않습니다. 같은 반복의 상태 추적입니다.

ValidatedMemberInput의 다섯 입력 뒤 count와 total 상태

숫자와 범위 오류는 상태를 보존하고 45의 정상 등록만 count 1과 total 45를 만든다. list와 quit는 누적 상태를 바꾸지 않는다.

INPUT · STATE BOUNDARY

다섯 입력 뒤 바뀌는 상태와 유지되는 상태

숫자·범위 오류는 count=0과 total=0을 유지합니다. 나이45의 정상 입력만 상태를 갱신하며 list와 quit는 값을 바꾸지 않습니다.

ValidatedMemberInput · 초기 count=0, total=0
소비한 입력관찰 출력처리 후 count처리 후 total
register [email protected] fortynumber.error00
register [email protected] 0range.error00
register [email protected] 45[email protected]145
listsummary=1:45145
quit출력 없음 · 반복 종료145
다섯 입력 뒤 바뀌는 상태와 유지되는 상태 숫자 오류0/0→범위 오류0/0→45등록1/45→list1/45→quit1/45, saved는 출력이며 회원배열 저장 아님 시작 상태 count=0 · total=0 숫자 오류 register · forty number.error count=0 · total=0 범위 오류 register · 0 range.error count=0 · total=0 유효한 45 등록 [email protected] count=1 · total=45 list · 조회만 summary=1:45 count=1 · total=45 quit · 반복 종료 출력 없음 count=1 · total=45 register의 이메일은 모두 [email protected]; 선은 다음 입력 뒤 상태
다섯 입력 뒤 바뀌는 상태와 유지되는 상태 숫자 오류0/0→범위 오류0/0→45등록1/45→list1/45→quit1/45, saved는 출력이며 회원배열 저장 아님 시작 상태 count=0 · total=0 숫자 오류 register · forty number.error count=0 · total=0 범위 오류 register · 0 range.error count=0 · total=0 유효한 45 등록 [email protected] count=1 · total=45 list · 조회만 summary=1:45 count=1 · total=45 quit · 반복 종료 출력 없음 count=1 · total=45 register의 이메일은 모두 [email protected]

count=0, total=0에서 시작

  1. register [email protected] forty

    출력: number.error · 처리 후 count=0, total=0

  2. register [email protected] 0

    출력: range.error · 처리 후 count=0, total=0

  3. register [email protected] 45

    출력: [email protected] · 처리 후 count=1, total=45

  4. list

    출력: summary=1:45 · 처리 후 count=1, total=45

  5. quit

    출력: 출력 없음 · 반복 종료 · 처리 후 count=1, total=45

여기서 저장되는 상태는 count와 total뿐입니다. saved= 출력이 개별 회원 배열을 보존한다는 뜻은 아닙니다.

이 예의 조건

관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2숫자 오류 0/0→범위 오류 0/0→45 등록 1/45→list 1/45→quit 1/45, saved는 출력이며 회원 배열 저장 아님
원 도표의 설명과 주석
구분내용
설명숫자 오류 0/0→범위 오류 0/0→45 등록 1/45→list 1/45→quit 1/45, saved는 출력이며 회원 배열 저장 아님
전체 읽기 정보실패한 입력은 count와 total을 그대로 남긴다 ValidatedMemberInput의 다섯 입력 뒤 count와 total 상태 숫자와 범위 오류는 상태를 보존하고 45의 정상 등록만 count 1과 total 45를 만든다. list와 quit는 누적 상태를 바꾸지 않는다. INPUT · STATE BOUNDARY 다섯 입력 뒤 바뀌는 상태와 유지되는 상태 숫자·범위 오류는 count=0과 total=0을 유지합니다. 나이 45의 정상 입력만 상태를 갱신하며 list와 quit는 값을 바꾸지 않습니다. ValidatedMemberInput · 초기 count=0, total=0 소비한 입력 관찰 출력 처리 후 count 처리 후 total register [email protected] forty number.error 0 0 register [email protected] 0 range.error 0 0 register [email protected] 45 [email protected] 1 45 list summary=1:45 1 45 quit 출력 없음 · 반복 종료 1 45 count=0, total=0 에서 시작 register [email protected] forty 출력: number.error · 처리 후 count=0, total=0 register [email protected] 0 출력: range.error · 처리 후 count=0, total=0 register [email protected] 45 출력: [email protected] · 처리 후 count=1, total=45 list 출력: summary=1:45 · 처리 후 count=1, total=45 quit 출력: 출력 없음 · 반복 종료 · 처리 후 count=1, total=45 여기서 저장되는 상태는 count 와 total 뿐입니다. saved= 출력이 개별 회원 배열을 보존한다는 뜻은 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
실패 두 입력

forty 변환 오류와 0 범위 오류를 처리합니다.

  • 출력: number.error,range.error
  • 상태: count 0, total 0
  • 거절된 입력은 누적 상태를 보존합니다.
45 등록

유효한 45를 누적합니다.

  • 검증 성공 입력만 상태를 증가시킵니다.
조회와 종료

list는 summary 1:45, quit은 출력 없이 끝냅니다.

  • list 뒤: 1/45
  • quit 뒤: 1/45
  • 조회와 반복 종료는 등록 상태를 변경하지 않습니다.

실패한 입력은 count와 total을 그대로 남긴다

숫자 오류와 범위 오류 뒤 count/total은 0을 유지하고 정상 입력에서 1/45가 되며 list와 quit은 바꾸지 않습니다. 같은 반복의 상태 추적입니다.

ValidatedMemberInput의 다섯 입력 뒤 count와 total 상태

숫자와 범위 오류는 상태를 보존하고 45의 정상 등록만 count 1과 total 45를 만든다. list와 quit는 누적 상태를 바꾸지 않는다.

INPUT · STATE BOUNDARY

다섯 입력 뒤 바뀌는 상태와 유지되는 상태

숫자·범위 오류는 count=0과 total=0을 유지합니다. 나이45의 정상 입력만 상태를 갱신하며 list와 quit는 값을 바꾸지 않습니다.

ValidatedMemberInput · 초기 count=0, total=0
소비한 입력관찰 출력처리 후 count처리 후 total
register [email protected] fortynumber.error00
register [email protected] 0range.error00
register [email protected] 45[email protected]145
listsummary=1:45145
quit출력 없음 · 반복 종료145
다섯 입력 뒤 바뀌는 상태와 유지되는 상태 숫자 오류0/0→범위 오류0/0→45등록1/45→list1/45→quit1/45, saved는 출력이며 회원배열 저장 아님 시작 상태 count=0 · total=0 숫자 오류 register · forty number.error count=0 · total=0 범위 오류 register · 0 range.error count=0 · total=0 유효한 45 등록 [email protected] count=1 · total=45 list · 조회만 summary=1:45 count=1 · total=45 quit · 반복 종료 출력 없음 count=1 · total=45 register의 이메일은 모두 [email protected]; 선은 다음 입력 뒤 상태
다섯 입력 뒤 바뀌는 상태와 유지되는 상태 숫자 오류0/0→범위 오류0/0→45등록1/45→list1/45→quit1/45, saved는 출력이며 회원배열 저장 아님 시작 상태 count=0 · total=0 숫자 오류 register · forty number.error count=0 · total=0 범위 오류 register · 0 range.error count=0 · total=0 유효한 45 등록 [email protected] count=1 · total=45 list · 조회만 summary=1:45 count=1 · total=45 quit · 반복 종료 출력 없음 count=1 · total=45 register의 이메일은 모두 [email protected]

count=0, total=0에서 시작

  1. register [email protected] forty

    출력: number.error · 처리 후 count=0, total=0

  2. register [email protected] 0

    출력: range.error · 처리 후 count=0, total=0

  3. register [email protected] 45

    출력: [email protected] · 처리 후 count=1, total=45

  4. list

    출력: summary=1:45 · 처리 후 count=1, total=45

  5. quit

    출력: 출력 없음 · 반복 종료 · 처리 후 count=1, total=45

여기서 저장되는 상태는 count와 total뿐입니다. saved= 출력이 개별 회원 배열을 보존한다는 뜻은 아닙니다.

이 예의 조건

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2숫자 오류 0/0→범위 오류 0/0→45 등록 1/45→list 1/45→quit 1/45, saved는 출력이며 회원 배열 저장 아님
원 도표의 설명과 주석
구분내용
설명숫자 오류 0/0→범위 오류 0/0→45 등록 1/45→list 1/45→quit 1/45, saved는 출력이며 회원 배열 저장 아님
전체 읽기 정보실패한 입력은 count와 total을 그대로 남긴다 ValidatedMemberInput의 다섯 입력 뒤 count와 total 상태 숫자와 범위 오류는 상태를 보존하고 45의 정상 등록만 count 1과 total 45를 만든다. list와 quit는 누적 상태를 바꾸지 않는다. INPUT · STATE BOUNDARY 다섯 입력 뒤 바뀌는 상태와 유지되는 상태 숫자·범위 오류는 count=0과 total=0을 유지합니다. 나이 45의 정상 입력만 상태를 갱신하며 list와 quit는 값을 바꾸지 않습니다. ValidatedMemberInput · 초기 count=0, total=0 소비한 입력 관찰 출력 처리 후 count 처리 후 total register [email protected] forty number.error 0 0 register [email protected] 0 range.error 0 0 register [email protected] 45 [email protected] 1 45 list summary=1:45 1 45 quit 출력 없음 · 반복 종료 1 45 count=0, total=0 에서 시작 register [email protected] forty 출력: number.error · 처리 후 count=0, total=0 register [email protected] 0 출력: range.error · 처리 후 count=0, total=0 register [email protected] 45 출력: [email protected] · 처리 후 count=1, total=45 list 출력: summary=1:45 · 처리 후 count=1, total=45 quit 출력: 출력 없음 · 반복 종료 · 처리 후 count=1, total=45 여기서 저장되는 상태는 count 와 total 뿐입니다. saved= 출력이 개별 회원 배열을 보존한다는 뜻은 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
실패 두 입력

forty 변환 오류와 0 범위 오류를 처리합니다.

  • 출력: number.error,range.error
  • 상태: count 0, total 0
  • 거절된 입력은 누적 상태를 보존합니다.
45 등록

유효한 45를 누적합니다.

  • 검증 성공 입력만 상태를 증가시킵니다.
조회와 종료

list는 summary 1:45, quit은 출력 없이 끝냅니다.

  • list 뒤: 1/45
  • quit 뒤: 1/45
  • 조회와 반복 종료는 등록 상태를 변경하지 않습니다.

이 프로그램은 회원 수와 나이 합계만 기억합니다. 그래서 list는 몇 명이 등록됐는지는 알려 주지만, 누가 등록됐는지는 보여 줄 수 없습니다. [email protected]도 출력일 뿐 이메일을 보관하지는 않습니다. 회원 한 명 한 명의 정보를 보관하려면 여러 값을 순서대로 담는 저장 공간이 필요합니다.

정리

  • try 블록에서 예외가 나면 블록의 나머지를 건너뛰고 해당 catch 블록으로 이동합니다. 예외를 붙잡으면 프로그램은 끝나지 않고 try/catch 다음 문장부터 계속 실행됩니다.
  • 숫자로 바꿀 수 없는 입력(형식 오류)과 숫자지만 허용 범위를 벗어난 입력(범위 오류)은 다른 실패이므로 다른 메시지로 안내합니다.
  • 센티널은 데이터가 될 수 없는 값으로 고릅니다. 센티널을 검사하는 위치가 저장보다 앞이어야 센티널 자체가 데이터로 처리되지 않습니다.
  • 검사는 앞 단계가 뒤 단계를 안전하게 만드는 순서로 배치하고, 상태 변경은 모든 검사를 통과한 뒤 맨 마지막에 합니다.
  • try는 실패할 수 있는 문장만 감싸도록 좁게 둡니다. 그 때문에 변수를 try 밖에 선언했다면, catch가 continue나 return처럼 실행을 다른 곳으로 보내거나 그 변수에 값을 대입해 두어야 컴파일러가 뒤에서 변수를 읽도록 허락합니다.

연습 문제

  1. "x\n13\n121\n60\n"을 읽어 숫자 오류와 범위 오류를 각각 출력하고, 올바른 나이 60을 받으면 종료하는 MemberAgeRetry를 작성하세요.
  2. MenuConversation에서 메뉴 0의 return을 break로 바꾸고, 입력을 "0\n2\n"으로 바꾸면 무엇이 출력될까요?
  3. ValidatedMemberInput의 입력을 "register [email protected] 45 extra\nREGISTER [email protected] 45\nregister [email protected] 2147483648\nlist\n"으로 바꾸면 무엇이 출력될까요? 각 줄이 어느 검사에서 걸리는지도 설명하세요.
해설 보기
  1. RetryNumber와 같은 구조에서 출력 형식만 바꾸면 됩니다.
src/MemberAgeRetry.java
import java.util.Scanner;

public final class MemberAgeRetry {
    public static void main(String[] args) {
        Scanner scanner = new Scanner("x\n13\n121\n60\n");
        while (scanner.hasNextLine()) {
            String raw = scanner.nextLine().strip();
            try {
                int age = Integer.parseInt(raw);
                if (age < 14 || age > 120) {
                    System.out.println("range=" + raw);
                    continue;
                }
                System.out.println("age=" + age);
                break;
            } catch (NumberFormatException error) {
                System.out.println("number=" + raw);
            }
        }
    }
}
number=x
range=13
range=121
age=60

x는 변환 단계에서 실패해 catch로 갑니다. 13과 121은 변환에는 성공하지만 각각 범위의 아래쪽과 위쪽을 벗어나 range=를 출력합니다. 경곗값 바로 바깥의 두 값을 모두 넣어 보는 것은 범위 조건의 <와 >가 맞게 쓰였는지 확인하는 좋은 방법입니다. 처음으로 올바른 값 60을 만나면 break로 끝납니다.

  1. 출력은 다음과 같습니다.
bye
count=0

break는 switch만 빠져나오므로 bye를 출력한 뒤에도 while이 계속 돕니다. 다음 줄 2를 읽어 count=0을 출력하고, 더 읽을 줄이 없어서야 반복이 끝납니다. 종료 메뉴를 고른 뒤에도 프로그램이 계속 입력을 처리하는 버그입니다. return을 쓰거나, 반복 조건에 running 같은 변수를 두고 메뉴 0에서 false로 바꾸는 방법으로 고칠 수 있습니다.

  1. 출력은 다음과 같습니다.
command.error
command.error
number.error
summary=0:0

첫 줄은 공백으로 나누면 조각이 네 개이므로 3단계에서 걸립니다. 둘째 줄은 조각은 세 개지만 equals가 대소문자를 구분하므로 REGISTER는 register와 다르다고 판단해 역시 3단계에서 걸립니다. 셋째 줄의 2147483648은 숫자 모양이지만 int의 최댓값 2147483647보다 커서 parseInt가 NumberFormatException을 던지므로 4단계에서 걸립니다. 세 줄 모두 거절됐으므로 list는 summary=0:0을 출력합니다. 입력에 quit이 없지만, 더 읽을 줄이 없으면 hasNextLine()이 false가 되어 반복이 정상적으로 끝납니다.

다음 글

배열과 인덱스에서는 같은 타입의 값 여러 개를 순서대로 담는 배열을 배웁니다. 배열이 있으면 회원 수와 합계뿐 아니라 회원 한 명 한 명의 이메일과 나이를 보관하고 다시 출력할 수 있습니다.