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

안동민 개발노트

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

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> kim@example.com
입력한 이메일=kim@example.com

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\nkim@example.com\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()은 현재 줄의 남은 부분, 즉 빈 문자열을 반환하고 kim@example.com은 아직 입력에 남습니다.

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

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

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

public final class LineBasedInput {
    public static void main(String[] args) {
        Scanner scanner = new Scanner("45\nkim@example.com\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=[kim@example.com]

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

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

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);
    }
}
입력과 출력
email> scanner@example.com
age> 35
preview=scanner@example.com:35

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이 발생하므로 반복 조건에 사용합니다.


다중 행 입력 반복

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

public final class ReadUntilEnd {
    public static void main(String[] args) {
        Scanner scanner = new Scanner("first@example.com\n\nkim@example.com\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);
    }
}
email=first@example.com
skip.empty
email=kim@example.com
accepted=2

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

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 scanner@example.com 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);
        }
    }
}
preview=scanner@example.com:45
bye

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

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


연습 문제

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

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

문자열 Scanner에 " lee@example.com \n50\n"을 넣어 결과를 확인하세요.

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

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

        System.out.println("email=" + email);
        System.out.println("age=" + age);
    }
}
email=lee@example.com
age=50

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