1. 프로세스 입출력 스트림
Java에서 외부 프로세스를 실행하면, 그 프로세스와 여러분의 프로그램 사이에 세 가지 통신 채널로 이루어진 작은 대화가 열립니다.
첫 번째 채널은 표준 출력입니다. 외부 프로세스가 “화면”에 보여 주고자 하는 모든 것을 Process.getInputStream()으로 읽을 수 있습니다. 두 번째 채널은 오류 스트림입니다. 프로세스가 경고나 오류 메시지를 출력하면 Process.getErrorStream()을 통해 알 수 있습니다. 마지막으로 세 번째는 표준 입력입니다. 이를 통해 프로세스가 여러분에게서 어떤 입력을 기다릴 때 데이터를 보낼 수 있습니다. Process.getOutputStream()을 사용합니다.
스트림 이름이 뒤바뀐 듯해 헷갈릴 수 있습니다. 프로세스 관점에서 InputStream은 여러분이 “읽는” 것(프로세스의 출력)이고, OutputStream은 여러분이 “쓰는” 곳(프로세스의 입력)입니다. 하지만 여러분의 Java 프로그램 관점에서 보면 논리적입니다. 여러분은 프로세스가 내보내는 것을 읽고, 프로세스에 전달하고 싶은 것을 씁니다.
프로세스 스트림 도식
+-------------------+ +---------------------+
| 여러분의 프로그램 | <-------> | 외부 프로세스 |
+-------------------+ +---------------------+
| |
| getOutputStream() --------> stdin
| getInputStream() <-------- stdout
| getErrorStream() <-------- stderr
2. 프로세스 출력 읽기 (stdout)
외부 프로세스가 화면에 출력하는 내용을 읽는 방법을 배워 봅시다. 명령의 결과, 프로그램 버전, 성공 메시지 등 무엇이든 가능합니다.
예제 1: java -version 명령의 출력 읽기
먼저 프로세스를 시작한 다음 그 출력을 읽습니다.
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
public class ProcessOutputExample {
public static void main(String[] args) throws IOException {
// 프로세스 생성: 명령은 OS에 따라 달라집니다!
ProcessBuilder pb = new ProcessBuilder("java", "-version");
Process process = pb.start();
// 오류 스트림 읽기 (java -version는 보통 여기에 씁니다!)
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("Process output: " + line);
}
}
}
}
주의! java -version 명령은 일반적인 stdout가 아니라 오류 스트림 stderr에 결과를 씁니다. 버그가 아니라 Java의 특성입니다. 따라서 우리는 process.getErrorStream()을 읽습니다. 대부분의 다른 명령(예: echo, ls, dir)의 경우에는 getInputStream()을 사용하세요.
예제 2: echo 명령의 출력 읽기
ProcessBuilder pb = new ProcessBuilder("echo", "Hello, Java!");
Process process = pb.start();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("Process output: " + line);
}
}
왜 BufferedReader가 필요할까요?
표준 입력 스트림은 바이트 스트림(InputStream)입니다. 이를 문자열로 읽기 위해서는 InputStreamReader(바이트를 문자로 변환) + BufferedReader(줄 단위로 편리하게 읽기) 조합을 사용합니다.
3. 프로세스 표준 입력(stdin)에 쓰기
때로는 외부 프로세스가 우리의 입력을 기다립니다. 예를 들어 사용자 이름을 입력받는 스크립트나 숫자를 기다리는 계산기 같은 경우입니다.
이를 위해 process.getOutputStream()을 사용하세요. 네, 맞습니다. 여러분은 프로세스의 입력 스트림에 “작성”합니다.
예제 3: 프로세스에 데이터 보내기
간단한 Python 스크립트가 한 줄을 읽어 그대로 출력한다고 가정해 보겠습니다:
echo_input.py:
# echo_input.py
line = input()
print("You said:", line)
이를 Java에서 실행하고 문자열을 보내 봅시다:
import java.io.*;
public class ProcessStdinExample {
public static void main(String[] args) throws IOException {
ProcessBuilder pb = new ProcessBuilder("python", "echo_input.py");
Process process = pb.start();
// 프로세스의 stdin에 쓰기
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(process.getOutputStream()))) {
writer.write("Java에서 인사합니다!\n");
writer.flush();
}
// 프로세스의 stdout 읽기
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("프로세스 응답: " + line);
}
}
}
}
flush()를 잊지 마세요. 그렇지 않으면 데이터가 버퍼에 “걸려” 프로세스로 전달되지 않을 수 있습니다.
4. 오류 처리: stderr 읽기
외부 프로세스는 결과만 주는 것이 아니라 오류도 출력할 수 있습니다. 오류 스트림을 읽지 않으면 중요한 정보를 놓칠 수 있고, 오류 버퍼가 가득 차서 (deadlock)이 발생할 수도 있습니다.
예제 4: 프로세스의 stderr 읽기
ProcessBuilder pb = new ProcessBuilder("java", "-version");
Process process = pb.start();
// 오류 읽기
try (BufferedReader errorReader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = errorReader.readLine()) != null) {
System.out.println("프로세스 오류: " + line);
}
}
왜 두 스트림을 모두 읽어야 할까요?
프로세스가 stdout나 stderr에 많은 데이터를 쓰는데 여러분이 해당 스트림을 읽지 않으면, OS 버퍼가 가득 차고 프로세스가 누군가 버퍼를 비워 줄 때까지 “멈출” 수 있습니다. 따라서 좋은 습관은 두 스트림을 병렬로(적어도 번갈아 가며) 읽는 것입니다.
5. 실습: 명령 실행, 출력 읽기, 오류 처리
지금까지 내용을 하나로 합쳐 외부 명령을 실행하고 표준 출력과 오류를 읽어 화면에 모두 출력하는 작은 유틸리티를 만들어 봅시다.
예제 5: 범용 명령 실행기
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class RunCommand {
public static void main(String[] args) throws Exception {
if (args.length == 0) {
System.out.println("실행할 명령을 지정하세요. 예: java RunCommand ls -l");
return;
}
var process = new ProcessBuilder(args).start();
// stdout와 stderr를 별도의 스레드로 읽기 시작합니다
var t1 = Thread.ofPlatform().start(() -> readStream(process.inputReader(), "[stdout]"));
var t2 = Thread.ofPlatform().start(() -> readStream(process.errorReader(), "[stderr]"));
int exitCode = process.waitFor(); // 프로세스 종료를 기다립니다
t1.join();
t2.join();
System.out.println("프로세스가 다음 코드로 종료되었습니다: " + exitCode);
}
private static void readStream(BufferedReader reader, String prefix) {
try (reader) {
reader.lines().forEach(line -> System.out.println(prefix + " " + line));
} catch (Exception e) {
System.err.println("읽기 오류 " + prefix + ": " + e.getMessage());
}
}
}
이런 접근 방식은 deadlock을 피하고 어떤 오류도 놓치지 않게 해 줍니다!
6. 인코딩 간단 정리
기본적으로 InputStreamReader는 시스템 인코딩을 사용합니다. 외부 프로세스가 다른 인코딩(예: UTF-8, 그런데 시스템은 Windows-1251)으로 출력하면 글자가 깨져 보일 수 있습니다. 인코딩을 명시적으로 지정하세요:
new InputStreamReader(process.getInputStream(), "UTF-8")
7. 예제: Python 스크립트 실행 및 데이터 전달
여러 줄의 입력을 받은 뒤 결과를 출력하는 Python 스크립트가 있다고 가정해 봅시다.
adder.py:
# adder.py
a = int(input())
b = int(input())
print(a + b)
Java에서 숫자를 보내고 응답을 읽을 수 있습니다:
import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
public class PythonAdder {
public static void main(String[] args) throws Exception {
var process = new ProcessBuilder("python", "adder.py").start();
// Python 스크립트의 stdin으로 두 개의 숫자 전송
try (var writer = process.outputWriter(StandardCharsets.UTF_8)) {
writer.println("10");
writer.println("25");
}
// inputReader로 stdout에서 결과 읽기
try (BufferedReader reader = process.inputReader(StandardCharsets.UTF_8)) {
reader.lines().forEach(line -> System.out.println("Python의 응답: " + line));
}
int exitCode = process.waitFor();
System.out.println("Python이 종료 코드로 종료되었습니다: " + exitCode);
}
}
8. 왜 deadlock이 발생할까? 어떻게 피할까
Deadlock은 프로세스와 여러분의 프로그램이 서로를 무한히 기다리는 상황입니다. 예를 들어 프로세스의 출력 버퍼가 가득 찼는데 여러분이 해당 스트림을 읽지 않으면 프로세스가 멈춥니다. 여러분은 프로세스가 끝나길 기다리고, 프로세스는 여러분이 무언가를 써 주길 기다리는 경우도 역시 deadlock입니다.
예방법:
- 항상 두 스트림(stdout과 stderr)을 모두 읽으세요.
- 오래 걸리거나 출력이 많은 프로세스는 읽기를 별도 스레드에서 수행하세요.
- 한 스레드에서 읽기와 쓰기를 동시에 막지 마세요.
9. 프로세스 입출력 스트림 다루기에서 흔한 실수
오류 1: 하나의 스트림(stdout 또는 stderr)만 읽는다.
stdout만 읽고, 프로세스가 stderr에 오류를 쓰면 오류 버퍼가 가득 차서 프로세스가 멈출 수 있습니다. 해결: 두 스트림을 모두 읽으세요(가능하면 별도 스레드에서).
오류 2: 잘못된 인코딩.
필요한 인코딩을 지정하지 않으면 출력이 깨져 보일 수 있습니다. 외부 프로세스가 어떤 인코딩을 사용하는지 항상 확인하세요(예: "UTF-8"을 명시하거나 StandardCharsets.UTF_8을 사용).
오류 3: 스트림을 닫지 않음.
열린 스트림은 자원 누수로 이어질 수 있습니다. try-with-resources를 사용하거나 모든 스트림을 명시적으로 닫으세요.
오류 4: 프로세스 stdin에 쓰고 flush()를 호출하지 않음.
flush() 호출을 잊으면 데이터가 프로세스로 전달되지 않아, 프로세스가 입력을 영원히 기다릴 수 있습니다.
오류 5: 플랫폼 차이를 무시.
명령과 구문은 Windows와 Linux/Mac에서 다를 수 있습니다. System.getProperty("os.name")으로 OS를 확인하고 그에 맞는 명령을 선택하세요.
오류 6: 예외를 처리하지 않음.
프로세스 작업에서는 IOException이 자주 발생합니다. 프로그램이 비정상 종료되지 않도록 항상 예외를 처리하세요.
GO TO FULL VERSION