
안녕하세요! 자바로 알고리즘 문제를 풀거나 콘솔 프로그램을 만들 때, 한 번쯤 이런 답답한 경험을 해보셨을 겁니다.
"분명히 첫 번째 테스트 케이스는 정답이 잘 나오는데, 왜 두 번째 루프부터 이상한 쓰레기값이 들어오거나 프로그램이 픽픽 쓰러질까?"
"코드를 아무리 뜯어봐도 알고리즘 로직은 완벽한데, 입출력 단계에서 배열이 뒤죽박죽 밀려버리네?"
SWE Expert Academy에서 문제를 풀면서 발생한 일입니다... 함수를 제대로 몰랐던 상태로 문제를 풀었으니... 시간을 많이 쏟게 되었네요.
이 문제는 여러분의 논리적인 제어문이 부족해서가 아닙니다.
자바의 대표적인 입력 클래스인 Scanner가 입력을 토큰(Token) 단위로 처리하는 방식과 nextLine()이 한 줄 단위로 처리하는 방식의 차이 때문에 발생하는 문제입니다.
이번 포스팅에서는 Scanner의 핵심 메서드들을 정리하고, 자바 입문자들을 괴롭히는 nextInt()와 nextLine() 충돌 미스터리를 원리부터 차근차근 파헤쳐 보겠습니다.
1. Scanner 공식문서 [ 링크 바로가기 ]

위 사진처럼 요약 할 수가 있습니다.
공식문서에선,
A Scanner breaks its input into tokens using a delimiter pattern, which by default matches whitespace.
The resulting tokens may then be converted into values of different types using the various next methods.
Scanner는 입력을 '토큰(Token)' 단위로 파싱(처리)하는 클래스입니다. 여기서 토큰이란, 구분자(Delimiter)에 의해 분리된 최소 데이터 덩어리를 말합니다.
- 구분자의 역할: "여기서부터 여기까지가 하나의 데이터야!"라고 경계를 짓는 지점입니다. (기본값은 공백, 탭, 줄바꿈 등)
- Scanner의 파싱 과정:
- 스캔(Scan): 입력된 데이터를 한 글자씩 훑습니다.
- 토큰 수집: 구분자를 만나기 전까지의 문자를 하나의 조각(토큰)으로 모읍니다.
- "Delimiter Matched!!": 구분자를 마주하면, 지금까지 모은 데이터를 하나의 완성된 토큰으로 확정합니다.
- 반복: 위 과정을 EOF(입력 종료)가 나올 때까지 반복합니다.
핵심은...
Scanner는 "10 20 hello~"라는 사용자의 입력을 받았을 때, 숫자를 미리 알고 있는 게 아니고
오직 '구분자(e.g. 공백)'이라는 신호를 만나서 "아, 여기서 토큰이 하나 끝났구나!"라고 판단하며 데이터를 조립할 뿐입니다.
1 → 토큰에 추가
0 → 토큰에 추가
□ → Delimiter matched!
2 → 새로운 토큰 시작
0 → 토큰에 추가
EOF → 마지막 토큰 종료
2. 왜 Scanner 입력 받을 때 에러가 날까?
2
3 3
1 2 3
1 2 3
1 2 3
...
보통 테스트 케이스 개수T를 받고, 그 아래에 배열의 길이(N, M)와 배열 데이터가 주어지는 상황을 가정해 봅시다.
Scanner sc = new Scanner(System.in);
int T = sc.nextInt(); // 테스트 케이스 개수 T값 입력
for(int i = 0; i < T; i++) {
// 요기요기!!!! 문제 발생 구간: N과 M을 입력받지 않고 바로 다음 줄을 읽으려 함
int[] arr = Arrays.stream(sc.nextLine().split(" ")).mapToInt(Integer::parseInt).toArray();
// ... 로직 수행 ...
}
여기서 주의사항은 int T = sc.nextInt()는 공백전까지 읽고, T값을 읽고나면은 엔터키(\n)는 그대로 버퍼에 남습니다...
| 단계 | 동작 | 버퍼 내용 ( ' [ <- 포인터 위치 ' ) | 설명 |
| 시작 | 초기 상태 | [2 \n 3 3 \n] | 포인터가 버퍼의 맨 앞을 가리킵니다. |
| Step 1 | nextInt() | 2 [ \n 3 3 \n] | 2만 읽고 포인터는 숫자 뒤에 멈춥니다. |
| Step 2 | 남은 버퍼 | 2 [ \n 3 3 \n] | 포인터 앞에 \n이 대기 중입니다. |
| Step 3 | nextLine() | 2 \n [ 3 3 \n] | nextLine()은 \n을 만나 즉시 종료! |
이런 느낌입니다.
nextInt() 호출후 Arrays.stream(sc.nextLine()... 이부분을 수행할때
2 이후에, 3 3을 하나의 문자열로 읽어오지 않고 sc.nextLine()은 \n을 읽기 때문에 함수는 즉시 종료됩니다.
그래서 해결 방법은 , nextInt()뒤에, 남아있는 \n 엔터키를 제거하기 위해 sc.nextLine()을 하나 추가해야 합니다.
Scanner sc = new Scanner(System.in);
int T = sc.nextInt(); // 테스트 케이스 개수 T값 입력
sc.nextLine(); /// 요기에 추가!!
for(int i = 0; i < T; i++) {
// 위에 nextLine()썼기 때문에 버퍼에 있었던 "2 \n" 이 한 문장이 사라져있는 상태. 6번 라인 정상 동작
int[] arr = Arrays.stream(sc.nextLine().split(" ")).mapToInt(Integer::parseInt).toArray();
// ... 로직 수행 ...
}
참고로,
사실 더 정확히 말하자면, nextInt()는 토큰만 소비합니다...
Scanner는 delimiter(구분자: 엔터, 공백...)를 그대로 남겨둡니다.
그래서
"10 20" 이 문자열을 사용자가 입력한다고 했을때
// 10 읽음.
// 정확히 공백 전까지의 문자를 토큰으로 모은 뒤
// nextInt()라는 함수가 모았던 토큰을 int로 변환.
// 이때 10뒤에 공백은 건들이지 않음.
int prefix = sc.nextInt();
// 버퍼에서, 공백+20\n 까지 읽어들임
// 입력 버퍼에 남아있던 공백부터~ 숫자와 줄바꿈까지 모두 읽어들인다.
// 그래서 반환값은 " 20"
String suffix = sc.nextLine();
그러나 위 입력에서, nextInt()를 두 번 호출한다면 nextInt()는 공백, \n(엔터키)를 구분자로 인식해서 건너뛰고 그다음 숫자 토큰을 찾아서 읽게 됩니다. 그래서 숫자 두 개를 문제없이 받을 수 있습니다.
3. Scanner 내장 함수들
Scanner의 next 계열 메서드들은 사용자가 무엇을 기대하고 데이터를 가져오느냐에 따라 나뉩니다. 크게 토큰을 단위로 읽는 메서드와 줄 단위로 읽는 메서드로 구분할 수 있습니다.
| 메서드 | 반환 타입 | 설명 (토큰/구분자 처리 방식) |
| next() | String | 공백(Space, Tab, Enter)을 기준으로 다음 토큰 하나를 읽음 |
| nextLine() | String | 엔터(\n)를 만날 때까지 한 줄 전체를 읽음 |
| nextInt() | int | 다음 토큰을 정수로 변환하여 읽음 |
| nextLong() | long | 다음 토큰을 8바이트 정수로 변환하여 읽음 |
| nextDouble() | double | 다음 토큰을 실수로 변환하여 읽음 |
| nextBoolean() | boolean | 다음 토큰을 참/거짓으로 변환하여 읽음 |
구분자 \n 을 대하는 태도에 따라 두분류로 나눠봤습니다.
| 분류 | 대상 메서드 | 행동 패턴 | 엔터(\n) 처리 방식 |
| 알맹이 사냥꾼 | nextInt(), next(), nextDouble() | 공백이나 엔터를 만나면 건너뜀 | 남겨둠 (버퍼에서 아직 읽지 않은 데이터로 있음) |
| 한 줄 청소기 | nextLine() | 엔터를 만날 때까지 싹 다 긁어모음 | 소비함 (버퍼에서 완전히 삭제) |
그래서 nextLine()이 아닌 경우엔 '\n'을 남겨두기 때문에,, 알고리즘 풀때 정확하게 nextInt()나 next()로만 받아오는게 아닌 경우라면 공백 신경 써야합니다~
만약에! 위에 T, NM과 배열들의 입력값을 nextInt() 로만 받는다면
Scanner sc = new Scanner(System.in);
int T = sc.nextInt(); // 테스트 케이스 개수
for (int test_case = 1; test_case <= T; test_case++) {
int N = sc.nextInt(); // 배열 길이 N
int M = sc.nextInt(); // 배열 길이 M
// 첫 번째 배열 읽기
int[] lhs = new int[N];
for (int i = 0; i < N; i++) {
lhs[i] = sc.nextInt();
}
// 두 번째 배열 읽기
int[] rhs = new int[M];
for (int i = 0; i < M; i++) {
rhs[i] = sc.nextInt();
}
}
Scanner의 nextInt()는 호출될 때마다 알아서 공백과 줄바꿈(엔터)을 건너뛰고 숫자 토큰만 찾아가기 때문에, 사실 버퍼 찌꺼기가 남든 말든 다음 nextInt()가 알아서 다 처리해 줍니다.
입력받는 규격을 정확하게 알고 있을 때 사용하면 될것 같습니다.
숫자 위주의 입력이라면 nextInt()만 사용하는 것이 편리합니다.
반대로 한 줄 전체를 문자열로 처리해야 하는 경우에는 nextLine()을 사용하는 것이 적합합니다.
3. 여기서부터는 TMI이긴한데, BufferedReader와 StringTokenizer의 조합은 빠르다고 합니다..
그냥 검색하다보니까 BufferedReader와의 차이가 데이터가 많아질수록 입출력 차이가 좀 난다고 하더라구요?
Scanner 내부 로직은 Regex 정규식을 사용해서 토큰 구분, 파싱을 하기 때문에, 데이터 많아질떄 10만개, 100만개 이상 주어질 때는 Scanner 만으로도 백준에서 시간초과가 나는 경우가 있다고 하더랍니다.,.
그리고 또 만약에 nextInt()를 쓴 경우 숫자만 들어야하는데, 사용자가 12.3 이렇게 입력하면 InputMismatchException 이런 에러를 떤진답니다..
그리고 EOF처리.. 간혹 문제에서 테스트케이스 T를 알려주지 않을 경우에
- while(sc.hasNext())
- while(sc.hasNextInt())
를 사용하면 됩니다.
Refs:
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Scanner.html