안녕하세요! 이번 글에서는 간단해 보였지만 예상치 못한 곳에서 발목을 잡았던 테스트 코드 트러블슈팅 과정을 공유하고자 합니다.
제가 구현하려는 로직은 사용자로부터 입력받은 값이 공백일 경우 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" 뿐입니다.
즉, 우리가 만들어 둔 ""는 없는 입력이 되버리는 겁니다.
🤔 그렇다면 왜 실제 실행(사용자 입력)에서는 문제가 발생하지 않는가?
플로우로 설명을 드리겠습니다.
- (프로그램 실행) Console.readLine() 호출 → 사용자가 pobi,java 입력 + 엔터 → readLine()은 "pobi,java" 반환
- 다음 Console.readLine() 호출 → 사용자가 엔터만 누름 → readLine()은 "" 반환
- checkEmptyInput("") 실행 → IllegalArgumentException 발생
즉, 실제 환경에서는 ""이 정상적으로 전달되어 checkEmptyInput()이 호출됩니다.
그러니 IllegalArgumentException이 성공적으로 발생하는겁니다.
😨 그렇다면 왜 테스트 실행(현재 문제)에서는 문제가 발생하는가?
플로우를 보면
- run("pobi,java", "") → command()가 System.in을 "pobi,java\n"으로 설정:
- 첫 번째 Console.readLine() → 스트림에서 "pobi,java" 읽음 (정상)
- 두 번째 Console.readLine() → 스트림에 더 이상 데이터가 없음 → readLine()/Scanner.nextLine()은 읽을 라인이 없어 NoSuchElementException 발생
- 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 |
