"폐쇄망에서 이거 쓰려면 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 선택 이유:
- JDK 내장 - 별도 도구 설치 불필요
- JRE 번들링 - Java 설치 없이 실행
- OS별 설치파일 - Windows exe, macOS pkg, Linux deb/rpm
- 폐쇄망 친화적 - 단일 설치파일로 배포
2. jpackage 기본 개념
jpackage란?
JDK 14부터 정식 포함된 배포 도구다. Java 앱을 OS별 설치 패키지로 만들어준다.
jpackage --name MyApp \
--input target/ \
--main-jar myapp.jar \
--type exe
이 한 줄로:
- JRE를 앱에 포함시키고
- 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()이 호출된다.
문제의 흐름:
- exe 실행 → CFT.cfg 읽음 → 인자 없음
guiMode = false(기본값)projectPath = null(인자 없음)- 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 "if exist %USERPROFILE%\.code-flow-tracer rmdir /s /q %USERPROFILE%\.code-flow-tracer""
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
설치 화면
설치 파일을 실행하면:

- 설치 경로 선택 (기본:
C:\Program Files\CFT)
- 바탕화면 바로가기 / 시작 메뉴 옵션

- 설치 완료 → 바로 실행
사용자는 Java 설치 없이 바로 실행할 수 있다.
6. 마치며
핵심 정리
| 문제 | 해결 |
|---|---|
| WiX 필요 | WiX 3.14 설치 (JDK 21 호환) |
| 한글 인코딩 깨짐 | description 영문 사용 |
| exe 실행 무반응 | --arguments '--gui' 추가 |
| clean 파일 잠금 | 출력 경로 분리 |
| 삭제 시 데이터 유지 | CustomAction으로 직접 삭제 |
삽질 포인트
- WiX 버전: 최신이 좋은 게 아니다. JDK 21은 WiX 3.x 필요.
- 인코딩 체인: Gradle → PowerShell → jpackage → WiX, 각 레이어의 인코딩이 다르다.
- 기본 실행 모드: CLI/GUI 겸용 앱은
--arguments로 기본 동작 설정 필수. - WiX Directory: 특수 폴더 참조 시 중첩 구조 주의.
이 글을 쓰며 배운점
- 배포는 개발의 연장선: 코드 완성이 끝이 아니다. 사용자 환경까지 고려해야 진짜 완성.
- 인코딩 이해: Windows 환경의 인코딩 레이어 (CP949, UTF-8, windows-1252)를 이해하게 됐다.
- 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 |