Java 09

Scanner의 다양한 얼굴: next(), nextLine(), close() 완전 정복

nextInt, nextDouble, next, nextLine이 모두 입력을 읽는 것처럼 보여서 언제 어떤 메서드를 써야 하는지 막힌다.

근거 · 교안 p65-p72

Scanner의 다양한 얼굴: next(), nextLine(), close() 완전 정복 대표 이미지

1장: 숫자를 읽는 Scanner, 문자를 읽는 Scanner?

솔라의 손가락이 노트북 트랙패드 위에서 허공을 맴돌았다. 화면에는 고작 다섯 줄짜리 간단한 자바 코드가 떠 있었다. 사용자에게서 입력을 받아 출력하는, 지극히 평범한 코드. 하지만 솔라의 미간은 펴질 줄 몰랐다. 코드 자체는 문제가 아니었다. 솔라의 머릿속을 어지럽히는 것은 따로 있었다.

Scanner는 자료형에 맞는 메서드로 사용자 입력을 읽는다.

방금 읽었던 책의 한 문장이었다. nextInt()는 정수, nextDouble()은 실수. 거기까지는 명확했다. 하지만 next()nextLine()에 이르면 설명이 모호해졌다. 둘 다 문자열을 읽는 것 같은데, 어떤 ‘자료형’에 맞는다는 걸까? 모두 키보드로 입력하는 걸 읽어 들이는 건 똑같은데, 왜 이렇게 복잡하게 나뉘어 있는 걸까?

“언니, 이거 좀 이상해.”

솔라는 생각의 매듭을 풀지 못한 채 거실 소파에 기대앉아 있던 루나를 불렀다. 루나는 읽던 책에서 눈을 떼고 솔라의 노트북 화면으로 시선을 옮겼다.

nextInt(), nextDouble(), next(), nextLine()… 다 그냥 입력받는 함수잖아. 그런데 왜 이렇게 이름이 여러 개인 거야? 그냥 readInput() 같은 거 하나만 있으면 안 되나?”

솔라의 질문에는 ‘나는 이미 이게 같은 기능을 한다고 결론 내렸는데, 왜 굳이 다르게 부르는지 모르겠다’는 짜증 섞인 호기심이 묻어났다. 특히 ‘자료형에 맞는’이라는 규칙이 next()nextLine() 사이에서는 어떻게 적용되는지 도무지 감이 잡히지 않았다.

루나는 솔라의 화면을 잠시 들여다보더니, 말없이 자기 노트북을 열었다. 그리고는 아주 짧은 코드를 타이핑했다.

import java.util.Scanner;

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

        System.out.print("나이를 입력하세요: ");
        int age = scanner.nextInt();

        System.out.println("당신의 나이는 " + age + "세입니다.");
    }
}

“솔라, 이 코드 한번 실행해 볼래?”

솔라는 코드를 쓱 훑어보았다. 나이를 입력받아 출력하는 간단한 프로그램이었다. 솔라는 코드를 실행하고는 콘솔에 숫자 17을 입력하고 엔터 키를 눌렀다.

당신의 나이는 17세입니다.

예상대로였다. “응, 잘 되는데?” 솔라가 시큰둥하게 대답했다. 뭐가 문제인지 모르겠다는 표정이었다.

“좋아. 그럼 한 번 더 실행해 봐. 이번에는 나이 대신 네 이름을 입력해 보는 거야.”

“내 이름을?”

솔라는 고개를 갸웃했지만, 루나가 시키는 대로 프로그램을 다시 실행했다. ‘나이를 입력하세요:’라는 메시지 옆에, 솔라는 키보드로 ‘솔라’라고 타이핑하고 엔터를 쳤다.

결과는 아까와 완전히 달랐다.

Exception in thread "main" java.util.InputMismatchException

화면 가득 낯선 빨간색 에러 메시지가 출력되며 프로그램이 비정상적으로 종료되었다. 솔라는 순간 당황했다. 그냥 글자를 입력했을 뿐인데, 프로그램이 터져버렸다.

“어? 왜… 왜 이러지? 그냥 입력한 것뿐인데.”

“방금 nextInt()가 뭘 하려고 했을까?”

루나의 차분한 질문에 솔라는 다시 에러 메시지와 코드를 번갈아 쳐다보았다. scanner.nextInt(). 이 코드가 실행되는 순간, ‘솔라’라는 글자가 들어가면서 모든 게 엉망이 되었다.

“음… nextInt()는… 정수(integer)를 기다렸던 건가?”

“맞아. 그냥 키보드 입력을 무작정 읽는 게 아니야. nextInt()는 들어온 입력값에서 ‘정수로 해석할 수 있는 부분’을 찾으려고 해. 그런데 ‘솔라’라는 글자는 숫자로 바꿀 수 없잖아. 그래서 자기가 뭘 해야 할지 모르겠다며 소리를 지른 거야. ‘입력 형태가 내가 기대한 거랑 맞지 않아!(Input Mismatch)’ 하고.”

그제야 솔라의 눈이 동그래졌다. 아, 그냥 ‘읽는’ 게 아니었구나. 읽어서 ‘자료형에 맞게 바꾸려고 시도’하는 거였구나. 실패하면 이렇게 프로그램이 멈춰버리는 것이고.

솔라는 다시 자신의 처음 질문으로 돌아갔다. Scanner의 메서드들은 모두 똑같이 입력을 읽는 줄 알았다. 하지만 아니었다. 각자 맡은 임무가 있었다. nextInt()는 정수 변환, nextDouble()는 실수 변환이라는 명확한 임무.

“그럼… ‘자료형에 맞는 메서드’라는 말이… 단순히 값을 담을 변수의 자료형뿐만 아니라, 입력될 값의 ‘생김새’에 맞는 메서드를 고르라는 뜻이었구나.”

솔라가 혼잣말처럼 중얼거렸다. 이제 nextInt가 왜 nextString 같은 것과 구분되는지 몸으로 이해할 수 있었다. 하나는 숫자 형태의 입력을 기대하고, 다른 하나는 그렇지 않을 테니까.

판단의 기준이 바뀌자, 머릿속이 한결 개운해졌다. Scanner의 메서드들은 똑같은 일을 하는 여러 버전이 아니었다. 저마다 다른 종류의 재료를 손질하는 각기 다른 모양의 칼과 같았다. 정수를 다루는 칼, 실수를 다루는 칼, 그리고…

문득 새로운 궁금증이 고개를 들었다.

“알겠어. 그럼 nextInt()nextDouble()은 숫자 모양을 찾는다는 건 확실히 이해했어. 그런데 언니, next()nextLine()은 둘 다 문자열을 다루잖아. 이 둘은 어떤 모양의 차이를 보고 고르는 거야?“

2장: 한 단어? 한 줄? next()와 nextLine()의 결정적 차이

루나는 솔라의 질문에 대답하는 대신, 자신의 노트북 화면에 새 파일을 열고 코드를 작성하기 시작했다. 잠시 후, 화면에는 거의 똑같이 생긴 두 개의 코드 블록이 나란히 나타났다.

왼쪽 코드 블록의 제목은 WordReader, 오른쪽은 LineReader였다. 두 코드 모두 사용자에게 무언가를 입력하라는 메시지를 띄우고, Scanner를 이용해 그 값을 읽어 다시 출력해 주는 간단한 구조였다. 하지만 결정적인 한 줄이 달랐다. WordReaderscanner.next()를, LineReaderscanner.nextLine()을 사용하고 있었다.

루나는 아무 말 없이 노트북을 솔라 쪽으로 돌려주었다. 솔라는 두 코드를 번갈아 훑어보았다. 방금 전까지 머릿속에서 맴돌던 막연한 질문이 눈앞에 놓인 구체적인 선택지가 되어 있었다. 정수를 다루는 칼과 실수를 다루는 칼은 이제 알겠다. 하지만 이 둘은? 둘 다 똑같이 생긴 문자열용 칼처럼 보이는데, 왜 날의 모양이 다른 걸까?

“백문이 불여일견이지.”

루나가 WordReader 코드를 가리켰다.

“왼쪽 코드부터 실행해 볼래?”

import java.util.Scanner;

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

        System.out.print("좋아하는 단어를 입력하세요: ");
        String word = scanner.next(); // next() 사용

        System.out.println("당신이 입력한 단어: " + word);
    }
}

솔라는 코드를 실행했다. 좋아하는 단어를 입력하세요: 라는 문구가 콘솔에 나타났다. 솔라는 잠시 고민하다가, 키보드로 이렇게 입력했다.

Java is fun

그리고 엔터 키를 눌렀다. 솔라는 당연히 “당신이 입력한 단어: Java is fun” 이라고 출력될 것을 기대했다. 하지만 화면에 나타난 결과는 예상과 달랐다.

당신이 입력한 단어: Java

“어? is fun은 어디 갔어? 분명히 다 쳤는데…”

솔라의 목소리에 당혹감이 묻어났다. nextInt() 때처럼 에러가 나진 않았지만, 입력의 일부가 증발해버린 듯한 이상한 현상이었다. 프로그램은 아무 문제 없다는 듯이 종료되었다. 사라진 is fun은 어디로 간 걸까?

“사라진 게 아니야. 아직 ‘입력 스트림’이라는 대기 공간에 남아있어.”

루나는 WordReader 코드를 살짝 수정했다. scanner.next() 호출을 세 번 연달아 하도록 바꾼 것이다.

// ...생략...
System.out.print("좋아하는 단어들을 입력하세요: ");
String word1 = scanner.next();
String word2 = scanner.next();
String word3 = scanner.next();

System.out.println("첫 번째 단어: " + word1);
System.out.println("두 번째 단어: " + word2);
System.out.println("세 번째 단어: " + word3);
// ...생략...

“자, 이걸로 다시 실행해서 똑같이 입력해 봐.”

솔라는 의아해하며 수정된 코드를 실행하고, 아까와 똑같이 Java is fun을 입력한 뒤 엔터를 쳤다. 그러자 이번에는 놀라운 결과가 나타났다.

첫 번째 단어: Java
두 번째 단어: is
세 번째 단어: fun

“아!”

솔라의 입에서 짧은 탄성이 터져 나왔다. 그제야 next() 메서드의 진짜 작동 방식이 보이기 시작했다. next()는 문자열 전체를 한 번에 가져오는 게 아니었다. 마치 뷔페에서 접시에 음식을 조금씩 덜어 담듯, 입력된 내용에서 ‘한 덩어리’씩만 가져오고 있었다. 그리고 그 덩어리를 나누는 기준은 바로 ‘공백(space)’이었다.

next()는 띄어쓰기를 만나면 일단 멈추는 거구나! 그래서 Java 까지만 읽고 멈췄던 거고, 내가 next()를 또 부르니까 대기하고 있던 is를 가져온 거네.”

“바로 그거야. next()가 찾는 값의 ‘모양’은 ‘공백으로 구분된 하나의 단어’인 셈이지.”

이제 그림이 명확해졌다. 그렇다면 nextLine()은? 솔라는 스스로 답을 찾을 수 있을 것 같았다. 그녀는 루나가 처음 보여주었던 LineReader 코드를 실행했다.

import java.util.Scanner;

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

        System.out.print("좋아하는 문장을 입력하세요: ");
        String line = scanner.nextLine(); // nextLine() 사용

        System.out.println("당신이 입력한 문장: " + line);
    }
}

솔라는 다시 한번 Java is fun을 입력하고 엔터를 눌렀다.

당신이 입력한 문장: Java is fun

이번에는 입력한 내용이 하나도 빠짐없이 그대로 출력되었다. 공백이 있었지만, nextLine()은 전혀 신경 쓰지 않았다.

“와… nextLine()은 띄어쓰기가 있어도 상관없네. 그냥 내가 엔터 키를 누를 때까지 입력된 모든 걸 한 줄로 보는구나.”

솔라는 두 메서드의 차이를 완벽히 이해했다. next()는 단어를 줍는 집게, nextLine()은 한 줄을 쓸어 담는 쓰레받기 같았다. 둘 다 문자열을 다루지만, 목표로 하는 ‘덩어리’의 크기가 근본적으로 달랐다. ‘자료형에 맞는’이라는 말이 이제 문자열 세계에서도 어떻게 적용되는지 알 것 같았다. 단어가 필요하면 next(), 문장이 필요하면 nextLine().

“응. 이제 둘을 언제 써야 할지 확실히 알겠어. 그럼… 만약에 나이를 nextInt()로 먼저 입력받고, 바로 다음에 자기소개를 nextLine()으로 입력받으면… 그냥 순서대로 잘 작동하겠지?”

솔라는 자신만만하게 물었다. 하나는 숫자용 칼, 하나는 문장용 쓰레받기. 각자 할 일이 명확하니, 섞어 쓴다고 문제 될 게 없어 보였다. 루나는 대답 대신 의미심장한 미소만 지어 보였다. 그 미소의 의미를, 솔라는 곧 알게 될 터였다.

3장: Scanner가 입력을 건너뛰었다고요? 줄바꿈 문자의 역습

자신감이 붙은 솔라의 손가락이 경쾌하게 키보드를 두드렸다. 숫자용 칼(nextInt), 단어용 집게(next), 문장용 쓰레받기(nextLine). 머릿속에 어지럽게 흩어져 있던 Scanner의 도구들이 제자리를 찾은 느낌이었다. 각자의 역할이 명확하니, 이제 섞어 쓰는 것도 문제없을 터였다.

이 새로운 깨달음을 증명해 보이고 싶었다. 솔라는 간단한 프로필 프로그램을 만들기로 마음먹었다. 나이를 정수로 입력받고, 이어서 한 줄짜리 자기소개를 문장으로 받는 코드. nextInt()nextLine()을 연달아 사용하는, 완벽한 시험 무대였다.

import java.util.Scanner;

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

        System.out.print("나이를 입력하세요: ");
        int age = scanner.nextInt();
        System.out.println("당신의 나이는 " + age + "세입니다.");

        System.out.print("자기소개를 한 줄로 입력하세요: ");
        String intro = scanner.nextLine();
        System.out.println("자기소개: " + intro);
    }
}

“언니, 이것 봐. 나이를 nextInt()로 받고, 바로 자기소개를 nextLine()으로 받는 코드. 이제 각자 역할이 뭔지 아니까 당연히 잘 되겠지?”

솔라는 의기양양하게 코드를 실행했다. 루나는 이전과 같은 의미심장한 미소를 띤 채 조용히 화면을 지켜볼 뿐이었다.

콘솔 창에 첫 번째 안내 문구가 나타났다.

나이를 입력하세요:

솔라는 숫자 20을 입력하고 망설임 없이 엔터 키를 눌렀다. 예상대로 다음 줄이 출력되었다.

당신의 나이는 20세입니다.

그리고 곧바로 다음 안내 문구가 나타났다. 그런데 뭔가 이상했다.

자기소개를 한 줄로 입력하세요: 자기소개:

자기소개를 입력하라는 문구와 결과가 출력되는 문장이 한꺼번에 나타나더니, 프로그램이 그대로 끝나버렸다. 솔라가 키보드에 손을 댈 틈도 없이 모든 것이 순식간에 지나갔다.

“어? 잠깐만. 왜… 왜 입력을 안 기다려주지?”

솔라는 당황해서 다시 프로그램을 실행했다. 이번에는 더 빠르게, 20을 누르고 엔터를 치자마자 자기소개를 입력할 준비를 했다. 하지만 결과는 같았다. 자기소개를 입력할 기회는 오지 않았다.

“내가 뭘 잘못 입력한 건가? nextInt() 다음에 nextLine()을 쓰면 안 되는 거였어?”

자신감 넘치던 솔라의 목소리에 금이 가기 시작했다. 분명 각자 역할이 다른 도구라고 생각했는데, 함께 쓰니 모든 게 엉망이 되어버렸다. nextLine()이 자기 역할을 건너뛴 것처럼 보였다.

“건너뛴 게 아니야, 솔라. nextLine()은 자기 일을 충실히 한 거야. 다만 네가 예상치 못한 손님을 먼저 만났을 뿐이지.”

루나는 종이 한 장을 가져와 가운데에 길쭉한 상자를 그렸다. ‘입력 대기실’이라고 이름 붙였다.

“네가 키보드로 20을 입력하고 엔터 키를 누른 순간을 생각해 봐. 이 대기실에 뭐가 들어갔을까?”

“숫자 20이 들어갔겠지.” 솔라가 대답했다.

“정확히는 ‘2’, ‘0’, 그리고 네가 누른 ‘엔터’ 키 신호가 순서대로 들어갔어.”

루나는 상자 안에 2, 0, 그리고 ↵ 모양의 화살표를 그려 넣었다.

[ 2 | 0 | ↵ ]

“자, 이 상태에서 scanner.nextInt()가 호출됐어. nextInt는 숫자만 찾는 녀석이지. 대기실을 쓱 훑어보고는 자기가 알아볼 수 있는 ‘2’와 ‘0’을 데리고 나가서 숫자 20으로 만들었어. 그리고 자기 일은 끝났다고 생각했지.”

루나는 상자에서 20을 지웠다. 대기실에는 이제 덩그러니 엔터 키 신호만 남아있었다.

[ ↵ ]

“그다음에 scanner.nextLine()이 호출됐네. nextLine의 임무가 뭐였지?”

nextLine은… 엔터 키를 만날 때까지 한 줄을 전부 읽는 거.”

솔라는 루나가 그린 그림과 자신의 코드를 번갈아 보았다. 그 순간, 모든 조각이 맞춰지는 느낌이 들었다.

“아! 설마…!”

솔라의 눈이 커졌다.

“대기실에… 내가 나이를 입력하고 남은 엔터 키가 이미 있었던 거야! nextLine()이 일을 시작하려고 대기실에 들어갔는데, 들어가자마자 엔터 키를 발견한 거지. 그래서 ‘어? 벌써 한 줄 끝났네?’ 하고 아무것도 안 읽고 그냥 바로 끝나버린 거구나!”

바로 그거였다. nextLine()이 입력을 건너뛴 게 아니었다. nextInt()가 처리하고 남긴 ‘엔터’라는 보이지 않는 입력값을 nextLine()이 곧바로 읽어버린 것이었다. 프로그램은 잘못이 없었다. 잘못은 nextInt()nextLine()의 작동 방식을 반만 이해했던 솔라 자신에게 있었다.

nextInt(), next(), nextDouble() 같은 메서드들은 모두 자기에게 필요한 값(숫자, 단어)만 쏙 빼가고, 줄바꿈을 알리는 엔터 키는 그대로 대기실에 남겨둔다. 그 뒤에 등장하는 nextLine()은 그 남겨진 엔터 키를 보고 입력이 끝났다고 착각해버리는 것이다.

“이제 알겠어. 범인은 남겨진 줄바꿈 문자였어.”

솔라가 허탈하게 웃으며 말했다. 함정의 정체를 파악하자 안도감이 들었지만, 동시에 새로운 의문이 떠올랐다.

“원인은 알았는데… 그럼 이 남겨진 엔터 키는 어떻게 처리해야 해? nextLine()이 속지 않게 하려면 어떻게 해야 하는 거야?“

4장: 버퍼 청소 대작전: 줄바꿈 문제 해결과 자원 정리의 시작

솔라는 노트북 화면의 코드와 루나가 그려준 ‘입력 대기실’ 그림을 번갈아 쳐다보았다. 화면 속 커서는 자기소개를 입력받지 못하고 끝나버린 프로그램의 흔적 위에서 무심하게 깜빡이고 있었다. 범인의 정체는 밝혀졌다. nextInt()가 남기고 간 줄바교체 신호, .

문제는 ‘어떻게’였다. 이 보이지 않는 침입자를 어떻게 처리해야 할까? nextLine()이 억울하게 빈손으로 돌아가지 않게 하려면, 진짜 자기소개를 입력받기 전에 누군가 저 를 치워줘야만 했다.

솔라는 nextLine()의 작동 방식을 다시 떠올렸다. ‘엔터 키를 만날 때까지 한 줄을 전부 읽는다.’ 그렇다면… 지금 입력 대기실에 덩그러니 남아있는 를 읽게 하려면 어떻게 해야 할까? 바로 nextLine()을 쓰는 거다! 솔라의 머릿속에서 하나의 가설이 번개처럼 스쳤다.

“언니, 혹시… nextInt()로 나이를 읽고 나서, 진짜 자기소개를 받기 전에, nextLine()을 그냥 한 번 더 불러주면 어떨까?”

“그냥 한 번 더?”

“응. 대기실에 남아있는 엔터 키()를 먹어치울 미끼로 쓰는 거야. 목적 없이 nextLine()을 한 번 실행해서 대기실을 비우는 거지. 그러면 그다음에 오는 진짜 nextLine()은 깨끗한 상태에서 내 자기소개를 기다려줄 수 있잖아.”

솔라의 목소리에는 불확실함과 기대감이 섞여 있었다. 논리적으로는 완벽한 해결책 같았다. nextLine()의 함정을 nextLine()으로 해결한다는 역발상이었다.

루나는 말없이 고개를 끄덕여 보였다. 직접 확인해 보라는 무언의 지지였다. 솔라는 떨리는 손으로 이전 코드를 수정하기 시작했다. nextInt()로 나이를 읽는 코드 바로 다음 줄에, 아무 변수에도 담지 않는 scanner.nextLine(); 한 줄을 조심스럽게 추가했다.

import java.util.Scanner;

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

        System.out.print("나이를 입력하세요: ");
        int age = scanner.nextInt();
        scanner.nextLine(); // 버퍼에 남은 줄바꿈 문자를 제거하기 위한 호출

        System.out.println("당신의 나이는 " + age + "세입니다.");

        System.out.print("자기소개를 한 줄로 입력하세요: ");
        String intro = scanner.nextLine();
        System.out.println("자기소개: " + intro);
    }
}

주석까지 꼼꼼하게 단 솔라는 심호흡을 한 번 하고 프로그램을 실행했다.

나이를 입력하세요:

솔라는 다시 숫자 20을 입력하고 엔터 키를 눌렀다. 이전과 똑같은 과정. 심장이 두근거렸다.

당신의 나이는 20세입니다.

그리고…

자기소개를 한 줄로 입력하세요:

커서가 깜빡이며 다음 입력을 기다리고 있었다! 프로그램은 멋대로 끝나지 않았다. 솔라의 작전이 성공한 것이다. 솔라는 환호성을 지를 뻔한 것을 간신히 참고, 키보드에 자기소개를 입력했다.

저는 자바를 배우는 솔라입니다.

엔터 키를 누르자, 완벽한 결과가 출력되었다.

자기소개: 저는 자바를 배우는 솔라입니다.

“됐다! 됐어! 미끼 작전이 통했어!”

솔라가 주먹을 불끈 쥐었다. nextInt()nextLine() 사이의 보이지 않는 벽을 허물 방법을 마침내 찾아낸 것이다. 의미 없어 보이는 nextLine() 한 줄이, 사실은 입력 대기실을 청소하는 아주 중요한 역할을 하고 있었다.

“이제 next()nextDouble() 다음에 nextLine()을 쓸 때도 똑같이 해주면 되겠네. 숫자나 단어만 쏙 빼먹고 남겨진 엔터 키를 청소해 주는 거지.”

스스로 발견한 해결책에 뿌듯해하며 노트북을 닫으려던 순간이었다. 루나가 솔라의 코드 첫 줄을 손가락으로 가리켰다.

Scanner scanner = new Scanner(System.in);

“Scanner를 만들어서 시스템의 입력 통로를 열었네. 그럼 다 쓰고 나면 어떻게 해야 할까?”

“어떻게 하냐니? 프로그램이 끝나면 알아서 다 정리되는 거 아니었어?”

솔라는 당연하다는 듯이 대답했다. 방을 빌려 쓰고 나면 문을 닫고 나오는 것처럼, Scanner라는 도구를 썼으면 뒷정리를 해야 한다는 루나의 암시에 솔라는 고개를 갸웃했다. 지금까지 close() 같은 걸 쓰지 않아도 아무 문제 없이 잘만 돌아갔기 때문이다.

“Scanner는 그냥 프로그램 안에서 쓰는 변수 같은 게 아니야. 운영체제의 자원을 빌려 쓰는 것에 가까워. 도서관에서 책을 빌렸으면 다 읽고 반납해야 다른 사람도 볼 수 있잖아. 비슷해.”

“반납? scanner.close() 같은 거 말하는 거야? 책에서 본 것 같긴 한데… 그거 안 하면 큰일 나?”

솔라의 질문에는 순수한 의문이 담겨 있었다. 당장 눈에 보이는 오류도 없고, 프로그램도 잘 돌아가는데 굳이 close()라는 걸 왜 해야 하는지 이해가 가지 않았다. 줄바꿈 문제처럼 명확한 함정도 아닌데 말이다.

솔라의 머릿속에 새로운 질문이 떠올랐다. 입력 대기실 청소는 끝났지만, ‘Scanner’라는 도구 자체의 뒷정리는 대체 무엇을 의미하는 걸까? 그리고 그 ‘반납’은 왜 선택이 아닌 필수일까?

5장: Scanner와 함께 떠나는 깔끔한 마무리: 자원 관리의 중요성

솔라는 마침내 완성한 ProfileMakerFixed 코드를 만족스럽게 바라보았다. nextInt()nextLine()을 함께 쓸 때 생기던 입력 건너뜀 현상을 ‘미끼용 nextLine()’이라는 기발한 방법으로 해결했다. 숫자, 단어, 문장을 읽는 각기 다른 Scanner의 메서드들이 이제야 손에 잡힐 듯 선명하게 느껴졌다. 이제 노트북을 닫아도 될 것 같았다.

그때, 거실 소파에 앉아있던 루나가 솔라의 노트북 화면을 향해 손짓했다. 솔라의 시선이 루나의 손끝을 따라 자신의 코드로 돌아갔다. 평소라면 무시하고 지나쳤을, 코드 편집기가 보내는 희미한 신호가 눈에 들어왔다. Scanner scanner = new Scanner(System.in); 부분에 그어진 옅은 노란색 밑줄. 에러를 의미하는 빨간색은 아니었지만, 분명 무언가 완전하지 않다는 표시였다.

“저 노란 밑줄, 전에도 본 적 있는 것 같은데…”

솔라가 중얼거렸다. 프로그램은 아무 문제 없이 잘 돌아갔기에, 솔라는 늘 그것을 사소한 경고쯤으로 여기고 넘겨왔다. 하지만 줄바꿈 문자가 숨겨놓은 함정을 겪고 나니, 이제는 작은 경고 하나도 예사롭게 보이지 않았다. 루나의 ‘도서관 책 비유’가 귓가에 맴돌았다. 책을 빌렸으면 반납해야 한다는 말.

“반납… scanner.close() 말하는 거지? 그거 안 하면 정말로 큰일 나? 내 프로그램은 멀쩡하게 잘 돌아가는데.”

솔라의 질문은 더 이상 ‘귀찮은데 왜 해야 해?’라는 투정이 아니었다. 보이지 않는 함정을 겪은 뒤, 눈에 보이지 않는 또 다른 규칙의 존재를 진지하게 묻는 탐구자의 질문에 가까웠다. close()를 하지 않아도 프로그램이 잘 돌아가는 이 현실과, 그것을 반드시 해야 한다는 규칙 사이의 간극을 이해하고 싶었다.

“작은 가게를 잠깐 열었다 닫는 거랑, 24시간 내내 운영하는 대형 쇼핑몰은 다르니까.”

루나는 비유를 이어갔다.

“솔라 네 프로그램은 아주 짧은 시간 동안만 ‘입력’이라는 문을 열고, 끝나면 바로 가게 전체가 문을 닫지. 그럼 운영체제가 ‘어차피 다 끝났네’ 하고 남은 것들을 정리해 줘. 그래서 당장 문제가 없는 것처럼 보이는 거야.”

루나는 솔라의 코드 맨 마지막 줄, main 메서드가 끝나기 바로 전에 커서를 옮겨 scanner.close(); 라는 한 줄을 타이핑했다. 그러자 마법처럼, Scanner 객체를 생성하던 첫 줄의 노란 밑줄이 스르르 사라졌다.

“어! 없어졌다.”

솔라가 작게 탄성을 질렀다. close() 한 줄이 코드 편집기의 경고를 잠재웠다. 이것은 close()가 단순한 권장 사항이 아니라, 짝이 맞는 약속이라는 명백한 증거였다.

“만약 프로그램이 끝나지 않고 계속 실행되는 서버라면 어떨까? 수많은 사용자가 드나들면서 계속 Scanner 같은 걸로 자원을 빌려 가는데, 아무도 반납하지 않는 거야. 도서관에서 책이 계속 사라지기만 하는 것처럼. 언젠가는 빌려줄 자원이 동나서 시스템 전체가 멈춰버리겠지. 그걸 ‘자원 누수(Resource Leak)’라고 불러. close()는 바로 그 누수를 막는, 아주 중요한 마개 역할을 하는 거야.”

그제야 솔라는 모든 것을 이해했다. close()는 프로그램의 예의범절 같은 것이었다. 작은 프로그램에서는 티가 나지 않지만, 이 습관이 몸에 배지 않으면 언젠가 큰 시스템에서 문제를 일으킬 수 있는 잠재적 위험 요소였다. 보이지 않던 위험의 실체를 확인하자, 오히려 마음이 편안해졌다.

“그럼 Scanner를 쓸 때마다 마지막에 close()를 까먹지 않도록 신경 써야겠네.”

“그것도 좋은 방법이지만, 더 안전하고 세련된 방법이 있어.”

루나는 방금 추가했던 scanner.close();를 지우고, 코드의 구조를 바꾸기 시작했다.

import java.util.Scanner;

public class ProfileMakerFinal {
    public static void main(String[] args) {
        try (Scanner scanner = new Scanner(System.in)) {
            System.out.print("나이를 입력하세요: ");
            int age = scanner.nextInt();
            scanner.nextLine(); // 버퍼 비우기

            System.out.println("당신의 나이는 " + age + "세입니다.");

            System.out.print("자기소개를 한 줄로 입력하세요: ");
            String intro = scanner.nextLine();
            System.out.println("자기소개: " + intro);

        } // 이 블록이 끝나는 순간, scanner.close()가 자동으로 호출됨
    }
}

“이건 try-with-resources 라는 구문이야. try 옆 괄호 안에 Scanner처럼 닫아줘야 하는 자원을 만들면, 이 try 중괄호 블록이 끝나는 순간 자바가 알아서 close()를 호출해 줘. 우리가 깜빡하고 잊어버릴 걱정이 아예 사라지는 거지.”

솔라는 바뀐 코드를 가만히 들여다보았다. close()를 직접 쓰는 것보다 훨씬 깔끔하고 안전해 보였다. 실수를 원천봉쇄하는 구조였다.

자료형의 ‘모양’에 맞는 메서드를 고르는 것부터 시작된 여정이었다. 단어와 줄의 차이를 배우고, 보이지 않는 줄바꿈 문자의 함정을 발견해 해결했다. 그리고 마지막으로, 사용한 자원을 책임지고 반납하는 방법까지. 어지럽게 흩어져 있던 Scanner의 조각들이 마침내 하나의 완벽한 그림으로 맞춰졌다.

솔라는 루나에게서 노트북을 돌려받았다. 그리고 망설임 없이 ProfileMakerFinal 코드를 직접 타이핑하기 시작했다. 이제는 루나의 코드를 따라 치는 것이 아니었다. 온전히 자신의 것이 된 지식으로, 더 견고하고 올바른 코드를 스스로 만들어내고 있었다.

try (Scanner scanner = new Scanner(System.in))

한 글자 한 글자 코드를 완성해나가며 솔라는 희미하게 미소 지었다. 이제 Scanner의 다양한 얼굴들이 낯설지 않았다. 언제 어떤 표정을 짓는지, 그리고 어떻게 대해야 하는지 명확히 알게 되었기 때문이다. 이것으로 Scanner와의 첫 만남은 깔끔하게 마무리되었다.