Command line으로 Java 코드를 실행하는 과정

blackdog·2021년 8월 7일
1
post-thumbnail

서론

Velog에 작성하는 첫 글이니 짧고 가벼운 글 먼저 써봐야겠다. 요즘 회사 일을 하면서 Java 실행 과정에 대한 기초가 많이 부족함을 느꼈다. 로컬은 IDE에서 컴파일, 빌드, 실행 모두를 해주니 실제 환경과 동일한 방식으로 코드를 실행시켜볼 생각을 하지 않았다. 또한 배포도 많은 부분이 자동화되어 있다보니, 로컬에서 작성한 코드를 프로세스에 따라서 배포하는 것에만 관심이 있었다. 버튼 몇번 누르면 잘 동작 하니까! 그러고는 실제 실행 환경에서 내가 짠 코드가 어떤 과정을 통해 실행되는지 제대로 파악하고 있지 않았던 것 같다. 그래서 그 동안 애매하게 알고 있던 부분을 조금이나마 정리를 하기 위해서 이 글을 작성했다.

Java 실행 과정

간단히 설명한 Java 코드 실행 과정은 다음과 같다.

  1. 자파 소스 파일(.java)을 컴파일을 통해 자바 바이트 코드(.class)로 변환
  2. 자바 바이트 코드를 클래스 로더가 로드해서 JVM의 Runtime Data Area에 적재한다.
  3. Runtime Data Area에 적재된 바이트 코드들은 JVM의 Execution Engine에 의해 해석(Interpret)되어 Binary code로 변경된다음 실행된다.

위 과정 에서 소스를 컴파일하고 실행하는 부분을 커맨드라인과 IDE(intellij)를 비교하며 실행해보려고 한다.

Command line으로 실행해보기

아래와 같이 간단한 예제 클래스를 작성했다.

package com.blackdog.example;

public class Hello {

    public static void main(String[] args) {
        System.out.println("com.blackdog.example.Hello");
    }
}

프로젝트 구조는 아래와 같다.

이제 이 Hello 클래스를 직접 컴파일하고 실행해보자.

executionSample/src/main/java$ javac com/blackdog/example/Hello.java

아래와 같이 컴파일 시 -d 옵션을 사용하면 컴파일 결과물이 생성되는 경로를 지정할 수 있다. 그러면 another_dir 밑에 com/blackdog/example/Hello.class 처럼 동일한 패키지 구조를 유지하며 컴파일 결과물이 생성된다.

executionSample/src/main/java$ javac -d another_dir com/blackdog/example/Hello.java

컴파일 결과물로는 Hello.class 파일이 생성되었고, 이 클래스 파일을 실행해보자. 패키지 명까지 같이 적어줘야 한다.

executionSample/src/main/java$ java com.blackdog.example.Hello

실행 결과, 정상 실행되어 com.blackdog.example.Hello가 출력된 것을 볼 수 있다.

executionSample/src/main/java 가 아닌 다른 경로에서 동일하게 코드를 실행하면 클래스를 찾을 수 없다는 에러를 리턴한다. 왜냐하면 JVM이 Hello 클래스를 찾을 수 없기 때문이다.
Hello 클래스는 com.blackdog.example 패키지에 포함되어 있다. 위에서는 JVM이 실행된 위치부터 패키지를 따라서 Hello 클래스를 찾을 수 있었다. ({실행위치}/com/blackdog/example)
하지만 실행 위치가 변하고, 패키지를 찾을 수 없게 되어 에러가 나는 것이다. 이럴 땐 패키지 계층의 최상위 경로를 -classpath 옵션으로 넣고 실행해주면 된다.

~$ java -cp executionSample/src/main/java com.blackdog.example.Hello

그런데 평소에 주로 실행하는 자바 코드는 jar 파일로 아카이빙 되어있다. jar 파일은 어떻게 실행하는가? 마찬가지로 클래스패스로 지정해주면 된다.

~$ java -cp executionSample.jar com.blackdog.example.Hello

Intellij에서 실행

그렇다면 Intellij 에서는 위 과정을 어떻게 실행하고 있는 걸까? 검색 결과 Intellij는 컴파일 시 java binary를 쓰지 않고, 자바 컴파일러 API를 사용하기 때문에 컴파일 시 실행되는 커맨드를 직접 볼 수 있는 방법은 없다고 한다.

하지만 코드 실행 커맨드는 볼 수 있었다. 아래는 Intellij에서 코드 실행 시 나타나는 커맨드이다. -classpath 부분을 보면 실행할 코드도 클래스패스에 추가하는 것을 볼 수 있다. maven 프로젝트 default output 폴더인 target 폴더의 classes 밑에 컴파일 된 파일이 위치하기 때문에 해당 경로를 클래스패스로 지정해주게 된다.

/usr/lib/jvm/java-1.8.0-openjdk-amd64/bin/java # java binary
-javaagent:... # java agent 지정
-Dfile.encoding=UTF-8 # 인코딩 지정 옵션
-classpath /usr/lib/jvm/java-1.8.0-openjdk-amd64/jre/lib/charsets.jar:... # jre lib
:/home/blackdog/workspace/executionSample/target/classes # 컴파일된 바이트 코드 위치
com.blackdog.example.Hello # 실행 클래스 지정

(그냥 코드 한 줄 실행하는데도 이렇게 많은 jar 파일을 참조하고 있다.)

참조하는 외부 dependency가 있는 경우

그렇다면 maven을 이용해 dependency를 추가하면 어떻게 될까?

/usr/lib/jvm/java-1.8.0-openjdk-amd64/bin/java ...
-classpath ...
:/home/blackdog/workspace/executionSample/target/classes
:/home/blackdog/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.12.3/jackson-core-2.12.3.jar
com.blackdog.example.Hello

pom.xml 파일에 jackson dependency를 추가한 후 다시 Intellij 코드를 실행했다. 그랬더니 위 처럼 command line에 jackson jar 파일을 클래스패스로 지정하는 부분이 생겼다. maven local repository인 .m2 디렉토리에 있는 jar 파일을 참조하고 있다.

그러면 다시 이 과정도 커맨드라인으로 재현해보자. (jackson 라이브러리를 사용하도록 테스트 코드를 변경했다.)
jar로 패키징한 파일을 커맨드라인에서 실행했더니 jackson 관련된 class를 찾을 수 없다는 오류가 발생했다.

왜냐하면 maven에서 import한 dependency를 jar 파일에 포함시키는 것이 아니기 때문이다. 그래서 classpath에 dependency path를 지정해서 실행해야 한다.

java
-cp /home/blackdog/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.12.4/jackson-databind-2.12.4.jar:...
:executionSample-1.0-SNAPSHOT.jar com.blackdog.example.Hello

만약에 jar 파일에 실제 dependency jar 파일을 모두 포함해서 만들고 싶다면 maven shade plugin을 사용하면 된다.

결론 및 후기

처음에 궁금했던 것은 'maven으로 빌드한 jar 파일을 어떻게 실행하는가?' 였다. 생각해보면, 항상 maven은 빌드 툴이라는 사실을 헷갈리는 것 같다. 그래서 막연하게 'maven을 이용해서 패키징을 하면 바로 실행할 수 있도록 필요한 모든 라이브러리를 수집해서 jar 파일에 넣어준다.' 라고 생각해왔고, 나는 maven이 만들어준 jar 파일을 서버에 올려서 실행만 시키면 된다는 생각을 하고 있었다.
하지만 maven이든, gradle이든 내가 짠 자바 코드만을 패키징을 해줄 뿐이고(shade라는 옵션이 있지만), 실제로 실행을 할 때는 필요한 라이브러리를 클래스패스로 지정해주어야 한다. 업무를 하면서 주로 보게되는 war/jar(spring boot) 파일은 안에 라이브러리 파일을 포함하고 있기 때문에, 다른 파일들도 마찬가지 일거라고 잘못 생각하고 있던 것 같다.

그리고 처음으로 글을 써보니, 내가 모르는 부분과 그 부분을 조사하여 내린 결론을 간결하게 전달하는게 쉽지 않다. 이번 글을 다시 보니 글이 되게 중구난방인데 다음에 글을 쓸 때는 전체 개요를 짜놓고 써보도록 해야겠다.

profile
검둥검둥 검둥개

0개의 댓글