본문 바로가기

WEB/Spring

[Spring] Apache.commons.exec 사용, 외부 명령어 실행 API 만들기, Java에서 Shell 사용

3번째 프로젝트가 성공적으로 종료되었습니다.

기존의 WEB 프로젝트와 많이 다른 방식으로 진행되었고 그 중 처음 사용해보거나 의미있는 기술들을 포스팅 해보려합니다.

 

프로젝트는 다음과 같이 진행되었습니다. 

  • Java 17
  • Spring Boot 3.2.5
  • Gradle
  • React

1. 목표 

이번 프로젝트의 주제는 초보자를 위한 인프라 플랫폼입니다.

GitHub 링크 : https://github.com/o54711254/Dobie

 

GitHub - o54711254/Dobie

Contribute to o54711254/Dobie development by creating an account on GitHub.

github.com

간단한 정보를 입력 후 배포를 경험할 수 있게 하는것이 이번 프로젝트의 주제였고 간단한 버튼 클릭만으로 Docker, Git에서 사용하는
외부 명령어를 실행하여야 했습니다. 또한 명령어 실행 결과를 관리할 수 있어야 했습니다.

이번 포스팅에서는 Java기반의 SpringBoot 환경에서 Apache.commons.exec 를 활용하여 외부 명령어를 실행하는법을 포스팅하겠습니다.

 

2. 개념 

1) Apache.commons.exec

Apache.commons.exec는 Java 애플리케이션에서 외부 프로세스를 실행할 수 있게 도와주는 라이브러리 입니다.

다음과 같은 장점을 가지고 있습니다.

  • 외부 프로세스 실행을 위한 간단하고 직관적인 API 제공
  • Window, Linux, Mac 다양한 환경에서 동일한 코드로 외부 프로세스 실행 가능
  • 프로세스를 비동기적으로 실행하여 메인 애플리케이션의 성능을 저하시키지 않고 병렬 처리
  • 표준 출력 및 표준 오류 스트림 관리 용이

2) DefaultExecutor

명령어를 실행하는 기본 실행기입니다.

3) ByteArrayOutputStream

명령어 실행 결과를 저장하는 스트림입니다.
스트림의 내용을 문자열로 반환하여 출력과 오류를 쉽게 확인할 수 있게 합니다.

4) PumpStreamHandler

명령어 실행 결과를 처리하는 handler.
실행결과를 ByteArrayOutputStream에 연결합니다.

5) CommandLine

실행할 명령어를 지정합니다.

3. 준비 

1) 의존성 추가

MavenRepository 링크 : https://mvnrepository.com/artifact/org.apache.commons/commons-exec/1.3


구현

프로젝트 코드를 들고왔습니다. 그대로 사용하기보단 참고하기를 권장드립니다.

1. 공통 사용 구성요소 선언

 

저는 CommandService 클래스에 명령어 실행 메서드를 모아놨습니다.

공통적으로 사용되는 DefaultExecutor, ByteArrayOutputStream, PumpStreamHandelr를 선언해주었습니다.

StringBuilder는 명령어를 작성하는데 사용했습니다.

저는 메서드마다 선언하는게 귀찮아서 선언을 해놓고 사용했는데 기호에 따라 사용하시면 될 것 같습니다.

 

2. CommandLine

CommandLine으로 실행할 명령어를 정의합니다.

CommandLine도 다양한 형태로 사용할 수 있습니다.

 

1) addArgument

addArgument는 명령어에 전달할 개별 인자를 체계적으로 추가합니다.

복수의 인자를 순차적으로 추가하는 방식입니다.

 

예시) git clone https://github.com/user/repo.git 명령어

CommandLine commandLine = new CommandLine("git");
cmdLine.addArgument("clone");
cmdLine.addArgument("https://github.com/user/repo.git");

 

2) parse

전체 명령어 문자열을 받아 이를 한번에 설정

 

예시) git clone https://github.com/user/repo.git 명령어

CommandLine commandLine = CommandLine.parse("git clone https://github.com/user/repo.git");

 

차이점

 

저는 StringBuilder로 문자열을 구성하고 이를 한번에 parse하는 방식으로 구현하였습니다.

 

2. 메서드 구현

제가 구현한 API를 예시로 설명하겠습니다.
docker compose up 명령어를 실행할때 사용한 메서드로 path 변수를 받아 사용했습니다.

@Override
    public void dockerComposeUp(String path) {
        sb = new StringBuilder();
        sb.append("docker compose -f ").append(path + "/docker-compose.yml").append(" up --build -d");

        CommandLine commandLine = CommandLine.parse(sb.toString());
        executor.setStreamHandler(streamHandler);
        try {
            executor.execute(commandLine);
            String result = outputStream.toString().trim(); // 명령어 실행 결과를 문자열로 받음
            System.out.println("compose up success : " + result);
        } catch (Exception e) {
            String result = outputStream.toString().trim();
            throw new ProjectStartFailedException(e.getMessage(), result);
        }
    }

 

1) StringBuilder를 이용해 명령어를 구성

알고리즘 문제를 풀때 StringBuilder를 많이 사용해 익숙해서 사용한 것이니 편하신 방식을 사용하시면 됩니다.

저렇게 했을때 sb는 "docker compose -f {path}/docker-compose.yml up --build -d" 가 됩니다.

 

2) CommandLine에 parse

sb 를 commandLine에 parse합니다. sb를 문자열로 parse 해야되기에 sb.toString()을 넣었습니다.

 

3) setStreamHandler

setStreamHandler는 DefaultExecutor의 메서드로 실행중인 프로세스의 입력, 출력, 그리고 오류 스트림을 제어하는데 사용됩니다.

위에서 선언한 PumpStreamHandler를 선언한 streamHandler를 설정해 주었습니다.

 

4) 실행

executor.execute(commandLine)으로 commandLine의 명령어를 실행합니다.

실행 결과는 result에 outputStream을 문자열로 받아서 System.out.println으로 보여줍니다.

try catch로 예외처리를 하였습니다.

 

3. 구현 예시

위의 내용을 한번에 볼 수 있는 코드를 보여드리겠습니다.

원래 프로젝트의 코드에서 설명에 포함된 부분만 잘라왔습니다.

참고만 하시길 권장드립니다.

@Service
@Slf4j
@RequiredArgsConstructor
public class CommandServiceImpl implements CommandService {

    DefaultExecutor executor = new DefaultExecutor();
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream);
    StringBuilder sb;
    
    @Override
  	public void dockerComposeUp(String path) {
        sb = new StringBuilder();
        sb.append("docker compose -f ").append(path + "/docker-compose.yml").append(" up --build -d");

        CommandLine commandLine = CommandLine.parse(sb.toString());
        executor.setStreamHandler(streamHandler);
        try {
            executor.execute(commandLine);
            String result = outputStream.toString().trim(); // 명령어 실행 결과를 문자열로 받음
            System.out.println("compose up success : " + result);
        } catch (Exception e) {
            String result = outputStream.toString().trim();
            throw new ProjectStartFailedException(e.getMessage(), result);
        }
    }
}

 


저의 부족한 코드가 조금이라도 도움이 될 수 있다면 좋겠습니다.

초보 개발자의 글이라 부족한 부분이 많이 있습니다. 지적해 주시면 감사히 받겠습니다.