본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
3장 : 입력·배열·메서드

입력 검증과 반복

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

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

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

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


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도 소비하지 않습니다.


메뉴 반복 구조

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

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

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하면 switchwhile을 함께 끝냅니다.

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

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


검증과 읽기의 순서

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

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

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

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


한 줄 입력 반복

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

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

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


연습 문제

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