1. 소개
대화형 프로세스란 혼자서 무언가를 실행하기만 하는 것이 아니라, 여러분이 그와 “대화”하길 기다리는 프로그램을 말합니다. 사용자로부터 입력을 받아 그에 응답을 돌려줍니다.
고전적인 예로는 Python, bash, PowerShell 같은 인터프리터가 있습니다. 이들은 명령을 차분히 기다렸다가 실행하고 결과를 보여줍니다. 또 다른 예로 계산기, 콘솔 데이터베이스(psql, sqlite3) 같은 대화형 유틸리티나 vi, nano 같은 텍스트 에디터도 있습니다. 때로는 평범한 스크립트도 실행 중에 매개변수나 응답을 요구하면 대화형이 됩니다.
이러한 프로세스를 Java에서 실행하는 일은 쉬운 산책이 아닙니다. 단순히 “명령을 던지고 결과를 받는” 것만으로는 부족합니다. 실시간 대화처럼 제때 데이터를 보내고 응답을 읽어야 합니다.
메신저에서 친구와 대화한다고 생각해 보세요. 메시지를 보내고, 답을 기다린 다음, 다시 씁니다. 올바르게 스트림을 설정하면 Java와 외부 프로세스의 상호작용도 정확히 이런 모습이 됩니다.
양방향 교환 구성
각 프로세스에는 세 가지 “통신 채널”이 있습니다: 입력(stdin), 출력(stdout), 그리고 오류(stderr). 첫 번째는 프로세스에 무언가를 전달하는 데, 두 번째는 결과를 받는 데, 세 번째는 오류 메시지에 사용됩니다.
핵심은 프로세스가 “기다림”에 멈춰 서지 않게 하는 것입니다. 일반 출력만 읽고 오류 스트림을 무시하면, 프로세스는 마치 여러분이 불평을 들어주길 기다리는 듯 멈출 수 있습니다. 반대로, 입력만 보내고 응답을 읽지 않으면 프로세스는 쌓인 데이터를 어디에 보낼지 몰라 멈출 수 있습니다.
따라서 대화형 프로세스와 작업할 때는 양방향 교환을 유지하는 것이 중요합니다. 정상적인 대화처럼 서로 듣고 답해야지, 공허한 독백이 되어서는 안 됩니다.
대화형 교환 다이어그램
+---------------------+
| Java program |
+---------------------+
| ^
v |
stdin stdout/stderr
| ^
+---------------------+
| External process |
+---------------------+
- Java는 프로세스의 입력 스트림(stdin)에 씁니다.
- Java는 프로세스의 stdout과 stderr에서 읽습니다.
- 이 모든 것은 동시에 일어날 수 있습니다!
2. 실습: 외부 프로세스와의 대화형 교환
실용적인 예를 살펴봅시다. 외부 프로세스(예: Python 인터프리터나 간단한 echo 스크립트)를 실행한 뒤, 문자열을 보내고 응답을 읽어 보겠습니다.
예제 1: 입력을 기다리는 Python 스크립트 실행
먼저 간단한 Python 스크립트를 만듭니다(이름은 echo_bot.py):
# echo_bot.py
while True:
try:
line = input()
if line == "exit":
print("Bye!")
break
print("Echo:", line)
except EOFError:
break
이 스크립트는 입력을 기다리고, 각 줄마다 "Echo: ..."로 응답합니다. "exit"를 입력하면 종료합니다.
Java에서 이 스크립트를 실행하고 “대화”하려면?
1. 프로세스 시작
ProcessBuilder builder = new ProcessBuilder("python", "echo_bot.py");
Process process = builder.start();
2. 통신 스트림 준비
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
3. 대화 구현
// 프로세스에 문자열 전송
writer.write("Hello, process!\n");
writer.flush();
// 응답 읽기
String response = reader.readLine();
System.out.println("프로세스 응답: " + response);
4. 필요할 때까지 반복
사용자가 콘솔에 입력하면 Java가 이를 프로세스에 보내고, 응답을 출력하는 간단한 루프로 만들어 봅시다.
전체 예제: 외부 프로세스와의 Java 채팅
import java.io.*;
public class InteractiveProcessDemo {
public static void main(String[] args) throws IOException {
ProcessBuilder builder = new ProcessBuilder("python", "echo_bot.py");
Process process = builder.start();
// 프로세스와 통신하기 위한 스트림
BufferedWriter toProcess = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
BufferedReader fromProcess = new BufferedReader(new InputStreamReader(process.getInputStream()));
BufferedReader userInput = new BufferedReader(new InputStreamReader(System.in));
System.out.println("프로세스에 보낼 문자열을 입력하세요 (종료하려면 exit):");
while (true) {
// 사용자로부터 한 줄 읽기
String line = userInput.readLine();
if (line == null) break;
// 프로세스에 전송
toProcess.write(line + "\n");
toProcess.flush();
// 프로세스의 응답 읽기
String response = fromProcess.readLine();
System.out.println("프로세스 응답: " + response);
if ("exit".equals(line)) break;
}
// 프로세스 종료
process.destroy();
}
}
코드 설명:
- 세 가지 스트림을 사용합니다: 사용자 입력을 읽는 System.in, 프로세스에 쓰는 process.getOutputStream(), 응답을 읽는 process.getInputStream().
- 각 줄을 프로세스에 보낸 후 즉시 readLine()으로 응답을 읽습니다.
- 사용자가 "exit"를 입력하면 루프를 종료하고 프로세스를 파기(process.destroy())합니다.
3. 왜 읽기와 쓰기를 동시에 해야 할까?
현실 세계의 작업에서는 프로세스가 많은 데이터를 출력하거나, 연달아 입력을 요구할 수 있습니다. 프로세스의 출력을 제때 읽지 않으면 내부 버퍼가 가득 차서, 누군가가 그 메시지를 읽어 줄 때까지 프로세스가 “멈출” 수 있습니다. 마찬가지로 입력을 보내지 않으면 프로세스가 기다리다가 “정지”될 수 있습니다.
Deadlock: 모든 것이 멈출 때
Deadlock은 두 주체(예: 스레드와 프로세스)가 서로를 기다리느라 아무도 진행하지 못하는 상황입니다.
deadlock 예:
- Java는 프로세스가 stdout에 뭔가 쓰길 기다립니다.
- 프로세스는 Java가 stdin에 뭔가 써 주길 기다립니다.
- 둘 다 기다리기만 하고 — 아무도 일하지 않습니다.
해결: 읽기/쓰기를 위한 별도 스레드
deadlock을 피하려면 보통 별도의 스레드(Thread)를 사용해 프로세스의 stdout과 stderr를 동시에 읽습니다. 예를 들어 하나의 스레드는 stdout을, 다른 하나는 stderr를 읽고, 메인 스레드는 stdin에 쓰도록 합니다.
stderr를 위한 별도 스레드 최소 예제
// 별도 스레드에서 stderr 읽기
Thread errorThread = new Thread(() -> {
try (BufferedReader errorReader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = errorReader.readLine()) != null) {
System.err.println("stderr: " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
});
errorThread.start();
4. 실습: 계산기와의 대화형 교환
항상 Python이 있는 것은 아닙니다. 거의 어디에나 있는 표준 명령을 실행해 보겠습니다 — 예를 들어, 커맨드라인 계산기입니다. Linux/Mac에서는 bc(basic calculator), Windows에서는 cmd나 powershell이 될 수 있습니다.
예제: bc와의 대화형 상호작용 (Linux/Mac)
ProcessBuilder builder = new ProcessBuilder("bc");
Process process = builder.start();
BufferedWriter toProcess = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
BufferedReader fromProcess = new BufferedReader(new InputStreamReader(process.getInputStream()));
toProcess.write("2 + 2\n");
toProcess.flush();
String result = fromProcess.readLine();
System.out.println("계산기 응답: " + result);
toProcess.write("quit\n");
toProcess.flush();
process.destroy();
Windows에서: cmd를 실행해 명령을 전달할 수도 있지만, 구문이 달라 더 복잡합니다. 보통은 두 플랫폼 모두에서 사용할 수 있는 유틸리티를 데모에 쓰거나, 직접 미니 스크립트를 작성합니다.
5. 문제와 함정
스트림 버퍼링
가끔 프로세스는 즉시 데이터를 출력하지 않고 내부 버퍼에 모아 두다가, 버퍼가 가득 차거나 개행 문자("\n")를 받으면 “꺼내 주기도” 합니다. 이 때문에 프로세스가 이미 “응답을 준비”했는데도 여러분은 한동안 보지 못할 수 있습니다.
팁:
- 프로세스의 stdin에 쓸 때는 항상 줄을 "\n"으로 끝내세요.
- 직접 스크립트를 작성한다면, 각 출력 후 flush()를 호출하세요.
잘못된 스트림 처리로 인한 deadlock
stderr를 읽지 않는데 프로세스가 그곳에 많은 데이터를 쓰면, 누군가가 오류를 읽어 줄 때까지 프로세스가 멈출 수 있습니다.
팁:
항상 프로세스의 stdout과 stderr를 읽으세요. 가능하면 별도 스레드에서 읽는 것이 좋습니다.
프로세스는 종료됐는데 여전히 쓰는 경우
프로세스가 이미 종료됐는데도 입력에 계속 쓰면 IOException이 발생합니다: "Stream closed".
7. Best practices: 대화형 상호작용
ExecutorService로 병렬 읽기
출력과 오류를 읽기 위한 스레드를 직접 만드는 대신, ExecutorService(예: 두 개 스레드의 풀)를 사용하는 것이 편리합니다:
import java.util.concurrent.*;
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> {
try (BufferedReader out = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = out.readLine()) != null) {
System.out.println("stdout: " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
});
executor.submit(() -> {
try (BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = err.readLine()) != null) {
System.err.println("stderr: " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
});
스트림을 올바르게 닫기
프로세스 작업을 마친 후에는 반드시 모든 스트림(stdin, stdout, stderr)을 닫아 리소스 누수를 방지하세요.
toProcess.close();
fromProcess.close();
process.destroy();
executor.shutdown();
8. 최종 예제: 외부 프로세스와의 대화형 교환
지금까지를 하나로 모으겠습니다. 아래 예제는 사용자와 외부 프로세스(예: Python 스크립트 또는 계산기) 사이의 대화형 “채팅”처럼 동작합니다.
import java.io.*;
import java.util.concurrent.*;
public class InteractiveProcessUniversal {
public static void main(String[] args) throws IOException {
// 여러분의 명령으로 바꾸세요 — 예: "python echo_bot.py" 또는 "bc"
ProcessBuilder builder = new ProcessBuilder("python", "echo_bot.py");
Process process = builder.start();
ExecutorService executor = Executors.newFixedThreadPool(2);
// 프로세스의 stdout 읽기
executor.submit(() -> {
try (BufferedReader out = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = out.readLine()) != null) {
System.out.println("[process] " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
});
// 프로세스의 stderr 읽기
executor.submit(() -> {
try (BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = err.readLine()) != null) {
System.err.println("[process-err] " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
});
// 메인 스레드: 프로세스의 stdin에 쓰기
try (BufferedWriter toProcess = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
BufferedReader userInput = new BufferedReader(new InputStreamReader(System.in))) {
System.out.println("프로세스에 보낼 문자열을 입력하세요 (종료하려면 exit):");
String line;
while ((line = userInput.readLine()) != null) {
toProcess.write(line + "\n");
toProcess.flush();
if ("exit".equals(line)) break;
}
}
process.destroy();
executor.shutdown();
}
}
9. 대화형 프로세스 작업에서 흔한 실수
오류 №1: 프로세스의 stderr를 읽지 않음. 프로세스가 오류를 많이 쓰는데 stderr를 읽지 않으면, 프로세스가 멈출 수 있습니다. 오류가 없을 거라 확신하더라도 — stderr를 읽으세요!
오류 №2: 스트림을 닫지 않음. 프로세스의 스트림을 닫지 않으면 리소스 누수가 발생하거나, 때로는 프로세스 종료가 막힐 수 있습니다.
오류 №3: 플랫폼별 명령 차이. 명령과 구문은 Windows와 Unix 계열 시스템 간에 다릅니다. OS를 확인하고 그에 맞는 명령을 선택하세요.
오류 №4: 프로그램이 ‘멈춤’ — flush()를 호출하지 않음. flush() 호출을 잊으면 데이터가 버퍼에 “갇혀” 프로세스에 전달되지 않을 수 있습니다.
오류 №5: 외부 프로세스의 출력 버퍼링. 어떤 프로세스는 충분한 글자가 모이거나 "\n"을 받을 때까지 출력을 내지 않기도 합니다. 직접 스크립트를 작성한다면 flush()를 사용하세요.
오류 №6: 입출력을 동시에 기다리다 발생하는 deadlock. 메인 스레드는 프로세스의 출력을 기다리고, 프로세스는 입력을 기다리면 둘 다 “멈출” 수 있습니다. 읽기/쓰기는 별도 스레드로 분리하세요.
오류 №7: 종료된 프로세스에 쓰면 ‘Stream closed’ 예외. 프로세스의 stdin에 쓰기 전에 프로세스가 살아 있는지 확인하세요. 오류가 나면 IOException: "Stream closed"를 받게 됩니다.
GO TO FULL VERSION