안동민 개발노트

본문 시작

Scanner 입력 경계

토큰 입력과 줄 입력의 차이를 재현하고 한 줄을 읽은 뒤 명시적으로 변환하는 회원가입 입력 경계를 만듭니다.

지금까지 회원가입은 실행 인자에서 값을 받았습니다.

대화형 CLI는 프로그램이 실행된 뒤 표준 입력을 여러 번 읽습니다.

Scanner는 공백으로 나눈 토큰과 줄 전체를 모두 읽을 수 있지만 두 방식을 섞으면 아직 소비하지 않은 줄바꿈이 다음 입력이 됩니다.


한 줄을 먼저 읽기

System.in은 사용자가 터미널에 입력하는 표준 입력입니다.

Scanner를 연결하면 Enter를 누를 때까지의 한 줄을 문자열로 읽을 수 있습니다.

src/ReadOneLine.java — 완전한 파일
import java.util.Scanner;

public class ReadOneLine {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.print("email> ");
        String email = scanner.nextLine();
        System.out.println("입력한 이메일=" + email);
    }
}
실행 예
email> [email protected]
입력한 이메일[email protected]

import java.util.Scanner는 표준 라이브러리에 있는 Scanner 타입을 짧은 이름으로 사용하게 합니다.

new Scanner(System.in)은 입력을 읽을 객체를 만들고 nextLine()은 다음 한 줄을 반환합니다.

이 정상 흐름을 확인한 뒤 토큰 방식과 줄 방식이 충돌하는 경우를 살펴봅니다.


nextInt와 nextLine 충돌

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

public final class MixedScannerBug {
    public static void main(String[] args) {
        Scanner scanner = new Scanner("45\n[email protected]\n");

        int age = scanner.nextInt();
        String email = scanner.nextLine();

        System.out.println("age=" + age);
        System.out.println("email=[" + email + "]");
    }
}
실패 관찰
age=45
email=[]

nextInt()는 숫자 토큰 45만 소비하고 그 뒤 줄바꿈은 남깁니다.

이어진 nextLine()은 현재 줄의 남은 부분, 즉 빈 문자열을 반환하고 [email protected]은 아직 입력에 남습니다.

예외가 아니라 잘못된 정상 출력이라 더 놓치기 쉽습니다.

입력 형식을 줄 단위로 통일하면 소비 규칙이 하나가 됩니다.

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

public final class LineBasedInput {
    public static void main(String[] args) {
        Scanner scanner = new Scanner("45\n[email protected]\n");

        String ageLine = scanner.nextLine();
        int age = Integer.parseInt(ageLine.strip());
        String email = scanner.nextLine().strip();

        System.out.println("age=" + age);
        System.out.println("email=[" + email + "]");
    }
}
age=45
email=[[email protected]]

nextLine은 줄바꿈을 제외한 한 줄을 반환하고 줄바꿈까지 소비합니다.

숫자 변환은 읽기와 분리돼 어느 원문 문자열이 실패했는지 기록할 수 있습니다.

strip은 앞뒤 유니코드 공백을 제거합니다.

같은 Scanner 입력을 토큰 혼합과 줄 단위 방식으로 소비한 두 실행

45 뒤 줄바꿈을 nextInt가 남기는 실행과 nextLine이 소비하는 실행에서 반환값과 남은 입력을 함께 비교한다.

같은 입력에서 토큰과 줄 읽기의 소비 경계가 갈라진다nextInt는 45만 소비하여 다음 nextLine이 빈 문자열을 반환하지만, 줄 단위 방식은 숫자 줄 전체를 소비한 뒤 다음 이메일 줄을 읽는다. MixedScannerBugLineBasedInputnextInt()nextLine()parseInt(...)nextLine()nextLine()같은 시작 입력45\[email protected]\n같은 시작 입력45\[email protected]\nage = 45남은 입력\[email protected]\n"45" → age = 45남은 입력[email protected]\nemail = "" · 정상 반환남은 입력[email protected]\nemail = [email protected]남은 입력 없음hasNextLine() == false

45\[email protected]\n

MixedScannerBug

  1. nextInt()

    age=45. 남은 입력은 \[email protected]\n입니다.

  2. nextLine()

    email=""을 정상 반환합니다. [email protected]\n은 아직 남아 있습니다.

LineBasedInput

  1. 한 줄 읽고 변환

    nextLine()이 "45"와 줄바꿈을 소비합니다. Integer.parseInt(ageLine.strip())은 45를 만듭니다. 남은 입력은 [email protected]\n입니다.

  2. 다음 한 줄 읽기

    nextLine().strip()은 [email protected]을 반환합니다. 입력은 모두 소비되어 hasNextLine()==false입니다.

\n은 줄바꿈을 나타냅니다. nextLine()은 줄바꿈까지 소비하지만 반환 문자열에는 넣지 않습니다. parseInt(ageLine.strip())은 읽어 둔 문자열만 변환합니다.


Scanner의 입력 원천과 수명

실제 표준 입력은 new Scanner(System.in)으로 연결합니다.

Scanner를 닫으면 감싼 System.in도 닫히므로 프로그램 전체에서 하나를 만들고 대화가 끝날 때 한 번 닫습니다.

작은 메서드마다 새 Scanner를 만들거나 먼저 만든 Scanner를 닫으면 다음 메뉴가 입력을 읽지 못합니다.

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

public final class PromptOnce {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("email> ");
        String email = scanner.nextLine().strip();
        System.out.print("age> ");
        String rawAge = scanner.nextLine().strip();
        int age = Integer.parseInt(rawAge);

        System.out.println("preview=" + email + ":" + age);
    }
}
입력과 출력

System.out.print는 줄을 바꾸지 않아 입력 위치가 프롬프트 뒤에 이어집니다.

자동 예제에서는 문자열 Scanner를 사용하고, 사람이 실행하는 CLI에서는 같은 읽기 코드를 System.in에 연결합니다.

입력 원천만 다르고 줄 소비 계약은 같습니다.

Scanner를 닫으면 감싼 입력도 함께 닫힙니다.

짧은 콘솔 프로그램에서는 하나를 만들어 프로그램이 끝날 때까지 사용합니다.

파일처럼 반드시 닫아야 하는 자원과 try-with-resources는 ch12와 ch23에서 정식으로 배웁니다.


숫자 변환 경계

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

public final class InvalidNumberLine {
    public static void main(String[] args) {
        Scanner scanner = new Scanner("forty\n");
        String raw = scanner.nextLine();
        int age = Integer.parseInt(raw);
        System.out.println(age);
    }
}
Exception in thread "main" java.lang.NumberFormatException: For input string: "forty"

Scanner 자체는 forty를 정상 문자열로 읽었습니다.

실패는 문자열을 정수 계약으로 바꾸는 parseInt에서 생겼습니다.

읽기 실패와 변환 실패를 구분하면 사용자에게 “입력이 없음”과 “숫자가 아님”을 다르게 안내할 수 있습니다.

hasNextLine()은 다음 줄 존재 여부를 먼저 확인합니다.

파일 끝이나 Ctrl+Z/Ctrl+D로 입력이 끝난 뒤 nextLine()을 호출하면 NoSuchElementException이 발생하므로 반복 조건에 사용합니다.

Scanner 줄 존재 검사와 문자열의 정수 변환에서 갈라지는 실패 경계

입력이 없는 경우와 forty를 정상적으로 읽은 뒤 숫자 변환에 실패하는 경우를 구분한다.

줄 읽기 실패와 숫자 변환 실패는 다른 경계다줄 존재 검사는 EOF의 읽기 호출을 막으며, forty와 45는 모두 문자열로 읽히지만 parseInt에서 실패와 성공으로 갈라진다. falsetrue"forty" / "45""forty""45"hasNextLine() ?줄이 존재하는가줄 없음 · 종료읽기를 호출하지 않음nextLine()문자열 읽기 성공Integer.parseInt(raw)읽기와 별개의 숫자 변환NumberFormatException변환 실패 · int 없음int 값 45변환 성공
줄 없음
hasNextLine()==false이면 읽기를 호출하지 않고 끝냅니다.
줄 있음 · "forty"
nextLine()은 "forty"를 정상 반환합니다. 다음 parseInt가 NumberFormatException으로 실패합니다.
줄 있음 · "45"
nextLine()으로 문자열을 읽고 parseInt로 int 45를 얻습니다.

EOF에서 검사 없이 nextLine()을 호출하면 NoSuchElementException입니다. InvalidNumberLine의 forty는 읽힌 문자열이며 실패 지점은 parseInt입니다.


다중 행 입력 반복

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

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

        while (scanner.hasNextLine()) {
            String email = scanner.nextLine().strip();
            if (email.isEmpty()) {
                System.out.println("skip.empty");
                continue;
            }
            accepted++;
            System.out.println("email=" + email);
        }

        System.out.println("accepted=" + accepted);
    }
}

입력 존재 여부, 한 줄 소비, 공백 정규화, 빈 값 검사를 순서대로 수행합니다.

continue가 실행돼도 줄은 이미 소비됐으므로 다음 회차가 진행합니다.


대화형 회원가입

한 줄 명령 register email age를 읽고 기존 계산을 호출합니다.

공백이 있는 이메일은 아직 지원하지 않는다는 형식 제한을 명시합니다.

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

public final class MemberRegistryInput {
    public static void main(String[] args) {
        Scanner scanner = new Scanner("register [email protected] 45\nquit\n");

        while (scanner.hasNextLine()) {
            String line = scanner.nextLine().strip();
            if (line.equals("quit")) {
                System.out.println("bye");
                break;
            }

            String[] parts = line.split("\\s+");
            if (parts.length != 3 || !parts[0].equals("register")) {
                System.out.println("usage: register <email> <age>");
                continue;
            }

            int age = Integer.parseInt(parts[2]);
            System.out.println("preview=" + parts[1] + ":" + age);
        }
    }
}

현재는 forty가 들어오면 전체 프로그램이 끝납니다.

다음 문서에서 변환 실패와 범위 실패를 재입력 경로로 돌려보내고, 센티널과 메뉴 반복을 결합합니다.


연습 문제

첫 줄은 이메일, 둘째 줄은 나이로 읽는 TwoLineMember를 작성하세요.

이메일은 strip()으로 앞뒤 공백을 제거한 뒤 출력하고, 나이는 줄 전체를 읽은 뒤 정수로 변환합니다.

문자열 Scanner에 " [email protected] \n50\n"을 넣어 결과를 확인하세요.

해설 보기
src/TwoLineMember.java
import java.util.Scanner;

public final class TwoLineMember {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(" [email protected] \n50\n");
        String email = scanner.nextLine().strip();
        int age = Integer.parseInt(scanner.nextLine().strip());

        System.out.println("email=" + email);
        System.out.println("age=" + age);
    }
}

토큰 메서드를 섞지 않았으므로 이메일 앞뒤 공백과 두 줄 사이의 줄바꿈이 각각 한 단계에서 처리됩니다.