스파르타 spring 2기 TIL day13

fart man·2025년 12월 11일

Java scanner에 대해

Java scanner를 쓰면서 문제를 직면했다.

간단한 프로그램을 보자.

import java.util.Scanner;

class Main {
    public static void main(String[] args) {
        func1();
        // func2();
    }

    public static void func1() {
        Scanner scanner = new Scanner(System.in);
        System.out.printf("input number : ");

        while (scanner.hasNextLong()) {
            long l = scanner.nextLong();
            System.out.printf("you typed %d\n", l);
            System.out.printf("input number : ");
        }
    }

    public static void func2() {
        Scanner scanner = new Scanner(System.in);

        while (true) {
            System.out.printf("input number : ");
            long l = 0;
            if (scanner.hasNextLong()) {
                l = scanner.nextLong();
            }
            System.out.printf("you typed %d\n", l);
        }
    }
}

func1, func2 둘다 비슷한 일을 하는 듯 보인다.

하지만 실제로 돌려보면 이상한 차이가 있다.

        func1();
        // func2();

의 경우,

C:>run scanner.java
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
input number : 10
you typed 10
input number : 22
you typed 22
input number : 33
you typed 33
input number : ^CTerminate batch job (Y/N)? y

이렇듯 잘 돌아간다. 하지만

        // func1();
        func2();

의 경우,

C:>run scanner2.java 
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
input number : 10
you typed 10
input number : 22
you typed 22
input number : 33
you typed 33
input number : you typed 0 // ctrl-c를 여기서 누름
input number : you typed 0
input number : you typed 0
input number : you typed 0
input number : you typed 0
input number : you typed ^CTerminate batch job (Y/N)? 0
input number : y

ctrl-c 를 눌러 SIGINT를 보낼경우 바로 종료가 되지 않는다. 왜그럴까? AI한테도 물어보고 실제 scanner 코드도 바라본 결과 내 이해로는 이런 일이 발생하는듯 하다.

    public boolean hasNextLong(int radix) {
        setRadix(radix);
        boolean result = hasNext(integerPattern());
        if (result) { // Cache it
            try {
                String s = (matcher.group(SIMPLE_GROUP_INDEX) == null) ?
                    processIntegerToken(hasNextResult) :
                    hasNextResult;
                typeCache = Long.parseLong(s, radix);
            } catch (NumberFormatException nfe) {
                result = false;
            }
        }
        return result;
    }

    ...

    public boolean hasNext() {
        ensureOpen();
        saveState();
        modCount++;
        while (!sourceClosed) {
            if (hasTokenInBuffer()) {
                return revertState(true);
            }
            readInput();
        }
        boolean result = hasTokenInBuffer();
        return revertState(result);
    }

    ...

    private void ensureOpen() {
        if (closed)
            throw new IllegalStateException("Scanner closed");
    }

    ...

    private void readInput() {
        if (buf.limit() == buf.capacity())
            makeSpace();
        // Prepare to receive data
        int p = buf.position();
        buf.position(buf.limit());
        buf.limit(buf.capacity());

        int n = 0;
        try {
            n = source.read(buf);
        } catch (IOException ioe) {
            lastException = ioe;
            n = -1;
        }
        if (n == -1) {
            sourceClosed = true;
            needInput = false;
        }
        if (n > 0)
            needInput = false;
        // Restore current position and limit for reading
        buf.limit(buf.position());
        buf.position(p);
    }

    ...

    public void close() {
        if (closed)
            return;
        if (source instanceof Closeable) {
            try {
                ((Closeable)source).close();
            } catch (IOException ioe) {
                lastException = ioe;
            }
        }
        sourceClosed = true;
        source = null;
        closed = true;
    }

긴 코드지만 여기서 주목할 점은 Scanner 한테는 sourceClosed와 closed의 개념이 다르다는 것이다!!!

실제로 closed를 true로 만든는 것은 Scanner의 저 close method뿐이다. 실제 InputStream이 닫혔는지 안닫혔는지(sourceClosed) scanner는 신경도 안쓴다.

readInput을 읽어보자. Scanner는 IOException은 catch하고 sourceClosed만 true로 바꾼뒤 조용히 무시한다. closed는 건들지도 않는다.

그리고 실제로 blocking을 하늗 readInput은 InputStream이 죽었을 경우 blocking을 하지 않고 그냥 없다고 보고만 한다.

그러니 func2를 실행하다 유저가 ctrl-c를 누르면

  1. func2가 hasNextLong을 부른다
  2. hasNextLong이 hasNext를 부른다
  3. ensureOpen은 scanner가 닫혀(closed)있는지만 신경쓰기 때문에 통과
  4. while문은 InputStream이 닫혔기 때문에 아예 실행 되지도 않음.
  5. 글고 버퍼에는 아무것도 없기 때문에 hasNext가 false를 돌려줌
  6. hasNextLong은 없다고 보고.
  7. func2로 돌아와서 while 문에서 1번으로 돌아감.

이게 프로그램이 실제로 죽기 전까지 몇번 반복되는 거 같다.

그러니까 7번, func2의 while문이 scanner가 더이상 처리할게 없다고 보고했을때 종료를 안한게 문제였던 거다.

그에비해 func1은 '혹시 버퍼에 있는 첫번째 토큰이 숫자니?' 하고 물어본뒤 없으면 while문을 빠져나오기 때문에 func2와 같은 문제를 겪지 않는다...

배운점

...프로그래밍에서는 정말 간단한 일도 알고보면 잘못될 구석이 많다.

0개의 댓글