[트러블슈팅] 공백 입력 테스트코드, NoSuchElementException

2025. 10. 28. 17:17·Develop note

안녕하세요! 이번 글에서는 간단해 보였지만 예상치 못한 곳에서 발목을 잡았던 테스트 코드 트러블슈팅 과정을 공유하고자 합니다.

제가 구현하려는 로직은 사용자로부터 입력받은 값이 공백일 경우 IllegalArgumentException을 발생시키는 것이었습니다.

 

1. 🤔 문제 발생 및 특이점

발생한 문제의 코드

다음은 두 번째 입력값(시도 횟수)이 빈 문자열("")일 때, 예상 예외인 IllegalArgumentException이 정상적으로 발생하는지를 확인하려던 테스트 코드입니다.

@Test
void 공백_횟수_입력2() {
    assertSimpleTest(() ->
        assertThatThrownBy(() -> runException("pobi,java", ""))
            .isInstanceOf(IllegalArgumentException.class)
    );
}

빈 입력인지를 확인하고 IllegalArgumentException을 반환하는 코드는 아래와 같이 짜여있었습니다.

public void checkEmptyInput(String input) {
    if (input.trim().isEmpty()) {
        RacingCarExceptionHelper.throwEmptyInputException();
    }
}

 

문제는 이 코드를 실행시켜도, IllegalArgumentException가 아닌 NoSuchElementException이 발생하고 코드가 종료된다는 것이었습니다.

 

또한 이해가 안되는 부분은 testcode에서만 문제가 발생하지, 코드를 run하고 빈 문자열을 입력해보면, 정상적으로 IllegalArgumentException이 발생한다는 점이었습니다.

 

특이했던 코드 (정상 동작)

반면, 아래 코드는 첫 번째 입력값(자동차 이름)이 빈 문자열일 때 정상적으로 IllegalArgumentException을 발생시키며 통과했습니다.

@Test
void 공백_횟수_입력1() {
    assertSimpleTest(() ->
        assertThatThrownBy(() -> runException("", "2"))
            .isInstanceOf(IllegalArgumentException.class)
    );
}
argument가 다르니까 당연히 이건 통과할 수 있지

 

그렇게 간단한 문제가 아닙니다...

run의 첫번째 argument와 두 번째 argument는 모두 아래의 동일한 검증 로직을 사용하고 있었기 때문에, 두 번째 입력값에서만 문제가 발생하는 것은 매우 의아한 현상이었습니다.

public void checkEmptyInput(String input) {
    if (input.isEmpty()) {
        RacingCarExceptionHelper.throwEmptyInputException(); // IllegalArgumentException 발생
    }
}

 

 

발생한 현상을 정리하자면 아래와 같습니다.

    • 테스트 코드에서는 빈 문자열 입력 시 IllegalArgumentException 대신 NoSuchElementException 발생
    • 실제 실행(run)에서는 IllegalArgumentException 정상 발생
    • 테스트 환경의 입력 스트림이 ""를 EOF로 처리함
    • checkEmptyInput() 호출 전에 Scanner.nextLine() 단계에서 예외 발생
    • 테스트 유틸(assertSimpleTest)의 입력 시뮬레이션 로직 불일치 가능성
    • 첫 번째 입력값이 빈 문자열일 때는 IllegalArgumentException 정상 발생
    • 두 번째 입력값에서만 실패한 이유는 두 입력 모두 같은 checkEmptyInput() 로직을 사용함에도, 테스트 환경에서만 두 번째 입력이 실제로는 전달되지 않은 것(EoF 처리) 으로 보임

2. 👀 실패했던 트러블슈팅 시도들

문제의 원인을 찾기 위해 몇 가지 가설을 세우고 시도해 보았습니다.


1차 시도: 빈 문자열 검증 로직 의심

가장 먼저 String.isEmpty() 메서드의 동작을 의심했습니다.

  • input.isEmpty()를 input.equals("")로 수정했습니다.
  • 혹시 모를 공백 문자(Whitespace)를 제거하기 위해 input.trim().isEmpty()로 수정했습니다.
public void checkEmptyInput(String input) {
    if (input.trim().isEmpty()) {
        RacingCarExceptionHelper.throwEmptyInputException();
    }
}

 

결과: 실패. 문제의 원인은 로직 자체가 아닌 다른 곳에 있었습니다.


2차 시도: 테스트 헬퍼 메서드 의심

다음으로, 테스트를 지원하는 헬퍼 클래스(NsTest)의 runException()이나 입력 처리 방식에 문제가 있을 수 있다고 의심했습니다.

protected final void runException(final String... args) {
    try {
        run(args);
    } catch (final NoSuchElementException ignore) {
    }
}
  • 테스트 헬퍼 메서드의 오동작 가능성을 배제하기 위해, 헬퍼를 거치지 않고 assertThrows를 직접 사용하여 예외를 확인해 보았습니다.
@Test
void 공백_횟수_입력3() {
    // when & then
    assertThrows(IllegalArgumentException.class, () -> {
        run("pobi,java", "");
    });
}

 

결과: 실패. 이 역시 문제의 본질은 아니었습니다.


3차 시도: 의존성 주입(DI) 및 객체 생성을 의심

예외 상황 테스트 중 객체 생성 문제로 Mock이 제대로 넘겨지지 않아 발생하는 오류를 의심하고, DI 패턴을 적용하여 코드를 수정해 보았으나 역시 동일한 문제에 직면했습니다.

 

⏸️ 수정 전 코드

public class Application {
    public static void main(String[] args) {
        // TODO: 프로그램 구현
        RacingCarController racingCarController = new RacingCarController();
        racingCarController.run();
    }
}
public class RacingCarController {
    private final ConsoleOutput consoleOutput;
    private final InputValidator inputValidator;
    private final RacingCarService racingCarService;

    public RacingCarController() {
        this.consoleOutput = new ConsoleOutput();
        this.inputValidator = new InputValidator();
        this.racingCarService = new RacingCarService();
    }
}

 

▶️ 수정 후 코드

public class Application {
    public static void main(String[] args) {
        // TODO: 프로그램 구현
        ConsoleOutput consoleOutput = new ConsoleOutput();
        InputValidator inputValidator = new InputValidator();
        RacingCarService racingCarService = new RacingCarService();

        RacingCarController racingCarController = new RacingCarController(consoleOutput, inputValidator, racingCarService);
        racingCarController.run();
    }
}
public class RacingCarController {
    private final ConsoleOutput consoleOutput;
    private final InputValidator inputValidator;
    private final RacingCarService racingCarService;

    public RacingCarController(ConsoleOutput consoleOutput, InputValidator inputValidator,
                               RacingCarService racingCarService) {

        this.consoleOutput = consoleOutput;
        this.inputValidator = inputValidator;
        this.racingCarService = racingCarService;
    }
}

 

결과: 실패. 이 시점에서는 테스트 환경과 실제 애플리케이션 환경의 차이점을 분석해야 함을 깨달았습니다.


3. 💪 핵심 분석: 테스트 환경과 입력 스트림

결론을 먼저 말씀드리자면, 문제는 테스트 코드가 사용자 입력을 처리하는 방식에 있었습니다.

테스트에서 run("pobi,java", "")가 NoSuchElementException을 던지는 이유는 NsTest.command()가 String.join("\n", args)로 입력 스트림을 만든 결과 두 번째 빈 인자가 실제로는 빈 줄("")이 아닌“존재하지 않는 입력(EOF)”로 처리되기 때문입니다.

이를 해결하려면 입력 스트림에 명시적으로 빈 라인(즉 \n\n)을 넣어야 합니다.

 

그냥 코드를 실행하고 값을 입력하면 Console.readLine()은 실제 콘솔에서는 사용자가 엔터를 치기를 기다리고, 엔터만 치면 ""(빈 문자열)를 반환합니다.

 

👉 But in NsTest

private void command(final String... args) {
    final byte[] buf = String.join("\n", args).getBytes();
    System.setIn(new ByteArrayInputStream(buf));
}

위 코드에서 run("pobi,java", "")을 호출하면 내부적으로는:

String.join("\n", new String[] {"pobi,java", ""}) == "pobi,java\n"

이런 코드가 실행되게 되고, 결과적으로 System.in에 들어가는 바이트는 "pobi,java\n" 뿐입니다.

 

즉, 우리가 만들어 둔 ""는 없는 입력이 되버리는 겁니다.


 

🤔 그렇다면 왜 실제 실행(사용자 입력)에서는 문제가 발생하지 않는가?

플로우로 설명을 드리겠습니다.

  1. (프로그램 실행) Console.readLine() 호출 → 사용자가 pobi,java 입력 + 엔터 → readLine()은 "pobi,java" 반환
  2. 다음 Console.readLine() 호출 → 사용자가 엔터만 누름 → readLine()은 "" 반환
  3. checkEmptyInput("") 실행 → IllegalArgumentException 발생

즉, 실제 환경에서는 ""이 정상적으로 전달되어 checkEmptyInput()이 호출됩니다.

그러니 IllegalArgumentException이 성공적으로 발생하는겁니다.

 

😨 그렇다면 왜 테스트 실행(현재 문제)에서는 문제가 발생하는가?

플로우를 보면

  1. run("pobi,java", "") → command()가 System.in을 "pobi,java\n"으로 설정:
  2. 첫 번째 Console.readLine() → 스트림에서 "pobi,java" 읽음 (정상)
  3. 두 번째 Console.readLine() → 스트림에 더 이상 데이터가 없음 → readLine()/Scanner.nextLine()은 읽을 라인이 없어 NoSuchElementException 발생
  4. checkEmptyInput()에 도달하지 못함 → 우리가 기대한 IllegalArgumentException이 발생하지 않음

또한 NsTest의 runException에서는 NoSuchElementException을 catch해서 그냥 ignore해버리고 있습니다.

이렇게 되니, 겉으로는 예외가 발생하지 않은 것처럼 보이는 것이었습니다.


4. ☺️ 그렇다면 어떻게 수정할 것인가?

방법은 많겠지만, 몇가지 보여드리자면

test에서 IllegalArgumentException이 아닌 NoSuchElementException을 던지고 있는지를 검증하면 통과합니다.

@Test
void 공백_횟수_입력2() {
    assertSimpleTest(() ->
        assertThatThrownBy(() -> runException("pobi,java", ""))
            .isInstanceOf(NoSuchElementException.class)
    );
}

 

하지만, 이 방법은 저희가 검증하고자 하는 빈 입력 확인 로직까지 도달하지 못합니다.

 

그렇다면, 무시될 수 있으나, join에서는 null값으로 처리되지 않는 개행 문자를 하나 보내봅니다.

@Test
void 공백_횟수_입력2() {
    assertSimpleTest(() ->
        assertThatThrownBy(() -> runException("pobi,java", "\n"))
            .isInstanceOf(IllegalArgumentException.class)
    );
}

 

결과는...?

 

 

아아... 성공입니다.


5. 🧹 정리!

  • 실제 실행: readLine()은 사용자가 엔터를 치면 빈 문자열 ""을 반환할 수 있음.
  • 테스트: NsTest.command() 방식(String.join("\n", args))은 빈 인자를 무조건 빈 라인으로 남기지 않음 → 결과적으로 두 번째 입력이 없음(EOF) 으로 처리됨.
  • 따라서 테스트에서 빈 입력(빈 라인)을 의도적으로 포함시키지 않으면 readLine()이 NoSuchElementException을 던짐.
  • 가장 안전한 해결책은 테스트 유틸을 수정하여 각 인자를 항상 라인 단위로 넣어주도록 하는 것(sb.append(arg); sb.append('\n');).

아아... 이번 트러블 슈팅은 머리가 다 빠지는 줄 알았습니다...

 

여러분도 제공된 라이브러리가 있다면, 저처럼 무작정 사용하지 말고 플로우를 자세히 살펴보고 사용하시기 바랍니다...

 

 

 

 

끗!

'Develop note' 카테고리의 다른 글

[Java] Utility class란?  (1) 2025.10.25
[Pattern] 예외 팩토리(Exception Factory) 패턴: 깔끔하고 유연한 예외 처리  (1) 2025.10.23
[Git] Git alias 설정은 어떻게 돌아가는가?  (0) 2025.10.20
[Git] Hooks 적용으로 커밋 메세지 형식 강제하기  (0) 2025.10.15
'Develop note' 카테고리의 다른 글
  • [Java] Utility class란?
  • [Pattern] 예외 팩토리(Exception Factory) 패턴: 깔끔하고 유연한 예외 처리
  • [Git] Git alias 설정은 어떻게 돌아가는가?
  • [Git] Hooks 적용으로 커밋 메세지 형식 강제하기
youngi2
youngi2
영기의 개발기록입니다!
  • youngi2
    영기의 개발일지
    youngi2
  • 전체
    오늘
    어제
    • 분류 전체보기 (15)
      • Algorithm (10)
      • Develop note (5)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    commit-msg
    7578 코틀린
    7578 kotlin
    백준 7578 kotlin
    예외 팩토리 패턴
    예외팩토리
    유틸리티클래스
    백준 2618 kotlin
    유틸리티 클래스
    백준 2618 코틀린
    2618 kotlin
    Exception Factory Pattern
    백준 7578 코틀린
    백준
    GIT
    예외팩토리패턴
    2618 코틀린
    백준 7578
    Exceptino Factory
    utility class
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
youngi2
[트러블슈팅] 공백 입력 테스트코드, NoSuchElementException
상단으로

티스토리툴바