1. 스트림 읽기/쓰기 시 데드락
가장 까다로운 오류 중 하나는 프로세스가 멈춘 채 종료되지 않는 상황입니다. 모든 것을 제대로 한 것처럼 보여도 그렇습니다. 흔한 원인은 외부 프로세스의 출력 또는 오류 스트림 버퍼가 가득 차는 것입니다. 두 스트림(stdout과 stderr)을 모두 읽지 않으면, 아무도 버퍼에서 데이터를 가져가지 않기 때문에 프로세스가 무언가를 출력하려다 막힐 수 있습니다.
문제가 되는 예
ProcessBuilder builder = new ProcessBuilder("java", "-version");
Process process = builder.start();
// stdout만 읽고, stderr는 무시합니다!
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
명령이 stderr로 무언가를 출력하면(예를 들어, java -version은 거의 항상 그쪽으로 출력합니다!), 해당 스트림 버퍼가 가득 차고 프로세스가 멈출 수 있습니다.
올바른 방법
두 스트림을 병렬로 읽으세요(별도 스레드 또는 ExecutorService 사용):
ProcessBuilder builder = new ProcessBuilder("java", "-version");
Process process = builder.start();
// stdout 읽기
Thread stdoutThread = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("[stdout] " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
});
// stderr 읽기
Thread stderrThread = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.err.println("[stderr] " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
});
stdoutThread.start();
stderrThread.start();
process.waitFor();
stdoutThread.join();
stderrThread.join();
결론:
stderr를 읽지 않으면 프로세스가 멈출 수 있습니다.
stdout을 읽지 않아도 멈출 수 있습니다.
두 스트림을 모두 읽으세요.
2. 인코딩 문제
외부 프로세스는 귀하의 Java 프로그램 기본값과 다른 인코딩으로 텍스트를 출력할 수 있습니다. 올바른 인코딩을 지정하지 않으면 텍스트가 깨져 보일 수 있습니다(특히 키릴 문자에서 두드러집니다).
잘못된 예
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
이 코드는 시스템 기본 인코딩을 사용합니다. 하지만 외부 프로세스가 예를 들어 UTF-8로 출력하고, 로컬이 Windows-1251라면 문자가 깨져서 읽을 수 없게 됩니다.
올바른 방법
필요한 인코딩을 알고 있다면 명시적으로 전달하세요:
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)
);
확신이 없다면 프로그램 문서를 확인하거나 여러 옵션을 시도해 보세요.
팁: Windows의 콘솔 유틸리티는 CP866 또는 Windows-1251을, Linux는 UTF-8을 사용하는 경우가 많습니다.
3. 플랫폼 차이
한 운영체제에서 동작하는 명령이 다른 운영체제에는 없을 수 있습니다. 예를 들어 ls는 Linux/Mac에 있지만 Windows에는 없습니다(대신 dir). 명령의 구문, 경로 구분자, 따옴표 처리도 서로 다릅니다.
잘못된 예
ProcessBuilder builder = new ProcessBuilder("ls", "-l");
builder.start(); // Windows에서는 "ls"를 찾을 수 없습니다!
올바른 방법
String os = System.getProperty("os.name").toLowerCase();
ProcessBuilder builder;
if (os.contains("win")) {
builder = new ProcessBuilder("cmd", "/c", "dir");
} else {
builder = new ProcessBuilder("ls", "-l");
}
파일 경로: 경로 문제를 피하려면 “/”나 “\” 대신 File.separator를 사용하세요.
4. 권한 문제
일부 명령은 관리자 권한이 필요합니다(시스템 파일 삭제, 네트워크 설정 변경 등). 권한이 부족하면 명령이 오류로 종료하거나 아예 시작되지 않을 수 있습니다.
예
ProcessBuilder builder = new ProcessBuilder("rm", "-rf", "/root/secret.txt");
Process process = builder.start();
// ... 'Permission denied' 오류가 발생할 것입니다
올바른 방법
- 명령에 관리자/상승 권한이 필요한지 확인하세요.
- process.exitValue()로 프로세스 종료 코드를 처리하세요.
- stderr를 읽으세요 — 대개 오류 원인이 들어 있습니다(예: «Permission denied»).
5. 리소스 누수
프로세스의 스트림(InputStream, OutputStream, ErrorStream)을 닫지 않으면 열린 채로 남아 리소스를 점유하고 종료를 방해할 수 있습니다. 마찬가지로 프로세스를 종료하지 않으면 시스템에 ‘좀비’로 남을 수 있습니다.
잘못된 예
Process process = builder.start();
// ... 처리만 하고 스트림을 닫지 않습니다!
올바른 방법
스트림에는 try-with-resources를 사용하세요:
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
// 출력 읽기
}
프로세스 작업이 끝난 뒤에는 올바르게 중지하세요:
process.destroy(); // 프로세스 종료(아직 살아 있다면)
주의: 스트림을 닫지 않으면 메모리 누수, 멈춤, 시스템 문제(특히 대량의 프로세스를 실행할 때)가 발생할 수 있습니다.
6. 대화형 상호작용 시 데드락
대화형 통신에서는 교착 상태에 빠지기 쉽습니다. Java 측은 응답을 기다리고, 외부 프로세스는 입력을 기다리며 서로 대기만 하는 상황입니다. 또는 메시지를 보냈지만 응답을 읽지 않으면 외부 프로그램의 출력 버퍼가 어느 순간 가득 차서 기록을 중단할 수 있습니다.
이를 피하려면 역할을 분리하세요. 한 스레드는 읽기, 다른 스레드는 쓰기를 담당하게 하면 상호작용이 병렬로 진행되어 서로를 막지 않습니다. 또한 작업이 끝난 뒤에는 스트림을 열어 둔 채 두지 말고 닫아 시스템 리소스가 불필요하게 점유되지 않도록 하세요.
GO TO FULL VERSION