1. 프로세스 종료 대기: waitFor()
Java에서 외부 프로세스를 실행하면 그 프로세스는 독립적으로 동작합니다. 무언가를 계산하고, 콘솔에 출력하고, 때로는 멈추기도 합니다(예: ping을 엄청나게 많은 패킷으로 실행한 경우). 하지만 우리는 때때로 그 프로세스가 언제, 어떻게 끝났는지(성공인지 실패인지) 알아야 하고, 문제가 생기면 종료시킬 수 있어야 합니다. 마치 현실에서 누군가에게 일을 맡겼다면, 잘 끝냈는지 확인하고, 끝나지 않으면 멈출 수 있어야 하는 것과 같습니다.
ProcessBuilder로 프로세스를 시작하면 Process 타입의 객체를 받습니다. 이 객체는 일종의 리모컨처럼 작동합니다. 이를 통해 프로세스 종료를 기다리고, 종료 코드를 얻고, 프로세스를 종료하며, 입출력 스트림을 다룰 수 있습니다.
프로세스 종료를 어떻게 기다리나요?
이를 위해 waitFor() 메서드를 사용합니다:
ProcessBuilder builder = new ProcessBuilder("ping", "google.com", "-c", "3");
Process process = builder.start();
System.out.println("프로세스가 끝나기를 기다리는 중...");
int exitCode = process.waitFor(); // 현재 스레드를 프로세스가 끝날 때까지 블록함
System.out.println("프로세스가 다음 코드로 종료되었습니다: " + exitCode);
무슨 일이 일어나나요?
- 프로그램이 외부 프로세스(ping)를 시작합니다.
- 그 후 waitFor()를 호출한 스레드는 외부 프로세스가 종료될 때까지 대기(블록)합니다.
- 외부 프로세스가 작업을 마치면 waitFor()는 종료 코드를 반환합니다(일반적으로 0은 성공, 0이 아니면 오류).
종료 코드는 어떻게 알 수 있나요?
waitFor()가 종료 코드를 반환합니다. 또는 exitValue()로 별도로 얻을 수도 있습니다:
int code = process.exitValue();
다만 주의하세요. 프로세스가 아직 끝나지 않았다면 exitValue() 호출은 IllegalThreadStateException을 던집니다. 보통은 먼저 waitFor()로 종료를 기다린 다음 코드를 확인합니다.
2. 프로세스 중단: destroy()와 destroyForcibly()
외부 프로세스가 너무 오래 실행되거나 멈출 때가 있습니다. 예를 들어 누군가 ping을 1000개의 패킷으로 실행했는데, 여러분은 5초 안에 끝내야 하는 상황일 수 있습니다. 이럴 때 필요한 것이 바로 “빨간 버튼”입니다.
온건한 종료: destroy()
destroy()는 프로세스에 종료 신호를 보냅니다(Unix에서는 SIGTERM, Windows에서는 CTRL-BREAK). 그러면 프로세스는 정상적으로 종료할 기회를 얻게 됩니다.
Process process = new ProcessBuilder("ping", "google.com").start();
Thread.sleep(2000); // 2초 동안 실행하게 둔다
process.destroy(); // 종료를 요청
강제 종료: destroyForcibly()
정상 종료 요청에 반응하지 않으면 destroyForcibly()를 사용할 수 있습니다. 말 그대로 전원을 뽑는 것과 같습니다.
process.destroyForcibly();
중요: destroy() 또는 destroyForcibly()를 호출한 뒤에는 반드시 waitFor()로 실제 종료를 기다리세요. 프로세스가 “종료”를 인지하는 데 시간이 걸릴 수 있습니다.
3. 타임아웃과 함께 프로세스 대기
Java 8+에는 타임아웃을 받는 메서드가 있습니다:
boolean finished = process.waitFor(5, java.util.concurrent.TimeUnit.SECONDS);
if (finished) {
System.out.println("프로세스가 제시간에 종료되었습니다!");
} else {
System.out.println("프로세스가 종료되지 않았습니다. 종료합니다...");
process.destroy();
}
- 프로세스가 5초 안에 끝나면 메서드는 true를 반환합니다.
- 그렇지 않으면 false를 반환하므로, 예를 들어 프로세스를 종료하는 등의 조치를 취할 수 있습니다.
예: 오래 걸리는 명령을 실행하고 종료를 시도해 봅시다:
ProcessBuilder builder = new ProcessBuilder("ping", "google.com", "-c", "10");
Process process = builder.start();
boolean finished = process.waitFor(2, java.util.concurrent.TimeUnit.SECONDS);
if (!finished) {
System.out.println("프로세스 실행 시간이 너무 깁니다. 종료합니다...");
process.destroy();
// 완전히 종료될 때까지 기다릴 수 있습니다
process.waitFor();
}
System.out.println("종료 코드: " + process.exitValue());
4. 실습: 타임아웃으로 명령을 실행하는 미니 유틸리티
타임아웃과 함께 외부 명령을 실행하고, 시간을 초과하면 프로세스를 종료하는 간단한 프로그램을 작성해 봅시다.
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;
public class ProcessTimeoutDemo {
public static void main(String[] args) throws IOException, InterruptedException {
Scanner scanner = new Scanner(System.in);
System.out.println("실행할 명령을 입력하세요(예: ping google.com):");
String commandLine = scanner.nextLine();
String[] command = commandLine.split(" ");
ProcessBuilder builder = new ProcessBuilder(command);
Process process = builder.start();
System.out.println("몇 초 동안 종료를 기다릴까요?");
int timeout = scanner.nextInt();
boolean finished = process.waitFor(timeout, TimeUnit.SECONDS);
if (finished) {
System.out.println("프로세스가 스스로 종료했습니다. 종료 코드: " + process.exitValue());
} else {
System.out.println("시간 초과! 프로세스를 종료합니다...");
process.destroy();
process.waitFor();
System.out.println("프로세스를 강제로 종료했습니다. 종료 코드: " + process.exitValue());
}
}
}
설명:
- 사용자가 실행할 명령을 입력합니다.
- 프로그램은 지정된 시간만큼 기다립니다.
- 프로세스가 끝나지 않으면 이를 종료합니다.
팁: Windows에서는 ping이 기본으로 4회만 수행되지만, Linux에서는 기본적으로 무제한입니다. Linux/Mac에서는 -c 5 옵션을 추가하세요(예: ping google.com -c 5).
5. 종료 코드 얻기: 그것은 무엇이며 왜 필요한가?
명령줄 세계에서는, 명령이 성공적으로 끝나면 0을 반환하고, 문제가 있으면 다른 어떤 코드든 반환하는 것이 관례입니다.
- Unix 계열 시스템: 0 = 성공, 1 이상 = 오류.
- Windows도 대체로 동일합니다.
이 코드를 이용해 Java 프로그램의 분기 로직을 만들 수 있습니다:
int exitCode = process.waitFor();
if (exitCode == 0) {
System.out.println("성공!");
} else {
System.out.println("오류! 코드: " + exitCode);
}
6. 멈춘(응답 없는) 프로세스 종료
프로세스가 입력을 기다리거나, 네트워크에 연결하지 못해 멈출 때가 있습니다. 방치하면 시스템에 남아 자원을 계속 소모할 수 있습니다.
이를 방지하려면 프로세스에 대해 인내심의 한계를 설정하세요. waitFor(timeout, unit)은 정해진 시간만 기다렸다가 종료하지 않으면 제어권을 돌려줍니다. 시간이 초과되면 과감히 프로세스를 종료하세요.
destroy()를 호출한 뒤에는 그냥 넘어가지 말고, 정말 종료되었는지 waitFor()로 확인하세요. 그래도 안 된다면 마지막 수단으로 destroyForcibly()가 있습니다. 말 그대로 강제 중단입니다.
이중 종료 예시
boolean finished = process.waitFor(5, TimeUnit.SECONDS);
if (!finished) {
process.destroy();
if (!process.waitFor(2, TimeUnit.SECONDS)) {
process.destroyForcibly();
}
}
Process를 다룰 때 기억할 점
프로세스가 끝났다고 바로 잊어버리지 마세요. 표준 출력과 오류 출력 모두를 확인해 보세요. 실패 메시지, 경고, 또는 성공 확인 같은 중요한 정보가 들어 있을 수 있습니다.
그 다음 관련된 모든 스트림 — InputStream, OutputStream, ErrorStream — 을 반드시 닫으세요. 마치 불을 끄듯이 자원을 아끼고 시스템을 깔끔하게 유지할 수 있습니다.
또한, 프로세스를 강제로 종료한 경우 출력이 완전히 기록되지 못할 수 있습니다. 이런 상황에서는 자연스러운 동작입니다.
7. 예시: 오래 실행되는 명령을 시작하고 종료하기
예를 들어, 많은 패킷 수로 ping 같은 오래 실행되는 프로세스를 시작하고 3초 후에 종료한다고 가정해 봅시다.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.concurrent.TimeUnit;
public class KillLongProcessDemo {
public static void main(String[] args) throws IOException, InterruptedException {
String[] command = {"ping", "google.com"};
// Linux/Mac용: {"ping", "google.com", "-c", "100"}
ProcessBuilder builder = new ProcessBuilder(command);
Process process = builder.start();
// 별도 스레드에서 프로세스 출력을 읽는다
Thread readerThread = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
// 무시
}
});
readerThread.start();
// 3초 동안 기다린다
if (!process.waitFor(3, TimeUnit.SECONDS)) {
System.out.println("프로세스 실행 시간이 너무 깁니다. 종료합니다...");
process.destroy();
process.waitFor();
}
readerThread.join(); // 출력 읽기가 끝날 때까지 기다린다
System.out.println("프로세스가 종료되었습니다. 종료 코드: " + process.exitValue());
}
}
8. 프로세스 관리에서 흔한 실수
오류 №1: 프로세스 종료를 기다리지 않는다. 프로세스를 시작하고 그냥 잊어버리면(waitFor()를 호출하지 않으면) Java 프로그램이 끝난 뒤에도 프로세스가 시스템에 남아 있을 수 있습니다. 입출력 스트림이 닫히지 않아 메모리 및 파일 디스크립터 누수로 이어질 수 있습니다.
오류 №2: 프로세스를 잘못 종료한다. destroy()를 호출해 놓고 waitFor()로 종료를 확인하지 않으면, 프로세스가 “좀비”로 남을 수 있습니다(특히 Unix 계열 시스템).
오류 №3: 프로세스가 끝나기 전에 종료 코드를 확인한다. 프로세스가 종료되기 전에 exitValue()를 호출하면 IllegalThreadStateException이 발생합니다.
오류 №4: 예외를 처리하지 않는다. 프로세스 관련 메서드들은 IOException과 InterruptedException을 던질 수 있습니다. 적절히 캐치하거나 메서드 시그니처에 선언하세요.
오류 №5: 프로세스 출력을 읽지 않는다. stdout과 stderr를 읽지 않으면, 출력 버퍼가 가득 차서 프로세스가 멈출 수 있습니다.
GO TO FULL VERSION