Java에서 외부 프로세스 실행 및 관리

Runtime과 ProcessBuilder를 활용한 명령어 실행

Java 애플리케이션에서 운영체제의 명령어를 실행해야 할 때 RuntimeProcessBuilder 두 가지 방식을 활용할 수 있습니다. 각각의 특징을 이해하고 적절히 선택하는 것이 중요합니다.

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());
    }
}

태그: java ProcessBuilder Runtime.exec shell script Process Management

6월 26일 18:52에 게시됨