우테코의 Console

Geek Cat·2024년 10월 26일

📘 이 글에서는...

우아한테크코스 7기를 프리코스를 하면서 우테코에서 제공하는 라이브러리 안에 있는 Console.java 파일에 대해 알아보았습니다.
왜 굳이 해당 클래스를 사용해서 입력을 받아야 하는지 그리고 주의할 점들에 대해 남기고자 합니다.

🛠️ Console의 구조

public class Console {
    private static Scanner scanner;

    private Console() {
    }

    public static String readLine() {
        return getInstance().nextLine();
    }

    public static void close() {
        if (scanner != null) {
            scanner.close();
            scanner = null;
        }
    }

    private static Scanner getInstance() {
        if (scanner == null) {
            scanner = new Scanner(System.in);
        }
        return scanner;
    }
}

Singleton

Console에는 기본적으로 Scanner를 사용하여 입력을 받습니다. Singleton 패턴을 적용하였고, 정확히는 Lazy initialization 방식의 singleton 패턴이 적용되어 있습니다.
Singleton을 사용함으로써의 리소스를 효율적으로 사용할 수 있습니다.

Console 클래스에 유저가 사용할 수 있는 메소드

  1. readLine
  2. close

readLine

public static String readLine() {
	return getInstance().nextLine();
}
private static Scanner getInstance() {
	if (scanner == null) {
		scanner = new Scanner(System.in);
	}
	return scanner;
}

readLine 메소드를 호출하게 되면 먼저 getInstance 메소드를 호출하면서 Scanner가 할당되어 있지 않다면 생성하고 그렇지 않다면 이미 만들어져있는 인스턴스를 가져오게 됩니다. 그리고 Scanner의 nextLine메소드를 호출하게 됩니다.

close

public static void close() {
	if (scanner != null) {
		scanner.close();
       	scanner = null;
	}
}

'scanner가 할당이 되어 있다면' scanner를 닫고 null로 초기화 합니다.

🔍 Scanner의 nextLine

여기까지만 보면 아쉽겠죠(?)
결국 Console의 readLine은 Scanner의 nextLine을 호출합니다.
next라인은 어떻게 구성이 되어 있을까요?

public String nextLine() {
	modCount++;
	if (hasNextPattern == linePattern())
		return getCachedResult();
	clearCaches();

	String result = findWithinHorizon(linePattern, 0);
	if (result == null)
		throw new NoSuchElementException("No line found");
	MatchResult mr = this.match();
	String lineSep = mr.group(1);
	if (lineSep != null)
		result = result.substring(0, result.length() - lineSep.length());
	if (result == null)
		throw new NoSuchElementException();
	else
		return result;
}

생각보다 코드가 복잡하진 않습니다. 코드를 부분적으로 살펴보겠습니다.
그 전에 먼저 알야아 하는 부분이 있습니다. Scanner는 기본적으로 정규표현식(regex)를 사용하고 있습니다. 때문에 nextLine과 같은 메소드를 실행시키게 되면 정규표현식으로 값을 가져와 사용자에게 반환합니다. 예를 들어서 '한 줄'을 가지고 오고싶다면 '\n'을 기준으로 앞에 있는 문자열들을 가지고 오는 방식입니다.

String result = findWithinHorizon(linePattern, 0);

해당 코드에서 가장 중요한 부분입니다. linePattern에는 한 줄을 가져오는 정규표현식을 컴파일한 패턴 객체이고 이 패턴 객체를 이용하여 한 줄을 가져옵니다.

예외

nextLine에서 던지는 예외로 크게 두 가지가 있습니다.
1. NoSuchElementException
2. IllegalStateException

NoSuchElementException 은 요구한 요소가 존재하지 않을 때 발생하는 예외입니다. 즉 정규표현식으로 한 줄을 찾고자 하였으나 존재하지 않을 때 발생합니다.

IllegalStateException는 scanner를 통해서 데이터를 읽으려고 하였으나 닫혀있는 상태일 때 던지는 에러입니다.

따라서 읽고자 하는 내용이 없다면 NoSuchElementException이 발생하게 됩니다. 그래서 Console의 readLine을 사용한다면 NoSuchElementException에 대한 에러 핸들링이 필요해 보입니다.

⚠️ 추가적인 주의사항 - NsTest 클래스

저는 테스트 코드를 작성하면서 우테코에서 작성한 NsTest 클래스의 run함수를 사용했습니다.

protected final void run(final String... args) {
	try {
		command(args);
		runMain();
	} finally {
		Console.close();
	}
}

여러 문자열을 넣으면 해당 문자열들을 입력스트림으로 넣어주는 역할을 하게됩니다. 그리고 테스트가 종료되면 Console.close 를 통해 Scanner를 닫아줍니다.

run("pobi,woni", "1")

다음과 같이 사용이 됩니다.

final byte[] buf = String.join("\n", args).getBytes();

이때 입력한 "pobi,woni"와 "1"는 String.join으로 "pobi,woni\n1"로 변환되어 버퍼스트림에 들어가게 됩니다.

여기에서 중요한 점은요...

만약 입력으로 "5"와 "" 를 입력 했을 때, 이 입력값은 다시 문장으로 풀어서 설명하면 "5"와 아무것도 입력하지 않은 채로 엔터를 입력하여 들어간 '빈문자열' 입니다. 그러면 프로그램에는 총 2개의 입력값이 들어갈 것으로 예상이 됩니다.

그런데,, Scanner에서 nextLine을 했던 방식을 다시 떠올려봅시다. "5"와 ""은 '\n'로 join을 하면 "5\n" 이 됩니다. 그런데 Scanner에서 nextLine을 할 경우 처음에는 "5\n" 이 나오고 두 번째에는 null이 나오게 됩니다. 때문에 빈 문자열일 ""이 아니라 null이 반환되어 NoSuchElementException이 발생됩니다.

음.. 왜 이렇게 구현한지는 모르겠지만 제 생각에는 join을 한 뒤에 '\n'을 하나 더 붙여야 한다고 생각합니다. 빈문자열도 엄연히 입력값이고 이를 고려하지 않았다고 봅니다.

그래서 만약 NsTest 클래스의 run메소드를 사용한다면, NoSuchElementException에 대한 예외처리를 해주거나, 인자를 하나 더 넣어서 빈문자열을 표현해야 합니다.

✅ 결론

우아한테크코스에서는 입력 처리를 위한 Console 클래스 사용을 강제화하고 있습니다. 이는 지원자 수가 많은 프리코스 환경에서 테스트 리소스 사용을 최적화하고, 성능을 높이기 위한 설계로 볼 수 있습니다.

프리코스에서는 많은 지원자가 동시에 코드를 제출하고 테스트를 수행하게 됩니다. 이때, 다양한 방식으로 입력을 처리하게 되면 불필요한 리소스가 소모되고 시스템 성능에 영향을 줄 수 있습니다. 이를 방지하기 위해 일관된 입력 방식으로 Console 클래스를 사용하도록 강제하여 리소스 낭비를 최소화하고, 안정적인 테스트 환경을 유지합니다.

또한, NsTest 클래스는 테스트가 완료된 후 Console.close()를 통해 Scanner를 명확히 종료합니다. 이는 테스트 자원을 명확히 관리하여 자원 누수를 방지하고 테스트 환경의 안정성을 높이는 역할을 합니다.

마지막으로, NsTest 클래스의 run 메소드를 사용시 빈문자열을 입력할 때 NoSuchElementException이 발생할 수 있으니 주의해주세요!

profile
안녕하세요. 개발자 Geek Cat 입니다.

0개의 댓글