안동민 개발노트

본문 시작

입력 검증과 반복

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

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

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

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


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에 필요한 만큼만 좁게 두고, 범위 검사는 변환 성공 뒤에 둡니다.

정상 값을 저장한 뒤 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("kim@example.com\nlee@example.com\nquit\nignored@example.com\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);
    }
}
accepted=kim@example.com
accepted=lee@example.com
count=2

센티널은 저장 전에 검사하므로 count에 포함되지 않고 뒤의 ignored도 소비하지 않습니다.

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

두 이메일은 count를 증가시키지만 quit는 소비한 즉시 break하며 뒤의 ignored@example.com은 읽히지 않는다.

quit는 소비되지만 저장되지 않고 뒤 입력은 남는다SentinelLoop는 두 이메일과 quit를 순서대로 소비하고 count 2에서 즉시 종료하며 ignored@example.com 줄은 Scanner에 남긴다. kim@example.com 소비lee@example.com 소비quit 소비 / break시작 · count = 0다음: kim@example.comcount = 1accepted=kim@example.comcount = 2accepted=lee@example.com종료 · count = 2quit는 count에 포함하지 않음미소비 입력ignored@example.com\n
  1. 시작

    count=0. 다음 줄은 kim@example.com입니다.

  2. 첫 이메일 소비

    accepted=kim@example.com을 출력하고 count=1입니다.

  3. 둘째 이메일 소비

    accepted=lee@example.com을 출력하고 count=2입니다.

  4. quit 소비 후 즉시 종료

    break합니다. quit는 저장하지 않아 count=2이며 ignored@example.com\n은 읽지 않고 남깁니다.

SentinelLoop의 이 실행은 quit에서 중단합니다. running 플래그를 바꾸거나 다음 반복 조건까지 기다리는 방식이 아닙니다.


메뉴 반복 구조

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

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

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

public final class MenuConversation {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(
            "1\nkim@example.com\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);
            }
        }
    }
}
saved=kim@example.com:40
count=1
unknown.menu=9
bye

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

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

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


검증과 읽기의 순서

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

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

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

배열 인덱스를 읽은 뒤 항목 수를 검사하거나 parseInt 뒤에 형식 오류를 안내할 수는 없습니다.

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

quit와 list를 먼저 처리한 뒤 남은 명령은 형태, 정수 변환, 14–120 범위를 모두 통과해야 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를 처리한 뒤 남은 한 줄

  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가 되어도 반복이 끝납니다.


한 줄 입력 반복

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

public final class ValidatedMemberInput {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(
            "register kim@example.com forty\nregister kim@example.com 0\nregister kim@example.com 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
saved=kim@example.com
summary=1:45
ValidatedMemberInput의 다섯 입력 뒤 count와 total 상태

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

ValidatedMemberInput · 초기 count=0, total=0
소비한 입력관찰 출력처리 후 count처리 후 total
register kim@example.com fortynumber.error00
register kim@example.com 0range.error00
register kim@example.com 45saved=kim@example.com145
listsummary=1:45145
quit출력 없음 · 반복 종료145

count=0, total=0에서 시작

  1. register kim@example.com forty

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

  2. register kim@example.com 0

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

  3. register kim@example.com 45

    출력: saved=kim@example.com · 처리 후 count=1, total=45

  4. list

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

  5. quit

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

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

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