jpackage로 배포하기 - 폐쇄망에서 Java 없이 실행하기

2026. 1. 12. 22:28·토스 러너스하이 2기/기술

"폐쇄망에서 이거 쓰려면 JDK 따로 구해서 환경변수 설정하고..."
그 과정이 싫었다. 사용자들은 그냥 exe 하나로 끝내게 하고 싶었다.
jpackage로 JRE를 통째로 번들링하면서 겪은 삽질들.


3줄 요약

  • jpackage는 JDK 14+에 내장된 배포 도구로, JRE 포함 설치파일을 만들 수 있다
  • Windows exe 생성에는 WiX Toolset이 필요한데, JDK 21은 WiX 3.x만 지원한다
  • 인코딩 지옥을 피하려면 description은 영문으로, exe 실행 문제는 --arguments로 해결한다

1. 들어가며

이전 글에서 Swing GUI를 완성했다.

FlatLaf로 다크 테마를 입히고, SwingWorker로 분석을 백그라운드에서 돌리고, 설정도 저장되게 만들었다.

이제 남은 건 배포다.

문제: 폐쇄망에서 Java가 없다

공공기관 폐쇄망 환경을 떠올려보자:

  • 인터넷 연결 불가
  • 임의 프로그램 설치 제한
  • Java 런타임? 설치되어 있을 수도 있고 없을 수도 있다
# 일반적인 Java 앱 실행
java -jar code-flow-tracer.jar --gui

이렇게 실행하려면 Java가 설치되어 있어야 한다. 폐쇄망에서 "Java 17 설치해주세요"라고 요청하는 건 현실적으로 어렵다.


비유: 요리와 재료

구분 JAR 배포 jpackage 배포
비유 레시피만 전달 밀키트 배송
전달 내용 앱 코드 (JAR) 앱 + JRE + 설치파일
사용자 준비 Java 설치 필요 그냥 설치
용량 ~1MB ~80MB

 

JAR 배포는 "레시피"만 전달하는 것과 같다. 사용자가 "재료"(Java)를 직접 준비해야 한다.

jpackage 배포는 밀키트를 보내는 것과 같다. 재료(JRE)가 다 들어있어서 바로 조리(실행)할 수 있다.


왜 jpackage인가?

Java 앱 배포 방법은 여러 가지가 있다:

방법 장점 단점 폐쇄망
JAR 용량 작음, 간단 Java 필요 ❌
launch4j exe로 래핑 Java 필요 ❌
GraalVM Native 빠름, 작음 리플렉션 제한, 빌드 복잡 △
jpackage JRE 번들링, 설치파일 용량 큼 ✅

 

jpackage 선택 이유:

  1. JDK 내장 - 별도 도구 설치 불필요
  2. JRE 번들링 - Java 설치 없이 실행
  3. OS별 설치파일 - Windows exe, macOS pkg, Linux deb/rpm
  4. 폐쇄망 친화적 - 단일 설치파일로 배포

2. jpackage 기본 개념

jpackage란?

JDK 14부터 정식 포함된 배포 도구다. Java 앱을 OS별 설치 패키지로 만들어준다.

jpackage --name MyApp \
         --input target/ \
         --main-jar myapp.jar \
         --type exe

이 한 줄로:

  1. JRE를 앱에 포함시키고
  2. Windows 설치파일(.exe)을 생성한다

생성되는 파일 구조

CFT-1.0.0.exe (설치파일)
└── 설치 후 →
    C:\Program Files\CFT\
    ├── CFT.exe              # 런처
    ├── app\
    │   └── code-flow-tracer.jar
    └── runtime\
        └── (JRE 전체)       # ~70MB

runtime 폴더에 JRE가 통째로 들어간다. 그래서 용량이 커지지만, 사용자는 Java 설치 없이 바로 실행할 수 있다.


3. Gradle에서 jpackage 설정

build.gradle 설정

def jpackagePath = System.getenv('JAVA_HOME') + '/bin/jpackage'

task jpackage(type: Exec, dependsOn: shadowJar) {
    def version = project.version  // 1.0.0
    def appName = 'CFT'
    def appDescription = 'Code Flow Tracer - Java Call Flow Analyzer'

    commandLine jpackagePath,
        '--name', appName,
        '--app-version', version,
        '--description', appDescription,
        '--vendor', 'KBroJ',
        '--input', 'build/libs',
        '--main-jar', "code-flow-tracer-${version}.jar",
        '--dest', 'build/release',
        '--type', 'exe',
        '--icon', 'src/main/resources/icon.ico',
        '--win-dir-chooser',
        '--win-shortcut',
        '--win-menu',
        '--java-options', '-Dfile.encoding=UTF-8',
        '--arguments', '--gui'  // 기본 GUI 모드로 실행

    doFirst {
        println "Building installer for ${appName} v${version}"
    }
}

주요 옵션 설명

옵션 설명
--name 앱 이름 (설치 폴더명)
--input JAR 파일 위치
--main-jar 실행할 JAR 파일
--type exe Windows 설치파일 형식
--win-shortcut 바탕화면 바로가기 생성
--arguments 기본 실행 인자

4. 트러블슈팅 모음

여기서부터가 본론이다. jpackage는 "그냥 되는" 도구가 아니었다.


Issue #015: WiX Toolset이 필요하다

Can not find WiX tools (light.exe, candle.exe)
Download WiX 3.0 or later from https://wixtoolset.org
Error: Invalid or unsupported type: [exe]

jpackage로 Windows exe를 만들려면 WiX Toolset이 필요하다. JDK에 포함되어 있지 않아서 별도 설치해야 한다.

# WiX 설치 (winget)
winget install WiXToolset.WiXToolset

# 설치 확인
where candle.exe
where light.exe

WiX (Windows Installer XML): Microsoft의 오픈소스 설치 패키지 도구다.

jpackage는 내부적으로 WiX를 호출해서 설치파일을 만든다.


Issue #016: WiX 6.0이 안 된다

WiX를 설치했는데도 같은 에러가 난다.

Can not find WiX tools (light.exe, candle.exe)

원인: WiX 버전 아키텍처 변경

WiX 버전 도구 구조
WiX 3.x candle.exe + light.exe (분리)
WiX 4/5/6 wix.exe (통합)

JDK 21은 WiX 3.x만 지원한다. WiX 6.0을 설치해도 candle.exe가 없어서 jpackage가 찾지 못한다.

왜 WiX 3.x만 지원하나?

jpackage가 내부적으로 candle.exe, light.exe를 직접 호출하는 방식으로 구현되어 있기 때문이다. WiX 4+에서는 이 도구들이 wix.exe로 통합되면서 호출 방식이 완전히 바뀌었고, JDK가 아직 새 방식을 지원하지 않는다.

JDK 버전별 WiX 지원:
- JDK 23 이하: WiX 3.x만 지원
- JDK 24+: WiX 4+ 지원 (JDK-8319457)

해결: WiX 3.14 설치

# WiX 6.0이 이미 있어도 3.14 추가 설치 가능
winget install WiXToolset.WiXToolset --version 3.14.0

설치 경로: C:\Program Files (x86)\WiX Toolset v3.14\bin\

이 폴더에 candle.exe, light.exe가 있다.


Issue #017: 한글 인코딩 지옥

WiX 3.14를 설치하고 다시 빌드했더니:

light.exe ... exited with 311 code

exit code 311이 뭔지 찾아봐도 정보가 없었다. 삽질 끝에 원인을 찾았다.

원인: --description에 한글 포함

// 문제의 코드
'--description', 'Code Flow Tracer - Java 호출 흐름 분석 도구'

인코딩이 여러 레이어를 거치면서 깨진다:

Gradle (UTF-8) → PowerShell (CP949) → jpackage → WiX (windows-1252)

WiX 기본 로컬라이제이션 파일이 windows-1252 인코딩을 사용하는데, 한글은 이 인코딩에서 지원되지 않는다.

해결: 영문으로 변경

// 우회 해결
'--description', 'Code Flow Tracer - Java Call Flow Analyzer'

빠른 배포가 목표였기 때문에 영문으로 변경했다. 한글이 꼭 필요하면 커스텀 WiX 로컬라이제이션 파일을 만들어야 한다.


Issue #018: exe 실행해도 아무 반응이 없다

드디어 CFT-1.0.0.exe가 생성됐다. 설치하고 실행했더니... 아무 반응이 없다.

바탕화면 바로가기 클릭 → 반응 없음
설치 폴더의 CFT.exe 클릭 → 반응 없음

프로세스가 순간적으로 시작되었다가 즉시 종료된다.

원인 분석

Main.java 코드를 보자:

@Command(name = "cft", ...)
public class Main implements Callable<Integer> {
    @Option(names = {"--gui", "-g"}, description = "GUI 모드로 실행")
    private boolean guiMode;

    @Parameters(index = "0", description = "분석할 프로젝트 경로", arity = "0..1")
    private String projectPath;

    @Override
    public Integer call() {
        if (guiMode) {
            SwingUtilities.invokeLater(() -> new MainFrame().setVisible(true));
            return 0;
        }
        // CLI 모드: projectPath 필수
        if (projectPath == null) {
            spec.commandLine().usage(System.out);
            return 1;  // 에러 종료 ← 여기서 끝남!
        }
        // ...
    }
}

왜 인자 없이 실행되나?

jpackage로 만든 exe는 내부적으로 CFT.cfg 설정 파일을 읽어서 실행 인자를 전달한다.
기본적으로 이 파일에 인자가 없으면 빈 인자로 main()이 호출된다.

문제의 흐름:

  1. exe 실행 → CFT.cfg 읽음 → 인자 없음
  2. guiMode = false (기본값)
  3. projectPath = null (인자 없음)
  4. CLI 모드로 진입 → 경로 없어서 즉시 종료

해결: --arguments 옵션 추가

'--arguments', '--gui'  // 기본 실행 인자

이렇게 하면 CFT.cfg 파일에 기본 인자가 설정된다:

[Application]
app.mainjar.argument.1=--gui

exe 실행 시 자동으로 --gui 인자가 전달되어 GUI 모드로 시작한다.


Issue #019: Gradle clean이 안 된다

개발 중에 반복 빌드를 하다 보면:

./gradlew clean shadowJar

그런데 clean이 실패한다:

Execution failed for task ':clean'.
> Unable to delete directory 'build'
  - build\installer\CFT-1.0.0.exe

원인: Windows 파일 잠금

  • 탐색기에서 build 폴더를 열어둠
  • 백신 프로그램이 exe 스캔 중
  • 이전 jpackage 프로세스가 완전히 종료되지 않음

해결: 출력 경로 분리

// 기존 (문제)
'--dest', 'build/installer'

// 변경 (해결)
'--dest', 'build/release'

build/release처럼 별도 폴더를 사용하면 clean 영향을 덜 받는다. 근본적으로는 탐색기를 닫거나, 백신 예외 설정을 해야 한다.


Issue #024: 삭제해도 데이터가 남는다

프로그램을 삭제하고 재설치해도 이전 세션 기록이 그대로 남아있다.

  • 세션 파일 위치: ~/.code-flow-tracer/session.json
  • 언인스톨 후에도 파일이 삭제되지 않음

시도 1: WiX RemoveFolder - 실패

<Directory Id="ProfileFolder">
  <Directory Id="CFTSessionDir" Name=".code-flow-tracer">
    <Component Id="SessionCleanup">
      <RemoveFile Id="RemoveAllSessionFiles" Name="*" On="uninstall" />
      <RemoveFolder Id="RemoveSessionFolder" On="uninstall" />
    </Component>
  </Directory>
</Directory>

레지스트리에는 등록됐지만 실제로 삭제가 안 됐다.

 

원인 분석

WiX에서 ProfileFolder가 TARGETDIR 내부에 중첩되어 있었다:

<Directory Id="TARGETDIR" Name="SourceDir">
  <Directory Id="ProfileFolder">  <!-- TARGETDIR 안에 있음! -->

 

WiX가 ProfileFolder를 %USERPROFILE%이 아닌 설치 경로의 하위 디렉토리로 해석했다.

의도: C:\Users\Winbit\.code-flow-tracer
실제: C:\Program Files\CFT\.code-flow-tracer (존재하지 않음)

 

해결: CustomAction으로 직접 삭제

WiX Directory 구조 문제를 우회해서 cmd.exe로 직접 삭제한다:

<CustomAction Id="RemoveSessionFolder"
              Directory="TARGETDIR"
              ExeCommand="cmd.exe /c &quot;if exist %USERPROFILE%\.code-flow-tracer rmdir /s /q %USERPROFILE%\.code-flow-tracer&quot;"
              Execute="deferred"
              Return="ignore" />

<InstallExecuteSequence>
  <Custom Action="RemoveSessionFolder" After="RemoveFiles">REMOVE="ALL"</Custom>
</InstallExecuteSequence>
  • REMOVE="ALL" 조건: 언인스톨 시에만 실행
  • %USERPROFILE% 환경변수로 정확한 경로 지정
  • rmdir /s /q: 폴더와 모든 내용 강제 삭제

5. 최종 결과물

빌드 실행

./gradlew clean shadowJar
./gradlew jpackage

생성된 파일

build/release/
└── CFT-1.0.0.exe  # 77.3 MB

설치 화면

설치 파일을 실행하면:

 

  1. 설치 경로 선택 (기본: C:\Program Files\CFT)
  2. 바탕화면 바로가기 / 시작 메뉴 옵션
  3. 설치 완료 → 바로 실행

 

사용자는 Java 설치 없이 바로 실행할 수 있다.


6. 마치며

핵심 정리

문제 해결
WiX 필요 WiX 3.14 설치 (JDK 21 호환)
한글 인코딩 깨짐 description 영문 사용
exe 실행 무반응 --arguments '--gui' 추가
clean 파일 잠금 출력 경로 분리
삭제 시 데이터 유지 CustomAction으로 직접 삭제

삽질 포인트

  1. WiX 버전: 최신이 좋은 게 아니다. JDK 21은 WiX 3.x 필요.
  2. 인코딩 체인: Gradle → PowerShell → jpackage → WiX, 각 레이어의 인코딩이 다르다.
  3. 기본 실행 모드: CLI/GUI 겸용 앱은 --arguments로 기본 동작 설정 필수.
  4. WiX Directory: 특수 폴더 참조 시 중첩 구조 주의.

이 글을 쓰며 배운점

  1. 배포는 개발의 연장선: 코드 완성이 끝이 아니다. 사용자 환경까지 고려해야 진짜 완성.
  2. 인코딩 이해: Windows 환경의 인코딩 레이어 (CP949, UTF-8, windows-1252)를 이해하게 됐다.
  3. WiX 기초: MSI 설치파일의 구조와 CustomAction 개념을 배웠다.

'토스 러너스하이 2기 > 기술' 카테고리의 다른 글

CRUD 분석과 테이블 영향도 - Bottom-Up 분석의 힘  (0) 2026.01.18
Gson으로 세션 영속성 구현하기 - 앱을 닫아도 분석 결과가 살아있다  (0) 2026.01.17
Swing으로 모던한 GUI 만들기 - CLI를 넘어서  (1) 2026.01.12
분석 결과를 Excel로 정리하기 - Apache POI 활용기  (0) 2026.01.10
분석 결과를 어떻게 보여줄까? - CLI 출력 구현기  (1) 2026.01.07
'토스 러너스하이 2기/기술' 카테고리의 다른 글
  • CRUD 분석과 테이블 영향도 - Bottom-Up 분석의 힘
  • Gson으로 세션 영속성 구현하기 - 앱을 닫아도 분석 결과가 살아있다
  • Swing으로 모던한 GUI 만들기 - CLI를 넘어서
  • 분석 결과를 Excel로 정리하기 - Apache POI 활용기
KBroJ9210
KBroJ9210
  • KBroJ9210
    개발일기
    KBroJ9210
  • 전체
    오늘
    어제
    • 분류 전체보기 (25)
      • 토스 러너스하이 2기 (11)
        • 회고 (1)
        • 기술 (10)
      • Loopers (9)
        • 테크니컬 라이팅 (6)
        • WIL(What I Learned) (3)
      • 두리두리넋두리 (5)
        • 개발일기 (5)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
KBroJ9210
jpackage로 배포하기 - 폐쇄망에서 Java 없이 실행하기
상단으로
목차

    티스토리툴바