Scanner 입력 경계
토큰 입력과 줄 입력의 차이를 재현하고 한 줄을 읽은 뒤 명시적으로 변환하는 회원가입 입력 경계를 만듭니다.
지금까지 회원가입은 실행 인자에서 값을 받았습니다.
대화형 CLI는 프로그램이 실행된 뒤 표준 입력을 여러 번 읽습니다.
Scanner는 공백으로 나눈 토큰과 줄 전체를 모두 읽을 수 있지만 두 방식을 섞으면 아직 소비하지 않은 줄바꿈이 다음 입력이 됩니다.
한 줄을 먼저 읽기
System.in은 사용자가 터미널에 입력하는 표준 입력입니다.
Scanner를 연결하면 Enter를 누를 때까지의 한 줄을 문자열로 읽을 수 있습니다.
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 충돌
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]은 아직 입력에 남습니다.
예외가 아니라 잘못된 정상 출력이라 더 놓치기 쉽습니다.
입력 형식을 줄 단위로 통일하면 소비 규칙이 하나가 됩니다.
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은 Character.isWhitespace 기준으로 자바가 공백으로 판단하는 앞뒤 문자를 제거합니다.
같은 입력에서 토큰과 줄 읽기의 소비 경계가 갈라진다
같은 45 줄과 이메일 입력을 토큰 읽기 또는 줄 읽기로 소비할 때 남는 줄바꿈이 달라집니다. 커서와 반환 문자열을 나란히 추적하면 빈 이메일의 원인이 드러납니다.
45 뒤 줄바꿈을 nextInt가 남기는 실행과 nextLine이 소비하는 실행에서 반환값과 남은 입력을 함께 비교한다.
INPUT · STATE BOUNDARY
같은 입력에서 달라지는 반환값과 남은 줄
nextInt는 숫자 뒤 줄바꿈을 남깁니다. 줄 단위 입력은 줄바꿈까지 소비하고, 읽어 둔 문자열을 별도로 정수로 변환합니다.
45\[email protected]\n
MixedScannerBug
nextInt()
age=45. 남은 입력은\[email protected]\n입니다.nextLine()
email=""을 정상 반환합니다.[email protected]\n은 아직 남아 있습니다.
LineBasedInput
한 줄 읽고 변환
nextLine()이"45"와 줄바꿈을 소비합니다.Integer.parseInt(ageLine.strip())은45를 만듭니다. 남은 입력은[email protected]\n입니다.다음 한 줄 읽기
nextLine().strip()은[email protected]을 반환합니다. 입력은 모두 소비되어hasNextLine()==false입니다.
\n은 줄바꿈을 나타냅니다. nextLine()은 줄바꿈까지 소비하지만 반환 문자열에는 넣지 않습니다. parseInt(ageLine.strip())은 읽어 둔 문자열만 변환합니다.
이 예의 조건
- 두 프로그램은 같은 입력으로 각각 시작한다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 같은 입력에서 토큰과 줄 읽기의 소비 경계가 갈라진다 같은 Scanner 입력을 토큰 혼합과 줄 단위 방식으로 소비한 두 실행 45 뒤 줄바꿈을 nextInt가 남기는 실행과 nextLine이 소비하는 실행에서 반환값과 남은 입력을 함께 비교한다. INPUT · STATE BOUNDARY 같은 입력에서 달라지는 반환값과 남은 줄 nextInt는 숫자 뒤 줄바꿈을 남깁니다. 줄 단위 입력은 줄바꿈까지 소비하고, 읽어 둔 문자열을 별도로 정수로 변환합니다. 45\[email protected]\n MixedScannerBug nextInt() age=45 . 남은 입력은 \[email protected]\n 입니다. nextLine() email="" 을 정상 반환합니다. [email protected]\n 은 아직 남아 있습니다. LineBasedInput 한 줄 읽고 변환 nextLine() 이 "45" 와 줄바꿈을 소비합니다. Integer.parseInt(ageLine.strip()) 은 45 를 만듭니다. 남은 입력은 [email protected]\n 입니다. 다음 한 줄 읽기 nextLine().strip() 은 [email protected] 을 반환합니다. 입력은 모두 소비되어 hasNextLine()==false 입니다. \n 은 줄바꿈을 나타냅니다. nextLine() 은 줄바꿈까지 소비하지만 반환 문자열에는 넣지 않습니다. parseInt(ageLine.strip()) 은 읽어 둔 문자열만 변환합니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 같은 시작 두 reader에 45 줄과 이메일 줄을 제공합니다. |
|
|
| 나이 읽기 nextInt는 줄바꿈을 남기고 nextLine은 줄 전체를 소비합니다. |
|
|
| 이메일 읽기 다음 nextLine이 서로 다른 문자열을 반환합니다. |
|
|
같은 입력에서 토큰과 줄 읽기의 소비 경계가 갈라진다
같은 45 줄과 이메일 입력을 토큰 읽기 또는 줄 읽기로 소비할 때 남는 줄바꿈이 달라집니다. 커서와 반환 문자열을 나란히 추적하면 빈 이메일의 원인이 드러납니다.
45 뒤 줄바꿈을 nextInt가 남기는 실행과 nextLine이 소비하는 실행에서 반환값과 남은 입력을 함께 비교한다.
INPUT · STATE BOUNDARY
같은 입력에서 달라지는 반환값과 남은 줄
nextInt는 숫자 뒤 줄바꿈을 남깁니다. 줄 단위 입력은 줄바꿈까지 소비하고, 읽어 둔 문자열을 별도로 정수로 변환합니다.
45\[email protected]\n
MixedScannerBug
nextInt()
age=45. 남은 입력은\[email protected]\n입니다.nextLine()
email=""을 정상 반환합니다.[email protected]\n은 아직 남아 있습니다.
LineBasedInput
한 줄 읽고 변환
nextLine()이"45"와 줄바꿈을 소비합니다.Integer.parseInt(ageLine.strip())은45를 만듭니다. 남은 입력은[email protected]\n입니다.다음 한 줄 읽기
nextLine().strip()은[email protected]을 반환합니다. 입력은 모두 소비되어hasNextLine()==false입니다.
\n은 줄바꿈을 나타냅니다. nextLine()은 줄바꿈까지 소비하지만 반환 문자열에는 넣지 않습니다. parseInt(ageLine.strip())은 읽어 둔 문자열만 변환합니다.
이 예의 조건
- 두 프로그램은 같은 입력으로 각각 시작한다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 같은 입력에서 토큰과 줄 읽기의 소비 경계가 갈라진다 같은 Scanner 입력을 토큰 혼합과 줄 단위 방식으로 소비한 두 실행 45 뒤 줄바꿈을 nextInt가 남기는 실행과 nextLine이 소비하는 실행에서 반환값과 남은 입력을 함께 비교한다. INPUT · STATE BOUNDARY 같은 입력에서 달라지는 반환값과 남은 줄 nextInt는 숫자 뒤 줄바꿈을 남깁니다. 줄 단위 입력은 줄바꿈까지 소비하고, 읽어 둔 문자열을 별도로 정수로 변환합니다. 45\[email protected]\n MixedScannerBug nextInt() age=45 . 남은 입력은 \[email protected]\n 입니다. nextLine() email="" 을 정상 반환합니다. [email protected]\n 은 아직 남아 있습니다. LineBasedInput 한 줄 읽고 변환 nextLine() 이 "45" 와 줄바꿈을 소비합니다. Integer.parseInt(ageLine.strip()) 은 45 를 만듭니다. 남은 입력은 [email protected]\n 입니다. 다음 한 줄 읽기 nextLine().strip() 은 [email protected] 을 반환합니다. 입력은 모두 소비되어 hasNextLine()==false 입니다. \n 은 줄바꿈을 나타냅니다. nextLine() 은 줄바꿈까지 소비하지만 반환 문자열에는 넣지 않습니다. parseInt(ageLine.strip()) 은 읽어 둔 문자열만 변환합니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 같은 시작 두 reader에 45 줄과 이메일 줄을 제공합니다. |
|
|
| 나이 읽기 nextInt는 줄바꿈을 남기고 nextLine은 줄 전체를 소비합니다. |
|
|
| 이메일 읽기 다음 nextLine이 서로 다른 문자열을 반환합니다. |
|
|
Scanner의 입력 원천과 수명
실제 표준 입력은 new Scanner(System.in)으로 연결합니다.
Scanner를 닫으면 감싼 System.in도 닫히므로 프로그램 전체에서 하나를 만들고 대화가 끝날 때 한 번 닫습니다.
작은 메서드마다 새 Scanner를 만들거나 먼저 만든 Scanner를 닫으면 다음 메뉴가 입력을 읽지 못합니다.
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> [email protected]
age> 35
[email protected]:35System.out.print는 줄을 바꾸지 않아 입력 위치가 프롬프트 뒤에 이어집니다.
자동 예제에서는 문자열 Scanner를 사용하고, 사람이 실행하는 CLI에서는 같은 읽기 코드를 System.in에 연결합니다.
입력 원천만 다르고 줄 소비 계약은 같습니다.
Scanner를 닫으면 감싼 입력도 함께 닫힙니다.
짧은 콘솔 프로그램에서는 하나를 만들어 프로그램이 끝날 때까지 사용합니다.
파일처럼 반드시 닫아야 하는 자원과 try-with-resources는 ch12와 ch23에서 정식으로 배웁니다.
숫자 변환 경계
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이 발생하므로 반복 조건에 사용합니다.
줄 읽기 실패와 숫자 변환 실패는 다른 경계다
줄 존재 검사 뒤 nextLine은 forty를 정상적으로 읽고 별도 parseInt에서 실패합니다. 입력 없음·문자열 읽기·변환 실패를 분리하여 실패 지점을 보여 줄 수 있습니다.
입력이 없는 경우와 forty를 정상적으로 읽은 뒤 숫자 변환에 실패하는 경우를 구분한다.
INPUT · STATE BOUNDARY
줄 읽기와 숫자 변환의 실패 경계
줄이 없으면 읽기를 호출하지 않습니다. forty는 정상적으로 읽힌 문자열이며, 그 다음 정수 변환에서 실패합니다.
- 줄 없음
hasNextLine()==false이면 읽기를 호출하지 않고 끝냅니다.- 줄 있음 · "forty"
nextLine()은"forty"를 정상 반환합니다. 다음parseInt가NumberFormatException으로 실패합니다.- 줄 있음 · "45"
nextLine()으로 문자열을 읽고parseInt로int 45를 얻습니다.
EOF에서 검사 없이 nextLine()을 호출하면 NoSuchElementException입니다. InvalidNumberLine의 forty는 읽힌 문자열이며 실패 지점은 parseInt입니다.
이 예의 조건
- 줄 존재와 정수 변환은 서로 다른 검사입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 줄 읽기 실패와 숫자 변환 실패는 다른 경계다 Scanner 줄 존재 검사와 문자열의 정수 변환에서 갈라지는 실패 경계 입력이 없는 경우와 forty를 정상적으로 읽은 뒤 숫자 변환에 실패하는 경우를 구분한다. INPUT · STATE BOUNDARY 줄 읽기와 숫자 변환의 실패 경계 줄이 없으면 읽기를 호출하지 않습니다. forty는 정상적으로 읽힌 문자열이며, 그다음 정수 변환에서 실패합니다. 줄 없음 hasNextLine()==false 이면 읽기를 호출하지 않고 끝냅니다. 줄 있음 · "forty" nextLine() 은 "forty" 를 정상 반환합니다. 다음 parseInt 가 NumberFormatException 으로 실패합니다. 줄 있음 · "45" nextLine() 으로 문자열을 읽고 parseInt 로 int 45 를 얻습니다. EOF에서 검사 없이 nextLine() 을 호출하면 NoSuchElementException 입니다. InvalidNumberLine 의 forty 는 읽힌 문자열이며 실패 지점은 parseInt 입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| EOF 가드 hasNextLine=false이면 읽기 호출 없이 끝냅니다. |
|
|
| 문자열 읽기 forty와 45 모두 nextLine으로 읽힙니다. |
|
|
| 숫자 변환 parseInt에서 두 값이 갈립니다. |
|
|
줄 읽기 실패와 숫자 변환 실패는 다른 경계다
줄 존재 검사 뒤 nextLine은 forty를 정상적으로 읽고 별도 parseInt에서 실패합니다. 입력 없음·문자열 읽기·변환 실패를 분리하여 실패 지점을 보여 줄 수 있습니다.
입력이 없는 경우와 forty를 정상적으로 읽은 뒤 숫자 변환에 실패하는 경우를 구분한다.
INPUT · STATE BOUNDARY
줄 읽기와 숫자 변환의 실패 경계
줄이 없으면 읽기를 호출하지 않습니다. forty는 정상적으로 읽힌 문자열이며, 그 다음 정수 변환에서 실패합니다.
- 줄 없음
hasNextLine()==false이면 읽기를 호출하지 않고 끝냅니다.- 줄 있음 · "forty"
nextLine()은"forty"를 정상 반환합니다. 다음parseInt가NumberFormatException으로 실패합니다.- 줄 있음 · "45"
nextLine()으로 문자열을 읽고parseInt로int 45를 얻습니다.
EOF에서 검사 없이 nextLine()을 호출하면 NoSuchElementException입니다. InvalidNumberLine의 forty는 읽힌 문자열이며 실패 지점은 parseInt입니다.
이 예의 조건
- 줄 존재와 정수 변환은 서로 다른 검사입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 줄 읽기 실패와 숫자 변환 실패는 다른 경계다 Scanner 줄 존재 검사와 문자열의 정수 변환에서 갈라지는 실패 경계 입력이 없는 경우와 forty를 정상적으로 읽은 뒤 숫자 변환에 실패하는 경우를 구분한다. INPUT · STATE BOUNDARY 줄 읽기와 숫자 변환의 실패 경계 줄이 없으면 읽기를 호출하지 않습니다. forty는 정상적으로 읽힌 문자열이며, 그다음 정수 변환에서 실패합니다. 줄 없음 hasNextLine()==false 이면 읽기를 호출하지 않고 끝냅니다. 줄 있음 · "forty" nextLine() 은 "forty" 를 정상 반환합니다. 다음 parseInt 가 NumberFormatException 으로 실패합니다. 줄 있음 · "45" nextLine() 으로 문자열을 읽고 parseInt 로 int 45 를 얻습니다. EOF에서 검사 없이 nextLine() 을 호출하면 NoSuchElementException 입니다. InvalidNumberLine 의 forty 는 읽힌 문자열이며 실패 지점은 parseInt 입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| EOF 가드 hasNextLine=false이면 읽기 호출 없이 끝냅니다. |
|
|
| 문자열 읽기 forty와 45 모두 nextLine으로 읽힙니다. |
|
|
| 숫자 변환 parseInt에서 두 값이 갈립니다. |
|
|
다중 행 입력 반복
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);
}
}[email protected]
skip.empty
[email protected]
accepted=2입력 존재 여부, 한 줄 소비, 공백 정규화, 빈 값 검사를 순서대로 수행합니다.
continue가 실행돼도 줄은 이미 소비됐으므로 다음 회차가 진행합니다.
대화형 회원가입
한 줄 명령 register email age를 읽고 이메일과 나이를 나눈 뒤 등록 정보를 미리 출력합니다.
공백이 있는 이메일은 아직 지원하지 않는다는 형식 제한을 명시합니다.
여기서 split("\\s+")은 하나 이상 이어진 공백 문자를 구분자로 삼아 한 줄을 여러 문자열로 나눕니다. 반환값 String[]은 문자열 배열이며, 첫 장의 args처럼 인덱스로 값을 읽습니다. register [email protected] 45를 나누면 parts[0]은 register, parts[1]은 이메일, parts[2]는 나이 문자열 45입니다.
parts.length는 나뉜 문자열의 개수입니다. 먼저 개수와 명령을 검사해야 존재하지 않는 인덱스를 읽지 않고 사용법을 안내할 수 있습니다. 배열 자체의 선언과 경계는 배열과 인덱스에서 이어서 배웁니다.
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);
}
}
}[email protected]:45
bye현재는 forty가 들어오면 전체 프로그램이 끝납니다.
다음 문서에서 변환 실패와 범위 실패를 재입력 경로로 돌려보내고, 센티널과 메뉴 반복을 결합합니다.
연습 문제
첫 줄은 이메일, 둘째 줄은 나이로 읽는 TwoLineMember를 작성하세요.
이메일은 strip()으로 앞뒤 공백을 제거한 뒤 출력하고, 나이는 줄 전체를 읽은 뒤 정수로 변환합니다.
문자열 Scanner에 " [email protected] \n50\n"을 넣어 결과를 확인하세요.
해설 보기
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);
}
}[email protected]
age=50토큰 메서드를 섞지 않았으므로 이메일 앞뒤 공백과 두 줄 사이의 줄바꿈이 각각 한 단계에서 처리됩니다.