0%

java에서 shell command 호출하기

들어가기

회사에서 springboot 백엔드 서버에서 파이썬 코드를 shell command로 호출하고 뒤 그 응답을 stdout으로 받아서 db에 저장해야 기능을 만들어야 하는 일이 생겼다.

욕심 같아선 파이썬 스크립트를 마이크로 서비스로 만드는 것이 가장 좋은 방법 처럼 보이지만, 매일 시간에 쫒기는 입장이다보니 그냥 java에서 shell command 로 파이썬을 호출하는 방향으로 진행하기로 했다.

이 글에서는 간단하게 java에서 shell command를 호출하고 호출결과(stdout)을 java에서 받아서 출력하는 예제를 정리한 글이다.

실행환경

  • 인텔리J
  • jdk 1.8
  • windows10

java 코드로 쉘 커멘드를 호출하는 방법을 알아보자.

java로 shell cmd를 호출하는 방법으로는 크게 두가지 방법이 있다고 한다.

  • Runtime 를 사용하는 방법
  • ProcessBuilder 를 사용하는 방법(이걸 더 추천하는듯 하다.)

하지만 위 두가지 방법을 구현하기 전에 java에서 쉘 커멘드를 호출하기 전에 고려해야 할 점이 있다.

JVM 실행 환경(OS) 구별하기

java에서 shell cmd를 호출할 때 os 마다 다르게 호출해 줘야 한다.

만약 windonws라면 cmd.exe 쉘을 호출해야 한다.

그리고 그외의 OS의 경우에는 sh라는 표준 쉘을 실행해야 해야 한다.

다음 코드로 JVM 실행환경 OS를 구별 할수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.demo;

/**
* java에서 쉘 커멘드 호출 예제
*/
public class TestShellCmd {
public static void main(String[] args) {
boolean isWindows = System.getProperty("os.name")
.toLowerCase().startsWith("windows");

System.out.println("실행환경이 윈도우인가? " + isWindows);
}
}

아래 처럼 간단한 예제 코드를 만들었다.

java%20shell%20command%20cd59c192a52849578dde8f6c36ee8eb9/Untitled.png

windows 노트북에서 돌려보니 역시 windows라고 나온다.

java%20shell%20command%20cd59c192a52849578dde8f6c36ee8eb9/Untitled%201.png

또 한가지 고려해야 할 점이 있다.

앞서 말했듯이, 쉘 커멘드를 단순히 콜하고 끝나는 것이 아니다.

쉘 커멘드로 그 호출에 대한 응답을 stdout으로 받아와야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static class StreamGobbler implements Runnable {
private InputStream inputStream;
private Consumer<String> consumer;

public StreamGobbler(InputStream inputStream, Consumer<String> consumer) {
this.inputStream = inputStream;
this.consumer = consumer;
}

@Override
public void run() {
new BufferedReader(new InputStreamReader(inputStream)).lines()
.forEach(consumer);
}
}

위 클래스는 별도의 스레드를 생성하는 클래스이다.

생성자로 InputStream과 Consumer를 주입받는다.

InputStream은 java에서 쉘 커멘드를 호출한 process로 부터 받아올 것이다.

shell command를 호출하는 process와 연결된 이 InputStream을 사용해서 shell command의 stdout을 받아 올 수 있다.

쓰레드의 실제 실행 로직인 run메소드를 보면 inputstream을 사용해서 한줄씩 BufferedReader로 읽어와서 consumer에 전달하는 것을 볼 수 있다.

예제코드에 아래처럼 추가했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.demo;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.function.Consumer;

/**
* java에서 쉘 커멘드 호출 예제
*/
public class TestShellCmd {

private static class StreamGobbler implements Runnable {
private InputStream inputStream;
private Consumer<String> consumer;

public StreamGobbler(InputStream inputStream, Consumer<String> consumer) {
this.inputStream = inputStream;
this.consumer = consumer;
}

@Override
public void run() {
new BufferedReader(new InputStreamReader(inputStream)).lines()
.forEach(consumer);
}
}

public static void main(String[] args) {
boolean isWindows = System.getProperty("os.name")
.toLowerCase().startsWith("windows");

System.out.println("실행환경이 윈도우인가? " + isWindows);
}
}

이제 Runtime 클래스를 사용해서 java에서 Shell command 를 호출해보자.

Runtime 클래스를 사용해서 shell command 호출하기.

일단 Runtime 클래스가 뭔지 간단히 알아보자.

Java Runtime 클래스는 Java 런타임 환경과 상호 작용하는 데 사용된다. Java Runtime 클래스는 프로세스를 실행하고, GC를 호출하고, 총 메모리 및 여유 메모리를 얻는 방법을 제공한다. java.lang의 인스턴스는 하나뿐입니다.하나의 Java 응용프로그램에 대해 런타임 클래스를 사용할 수 있다.
Runtime.getRuntime() 메서드는 Runtime 클래스의 싱글톤 인스턴스를 반환한다.

뭔소린지 말 모를 땐 예제를 보자.

아래 예제는 운영체제에 따라서 현재 사용자의 디렉토리를 조회하는 쉘 커멘드를 호출하는 로직이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 간단하게 Runtime 클래스를 사용해서 현재 사용자 디렉토리 정보를 가져와 출력하는 쉘 커멘드를 호출하는 예제
*/
String homeDirectory = System.getProperty("user.home");
Process process;
if (isWindows) {
process = Runtime.getRuntime()
.exec(String.format("cmd.exe /c dir %s", homeDirectory));
} else {
process = Runtime.getRuntime()
.exec(String.format("sh -c ls %s", homeDirectory));
}

/**
* 쉘 프로세스를 실행하고 그 프로세스에 inputstream을 연결하고 컨슈머로 System.out::println를 넘긴다.
*/
String psRetMsg = "";
StreamGobbler streamGobbler =
new StreamGobbler(process.getInputStream(), (item)->{
System.out.println(item);
});
Executors.newSingleThreadExecutor().submit(streamGobbler);
int exitCode = process.waitFor();
assert exitCode == 0;

아래에서 빨간 줄 부분이 Runtime에게 실행을 부탁하는 쉘 커멘더 명령어이다.(String)

java%20shell%20command%20cd59c192a52849578dde8f6c36ee8eb9/Untitled%202.png

위 코드를 호출히면 Runtime 클래스는 자신이 동작하는 JVM외부 환경에 해당 빨간 명령어 실행을 요청하고, 그 명령어를 실행하는 process 인스턴스를 리턴 하게 된다.

이 프로세스에서 InputStream을 가져와서 앞서 생성한 StreamGobbler를 생성하면, StreamGobbler를 통해서 해당 process의 stdout을 가져올 수 있다.

아래는 최종 결과물이다.(한글이 깨져서 InputStreamReader의 파라미터가 추가 되었다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.example.demo;

import java.io.*;
import java.util.concurrent.Executors;
import java.util.function.Consumer;

/**
* java에서 쉘 커멘드 호출 예제
*/
public class TestShellCmd {

private static class StreamGobbler implements Runnable {
private InputStream inputStream;
private Consumer<String> consumer;

public StreamGobbler(InputStream inputStream, Consumer<String> consumer) {
this.inputStream = inputStream;
this.consumer = consumer;
}

@Override
public void run() {
try {
new BufferedReader(new InputStreamReader(inputStream, "euc-kr")).lines()
.forEach(consumer);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) throws IOException, InterruptedException {
boolean isWindows = System.getProperty("os.name")
.toLowerCase().startsWith("windows");

System.out.println("실행환경이 윈도우인가? " + isWindows);

/**
* 간단하게 Runtime 클래스를 사용해서 현재 사용자 디렉토리 정보를 가져와 출력하는 쉘 커멘드를 호출하는 예제
*/
String homeDirectory = System.getProperty("user.home");
Process process;
if (isWindows) {
process = Runtime.getRuntime()
.exec(String.format("cmd.exe /c dir %s", homeDirectory));
} else {
process = Runtime.getRuntime()
.exec(String.format("sh -c ls %s", homeDirectory));
}

/**
* 쉘 프로세스를 실행하고 그 프로세스에 inputstream을 연결하고 컨슈머로 System.out::println를 넘긴다.
*/
String psRetMsg = "";
StreamGobbler streamGobbler =
new StreamGobbler(process.getInputStream(), (item)->{
System.out.println(item);
});
Executors.newSingleThreadExecutor().submit(streamGobbler);
int exitCode = process.waitFor();
assert exitCode == 0;
}
}

실행결과

java%20shell%20command%20cd59c192a52849578dde8f6c36ee8eb9/Untitled%203.png

위 처럼 쉘커멘더가 동작하고 그 결과를 java에서 가져와서 출력하는 것을 확인 할 수 있다.

ProcessBuilder를 사용해서 shell command 호출하기

ProcessBuilder는 RunTime 보다 선호된다고 한다.

그 이유는 다음과 같다.

  • builder.directory()를 사용해서 shell command가 동작하는 working directory를 바꿀 수 있다.
  • builder.environment()를 사용해서 key-value map 형태의 사용자 정의 변수를 설정 할 수 있다.
  • inputstream, outputstream을 커스텀으로 교체할 수 있다.
  • inherit both of them to the streams of the current JVM process using builder.inheritIO() (이건 무슨말인지 모르겠다… 하위 프로세스의 IO를 현재 동작하는 JVM process와 동일하게 지정한다는 것 같은데…. 무슨 소리지?)

아래는 RunTime대신 ProcessBuilder를 사용한 예제이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.example.demo;

import java.io.*;
import java.util.concurrent.Executors;
import java.util.function.Consumer;

/**
* java에서 쉘 커멘드 호출 예제
*/
public class TestShellCmd {

private static class StreamGobbler implements Runnable {
private InputStream inputStream;
private Consumer<String> consumer;

public StreamGobbler(InputStream inputStream, Consumer<String> consumer) {
this.inputStream = inputStream;
this.consumer = consumer;
}

@Override
public void run() {
try {
new BufferedReader(new InputStreamReader(inputStream, "euc-kr")).lines()
.forEach(consumer);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) throws IOException, InterruptedException {
boolean isWindows = System.getProperty("os.name")
.toLowerCase().startsWith("windows");

System.out.println("실행환경이 윈도우인가? " + isWindows);

ProcessBuilder builder = new ProcessBuilder();
if (isWindows) {
builder.command("cmd.exe", "/c", "dir");
} else {
builder.command("sh", "-c", "ls");
}
builder.directory(new File(System.getProperty("user.home")));
Process process = builder.start();
StreamGobbler streamGobbler =
new StreamGobbler(process.getInputStream(), System.out::println);
Executors.newSingleThreadExecutor().submit(streamGobbler);
int exitCode = process.waitFor();
assert exitCode == 0;
}
}

ProcessBuilder 클래스는 command로 입력 받은 명령어를 사용해서 builter.start()를 호출해서 서브 해당 명령어를 실행하는 서브 프로세스를 생성한다.

실행결과

java%20shell%20command%20cd59c192a52849578dde8f6c36ee8eb9/Untitled%204.png

마무리

위 예제는 단순히 java에서 쉘 커멘드를 호출하는 예제이다.

java에서 shell command를 위처럼 단순히 호출하는 일은 거의 없을 것이다.

이런 다양한 요구 사항 또는 외부 전제 조건이 있을 수 있다.

  • java에서 shell command를 호출하고 그 응답으로 JSON String으로 받는다.
  • java에서 응답 받은 String을 객체화 하여 DB에 저장해야 한다.
  • java에서 호출하는 shell Command는 한번 호출하면 최소 5분 이상의 동작하고, 그 동작이 끝나야 응답을 한다.
  • 동작이 끝나지 않은 shell command가 존재하면, java에서 shell command를 호출 하면 안된다.
  • 동작이 끝나지 않은 shell command가 존재하더라도, java에서 shell command를 호출 할 수 있어야 한다.

대충 위와 같은 경우의 수를 처리 하려면, 얼핏봐도 비동기, 쓰레드 코딩이 필요해 보인다.

단순히 스크립트등을 shell command로 호출하고 결과를 받는 코딩을 하기에 너무 많은 의존성과 공수가 필요해 보인다.

이럴 바에는 차라리 shell cmd로 호출할 로직을 간단한 MSA로 구축해서 서비스로 만드는 것이 좋아 보인다.

날림으로 java를 공부해서 예제를 따라 만드는데도 이해가 잘 안가는 부분이 있다.

java를 다시 공부해야 겠다.

참고자료

https://www.baeldung.com/run-shell-command-in-java (java로 shell command 호출)

https://m.blog.naver.com/PostView.nhn?blogId=jhnyang&logNo=221407227360&proxyReferer=https:%2F%2Fwww.google.com%2F (java로 파이썬 실행)

https://d2.naver.com/helloworld/1113548 (java 에서 쉘 커멘드 호출 시 주의 사항)

https://www.javatpoint.com/java-runtime-class (java Runtime 클래스)