Java 프로그램에서 외부 프로그램을 실행하고 싶을 때, Runtime.exec() 또는 Java 1.5에서 추가된 ProcessBuilder를 사용해 Process 객체를 얻을 수 있다. 처음으로 이런 프로그램을 짤 때, 실수한 것이 없어보이는데도 실행한 프로그램이 종료되지 않는 경우가 있다. 더군다가 어떤 경우엔 정상적으로 종료되고 어떤 경우엔 종료가 되지않는 경우도 보인다.
원인은 바로 Process 클래스의 API Reference에 있는 다음과 같은 설명에서 찾을 수 있다.
The created subprocess does not have its own terminal or console. All its standard io (i.e. stdin, stdout, stderr) operations will be redirected to the parent process through three streams (getOutputStream(), getInputStream(), getErrorStream()). The parent process uses these streams to feed input to and get output from the subprocess. Because some native platforms only provide limited buffer size for standard input and output streams, failure to promptly write the input stream or read the output stream of the subprocess may cause the subprocess to block, and even deadlock.
즉, Java에서는 Child 프로세스로의 표준 입출력을 Parent 프로세스가 다루어주어야 한다. 당연하게도, Child의 출력을 언제까지고 버퍼링할 수 없기 때문에, 다른 프로그램을 실행할 때는, 특히 표준 입출력이 존재하는 프로그램을 실행할 경우에는, 항상 이 스트림들의 입력 또는 출력을 제대로 다루어주어야 한다.
이러한 동작은 일반적으로 프로그래머들이 익숙한 시스템콜들, 이를테면 UNIX 계열의 fork()/exec()나 Windows의 CreateProcess()처럼, parent 프로세스의 터미널/콘솔을 공유하는 동작과는 다르기 때문에, 프로그래머들이 쉽게 간과하고 실수하기 쉽다. 더군다나 API Reference가 이러한 점을 명확히 설명하고 있지도 않다.
해결책은 당연하게도 표준 입력이 필요할 때는 Process.getOutputStream()을 이용하여 필요한 입력을 해주고 스트림을 닫아주어야하고, 표준 출력이 필요할 때는 Process.getInputStream() 그리고 Process.getErrorStream()을 이용하여 스트림을 비워주어야한다. 드물겠지만 만약 실행할 프로그램이 interactive한 프로그램일 경우에는 입출력에 좀 더 신경을 써야한다.
다음은 표준 출력을 비워주기 위한 코드가 들어간 간단한 예다.
Runtime rt = Runtime.getRuntime(); try { Process proc = rt.exec("cmd /c dir"); // Process proc = new ProcessBuilder().command("cmd", "/c", "dir").start(); InputStream is = proc.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); String line; while ((line = reader.readLine()) != null) { System.out.println(line); } int exitVal = proc.waitFor(); System.out.println("Process exited with " + exitVal); } catch (Exception e) { System.err.println("Failed to execute: " + e.getMessage()); }
표준 출력 뿐만 아니라 표준 에러까지도 처리를 할 수 있어야 좀 더 일반적인 코드일 것이다. 표준 에러의 버퍼가 차버리면 표준 출력을 비우려고 해도 블럭될 수 있다. 이러한 문제는 Java World의 When Runtime.exec() won’t 라는 글에서 StreamGobbler라는 클래스를 도입해서 약간 더 우아하게 해결하고 있다. 하지만, 간단한 작업을 하기 위해 쓰레드를 사용하고 있기 때문에 좀 오버킬이라는 생각이 든다.
표준 출력과 표준 에러를 간단하게 무시하고 다른 프로그램을 실행할 수 있도록 하는 옵션이 있다면 자주 사용할 수 있으며 편리할 듯 하다. 또는, 디폴트 동작을 일반적인 시스템콜의 동작과 비슷하게 만들어주는 것도 괜찮을 듯하다. 시간이 나면 이런 방식의 Wrapper를 찾아보거나 만들어보아야겠다.