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를 누르면
이게 프로그램이 실제로 죽기 전까지 몇번 반복되는 거 같다.
그러니까 7번, func2의 while문이 scanner가 더이상 처리할게 없다고 보고했을때 종료를 안한게 문제였던 거다.
그에비해 func1은 '혹시 버퍼에 있는 첫번째 토큰이 숫자니?' 하고 물어본뒤 없으면 while문을 빠져나오기 때문에 func2와 같은 문제를 겪지 않는다...
...프로그래밍에서는 정말 간단한 일도 알고보면 잘못될 구석이 많다.