Runtime과 ProcessBuilder를 활용한 명령어 실행
Java 애플리케이션에서 운영체제의 명령어를 실행해야 할 때 Runtime과 ProcessBuilder 두 가지 방식을 활용할 수 있습니다. 각각의 특징을 이해하고 적절히 선택하는 것이 중요합니다.
Runtime.exec()의 기본 활용
Runtime 클래스의 exec() 메서드는 간단한 명령어 실행에 적합합니다. 내부적으로 ProcessBuilder를 생성하므로 복잡한 설정이 필요 없을 때 유리합니다.
import java.io.*;
public class RuntimeExecutor {
public static void main(String[] args) {
executeNetworkCommand();
}
static void executeNetworkCommand() {
try {
String networkCmd = "netstat -an";
Process proc = Runtime.getRuntime().exec(networkCmd);
try (BufferedReader stdOut = new BufferedReader(
new InputStreamReader(proc.getInputStream(), "GBK"))) {
String outputLine;
while ((outputLine = stdOut.readLine()) != null) {
System.out.println(outputLine);
}
}
int completionStatus = proc.waitFor();
System.out.println("종료 상태: " + completionStatus);
} catch (IOException | InterruptedException ex) {
Thread.currentThread().interrupt();
ex.printStackTrace();
}
}
}
ProcessBuilder의 고급 설정
ProcessBuilder는 작업 디렉터리 설정, 환경변수 조작, 표준 에러 스트림 합치기 등 세밀한 제어가 가능합니다.
import java.io.*;
import java.nio.file.Paths;
public class AdvancedProcessBuilder {
public static void main(String[] args) {
configureAndLaunch();
}
static void configureAndLaunch() {
ProcessBuilder pb = new ProcessBuilder(
"powershell.exe", "-Command", "Get-Process | Select-Object -First 5"
);
// 작업 디렉터리 지정
pb.directory(Paths.get("C:\\Temp").toFile());
// 환경변수 추가
pb.environment().put("CUSTOM_VAR", "process_value");
// 표준 에러를 표준 출력으로 합침
pb.redirectErrorStream(true);
try {
Process proc = pb.start();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(proc.getInputStream(), "UTF-8"))) {
reader.lines().forEach(System.out::println);
}
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}
비동기 출력 처리 패턴
외부 프로세스의 출력 스트림을 읽지 않으면 버퍼가 가득 차 데드락이 발생할 수 있습니다. 별도 스레드에서 즉시 소비하는 패턴을 적용해야 합니다.
import java.io.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
public class AsyncStreamHandler {
public static void main(String[] args) throws Exception {
runWithAsyncCapture("cmd", "/c", "ping", "-n", "4", "localhost");
}
static void runWithAsyncCapture(String... command) throws Exception {
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true);
Process proc = pb.start();
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<String> outputFuture = executor.submit(() -> {
try (BufferedReader br = new BufferedReader(
new InputStreamReader(proc.getInputStream(), "GBK"))) {
return br.lines().collect(Collectors.joining("\n"));
}
});
// 타임아웃과 함께 완료 대기
boolean finished = proc.waitFor(10, java.util.concurrent.TimeUnit.SECONDS);
String result = outputFuture.get(5, java.util.concurrent.TimeUnit.SECONDS);
System.out.println("실행 완료: " + finished);
System.out.println("=== 출력 내역 ===");
System.out.println(result);
executor.shutdownNow();
}
}
Shell 스크립트 실행 및 파라미터 전달
Linux 환경에서 스크립트를 실행할 때는 인터프리터를 명시적으로 지정하고, 파라미터는 분리된 형태로 전달하는 것이 안전합니다.
import java.io.*;
public class ShellScriptInvoker {
public static void main(String[] args) {
deployWithParameters("192.168.1.100", "production");
}
static void deployWithParameters(String hostIp, String envName) {
// 스크립트 경로와 인자 분리
String[] shellCmd = {
"bash", "-c",
"/opt/scripts/deploy.sh " + hostIp + " " + envName
};
ProcessBuilder pb = new ProcessBuilder("bash", "-c",
"echo 'Starting deployment...'; " +
"echo 'Target: " + hostIp + "'; " +
"echo 'Environment: " + envName + "'; " +
"sleep 2; " +
"echo 'Deployment finished'"
);
try {
Process proc = pb.start();
// 실시간 로그 출력
try (BufferedReader logReader = new BufferedReader(
new InputStreamReader(proc.getInputStream()))) {
logReader.lines().forEach(line ->
System.out.println("[LOG] " + line));
}
int status = proc.waitFor();
System.out.println("최종 상태: " + status);
} catch (IOException | InterruptedException ex) {
Thread.currentThread().interrupt();
throw new RuntimeException("배포 실패", ex);
}
}
}
프로세스 강제 종료 및 자원 해제
외부 프로세스의 생명주기를 적절히 관리하지 않으면 좀비 프로세스가 남을 수 있습니다. 계층적 종료 전을 구현해야 합니다.
import java.io.*;
import java.util.concurrent.TimeUnit;
public class ProcessLifecycleManager {
public static void main(String[] args) {
managedExecution("ffmpeg", "-f", "dshow", "-i", "video=screen-capture-recorder",
"-t", "30", "output.mp4");
}
static void managedExecution(String... command) {
Process proc = null;
try {
proc = new ProcessBuilder(command).start();
final Process capturedProc = proc;
// 종료 훅 등록
Thread shutdownHook = new Thread(() -> {
if (capturedProc.isAlive()) {
System.err.println("JVM 종료 시 프로세스 정리");
capturedProc.destroyForcibly();
}
});
Runtime.getRuntime().addShutdownHook(shutdownHook);
// 10초 후 강제 종료 시뮬레이션
Thread terminator = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(10);
System.out.println("타임아으로 인한 프로세스 종료 요청");
// 우선 정상 종료 시도
capturedProc.destroy();
if (!capturedProc.waitFor(5, TimeUnit.SECONDS)) {
System.out.println("강제 종료 실행");
capturedProc.destroyForcibly();
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
});
terminator.start();
// 출력 소비
try (BufferedReader stdReader = new BufferedReader(
new InputStreamReader(proc.getInputStream()));
BufferedReader errReader = new BufferedReader(
new InputStreamReader(proc.getErrorStream()))) {
// 에러 스트림 병렬 소비
Thread errConsumer = new Thread(() -> {
errReader.lines().forEach(line ->
System.err.println("[ERR] " + line));
});
errConsumer.start();
stdReader.lines().forEach(line ->
System.out.println("[OUT] " + line));
errConsumer.join();
}
} catch (IOException | InterruptedException ex) {
Thread.currentThread().interrupt();
throw new RuntimeException("프로세스 관리 오류", ex);
} finally {
if (proc != null && proc.isAlive()) {
proc.destroyForcibly();
}
}
}
}
크로스 플랫폼 유틸리티 클래스
운영체제별 차이를 추상화한 유틸리티를 구현하면 코드의 재사용성이 높아집니다.
import java.io.*;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
public class CrossPlatformExecutor {
private static final boolean IS_WINDOWS =
System.getProperty("os.name").toLowerCase().contains("win");
private static final Charset PLATFORM_CHARSET =
IS_WINDOWS ? Charset.forName("GBK") : Charset.forName("UTF-8");
public static ExecutionResult runWithTimeout(long timeoutSeconds, String... command) {
List<String> wrappedCommand = wrapForPlatform(command);
ProcessBuilder pb = new ProcessBuilder(wrappedCommand);
pb.redirectErrorStream(true);
Process proc = null;
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
proc = pb.start();
final Process finalProc = proc;
Future<String> outputTask = executor.submit(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(finalProc.getInputStream(), PLATFORM_CHARSET))) {
return reader.lines().collect(Collectors.joining("\n"));
}
});
String output = outputTask.get(timeoutSeconds, TimeUnit.SECONDS);
int exitCode = proc.waitFor();
return new ExecutionResult(exitCode, output, null);
} catch (TimeoutException ex) {
if (proc != null) proc.destroyForcibly();
return new ExecutionResult(-1, null, "Execution timeout after " + timeoutSeconds + "s");
} catch (Exception ex) {
return new ExecutionResult(-1, null, ex.getMessage());
} finally {
executor.shutdownNow();
}
}
private static List<String> wrapForPlatform(String... command) {
List<String> result = new ArrayList<>();
if (IS_WINDOWS) {
result.add("cmd");
result.add("/c");
} else {
result.add("sh");
result.add("-c");
// Linux에서는 단일 문자열로 결합
String combined = Arrays.stream(command)
.collect(Collectors.joining(" "));
result.add(combined);
return result;
}
Collections.addAll(result, command);
return result;
}
public record ExecutionResult(int exitCode, String output, String error) {
public boolean isSuccess() {
return exitCode == 0 && error == null;
}
}
// 사용 예시
public static void main(String[] args) {
ExecutionResult result = runWithTimeout(5, "echo", "Hello from",
IS_WINDOWS ? "Windows" : "Linux");
System.out.println("성공 여부: " + result.isSuccess());
System.out.println("출력:\n" + result.output());
}
}