본문 바로가기

입력 검증과 반복

센티널·메뉴 반복·형식 및 범위 검증을 결합해 잘못된 입력 뒤에도 계속 동작하는 회원가입 대화를 만듭니다.

대화형 프로그램은 한 번의 정상 입력보다 실패 뒤의 다음 행동이 중요합니다.

숫자가 아닌 나이, 범위 밖 값, 알 수 없는 메뉴를 만났을 때 예외로 끝낼지 같은 메뉴를 다시 보여 줄지 정해야 합니다.

센티널은 일반 데이터와 구분되는 종료 신호입니다.


try/catch의 가장 작은 의미

Integer.parseInt는 문자열이 정수 모양이 아니면 NumberFormatException이라는 실행 중 오류를 알립니다.

try에는 실패할 수 있는 변환을 두고, catch에는 그 오류가 생겼을 때 실행할 문장을 둡니다.

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("숫자를 입력하세요");
        }
    }
}

raw가 "45"이면 try 안의 출력이 실행됩니다.

"forty"로 바꾸면 변환 뒤 문장은 건너뛰고 catch가 실행됩니다.

예외의 계층과 전파 규칙은 ch12에서 깊게 다루며, 여기서는 사용자 입력 한 건이 잘못돼도 대화를 계속하기 위한 최소 형태만 사용합니다.


변환 실패와 입력 복구

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"

두 번째 줄 45는 정상이어도 첫 줄에서 프로그램이 종료돼 읽히지 않습니다.

한 입력의 사용자 오류를 전체 CLI 장애로 취급한 결과입니다.

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

형식 변환과 값 범위는 서로 다른 실패입니다.

try 블록은 parseInt에 필요한 만큼만 좁게 두고, 범위 검사는 변환 성공 뒤에 둡니다.

유효한 값을 saved= 문자열로 출력한 뒤 break로 재입력을 끝냅니다. 이 작은 예제는 회원 배열이나 파일에 기록하지 않습니다. 출력과 저장 상태의 차이는 뒤의 배열 예제에서 확인합니다.


센티널 값의 조건

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에 포함되지 않고 뒤의 ignored도 소비하지 않습니다.

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합니다.

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

메뉴 반복 구조

회원가입은 이메일과 나이를 한 쌍으로 받습니다.

메뉴 선택을 먼저 읽고 등록 메뉴일 때만 상세 입력 두 줄을 소비합니다.

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

메뉴 0에서 return하면 switch와 while을 함께 끝냅니다.

메뉴 1의 상세 입력이 부족할 수 있다는 결함은 hasNextLine 검사나 한 줄 명령 형식으로 보완할 수 있습니다.

입력 프로토콜은 소비할 줄 수까지 계약에 포함합니다.


검증과 읽기의 순서

검증 문제를 풀 때 입력 하나를 다음 단계로 보냅니다.

  1. 줄이 존재하는가?
  2. 종료 센티널인가?
  3. 명령 형태와 필요한 항목 수가 맞는가?
  4. 숫자 문자열을 변환할 수 있는가?
  5. 변환 값이 14–120 범위인가?
  6. 모든 조건을 통과한 뒤에만 저장 상태를 바꾼다.

앞 단계가 다음 단계의 안전을 보장합니다.

항목 수를 확인한 뒤에만 배열 원소를 읽습니다. parseInt에서 발생한 형식 오류는 catch로 안내하고, 변환에 성공한 값만 범위 검사로 넘깁니다.

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 검사
  • 실패 경로도 성공 경로도 다음 입력 검사로 복귀합니다.

한 줄 입력 반복

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

실패한 입력은 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
  • 조회와 반복 종료는 등록 상태를 변경하지 않습니다.

현재 count와 total만 남기므로 개별 회원을 다시 출력할 수 없습니다.

다음 문서에서 고정 용량 배열을 만들고 유효한 회원의 위치를 보존합니다.


연습 문제

"x\n13\n121\n60\n"을 읽어 숫자 오류와 범위 오류를 각각 출력한 뒤 60을 받으면 종료하는 MemberAgeRetry를 작성하세요.

해설 보기
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

변환에 성공한 값만 범위 비교에 도달하고 유효한 첫 값에서 break합니다.