"명령어로 해야하는건 접근성이 좀 떨어지는데.. 비개발자도 쓸 수 있어야 하지 않을까?"
Swing으로 GUI를 만들면서 FlatLaf, SwingWorker, JEditorPane을 처음 써봤다. 그 과정에서 배운 것들.
3줄 요약
- FlatLaf 한 줄로 다크 테마:
FlatDarculaLaf.setup()한 줄로 IntelliJ 스타일 모던 UI - SwingWorker로 UI 블로킹 방지: 분석 작업을 백그라운드에서 실행하여 반응성 유지
- JEditorPane + HTML로 유연한 표현: 색상, 텍스트 선택, 복사 기능을 동시에 지원
들어가며
이전 글에서 분석 결과를 Excel로 정리하는 방법을 다뤘다.
CLI와 Excel, 두 가지 출력 방식이 완성됐는데... 한 가지 고민이 생겼다.
"명령어로 해야하는건 접근성이 좀 떨어지는데.. 비개발자도 사용할 수 있어야 하지 않을까?"
터미널에 익숙하지 않은 분들에게 java -jar code-flow-tracer.jar --path C:\project를 입력하라고 하는 건 진입 장벽이 높다. 마치 리모컨 없이 TV 뒷면 버튼으로만 조작하라는 것과 같다.
그래서 GUI를 만들기로 결정했다. 폴더 선택 버튼 하나로 분석을 시작할 수 있는 "리모컨"을 만드는 것이다.
기술 선택: 왜 Swing인가?
GUI 프레임워크를 선택할 때 몇 가지 옵션을 검토했다:
| 옵션 | 장점 | 단점 |
|---|---|---|
| JavaFX | 모던 UI, CSS 스타일링 | 별도 런타임 필요, JDK 11+ |
| Swing | JDK 내장, 안정성 | 촌스러운 기본 UI |
| Electron | 웹 기술 활용 | 무거움, Java 연동 복잡 |
Swing을 선택한 이유:
- JDK 내장: 추가 의존성 없이 바로 사용 가능
- jpackage 호환: 단일 EXE로 배포할 때 문제 없음
- 폐쇄망 친화적: 외부 리소스 다운로드 불필요
- FlatLaf로 해결: 촌스러운 UI 문제는 한 줄로 해결 가능
1. FlatLaf - 한 줄로 모던 UI
Swing의 가장 큰 단점은 촌스러운 기본 UI다. 그런데 이걸 한 줄로 해결할 수 있다.
기본 Swing UI의 문제
Swing을 처음 실행하면 Metal Look and Feel이라는 기본 테마가 적용된다.
- 볼록한 3D 버튼 (90년대 Windows 느낌)
- 밝은 회색 배경
- 투박한 스크롤바와 테두리
요즘 IDE나 개발 도구들은 대부분 다크 테마를 지원하는데, 기본 Swing은 그런 느낌이 전혀 없다. IntelliJ IDEA나 VS Code 같은 모던한 다크 UI를 원한다면 FlatLaf이 정답이다.
적용 방법
// build.gradle
dependencies {
implementation 'com.formdev:flatlaf:3.2.5'
}
import com.formdev.flatlaf.FlatDarculaLaf;
public class MainFrame extends JFrame {
public static void main(String[] args) {
// 이 한 줄로 전체 앱에 다크 테마 적용!
FlatDarculaLaf.setup();
SwingUtilities.invokeLater(() -> {
new MainFrame().setVisible(true);
});
}
}
진짜 한 줄이다. FlatDarculaLaf.setup()을 호출하면 모든 Swing 컴포넌트가 IntelliJ IDEA 스타일의 다크 테마로 변환된다.
실제 적용 결과

FlatLaf Darcula 테마가 적용된 Code Flow Tracer GUI. 좌측에 URL 목록, 우측에 분석 결과가 표시된다.
FlatLaf이 제공하는 테마들
FlatLightLaf.setup(); // 밝은 테마
FlatDarculaLaf.setup(); // IntelliJ Darcula 스타일
FlatDarkLaf.setup(); // 순수 다크 테마
FlatIntelliJLaf.setup(); // IntelliJ 밝은 테마
2. SwingWorker - UI 블로킹 방지
GUI 앱에서 가장 흔한 실수가 메인 스레드에서 오래 걸리는 작업을 실행하는 것이다.
식당에 비유하면, 홀 직원이 직접 주방에서 요리까지 하는 것과 같다. 손님(사용자)이 주문하면 홀 직원이 주방으로 가서 요리를 완성할 때까지 다른 손님은 아무것도 할 수 없다. 주문도 못 받고, 계산도 못 하고, 심지어 "여기요!" 해도 반응이 없다.
SwingWorker는 주방 직원을 따로 두는 것이다. 홀 직원(UI 스레드)은 계속 손님 응대를 하고, 주방 직원(백그라운드 스레드)이 요리(분석 작업)를 한다.
문제 상황
// ❌ 나쁜 예: UI 스레드에서 직접 실행
btnAnalyze.addActionListener(e -> {
// 이 작업이 10초 걸리면 UI가 10초간 멈춤!
FlowResult result = analyzer.analyze(projectPath);
displayResult(result);
});
버튼을 클릭하면 창이 멈추고, 드래그도 안 되고, 심지어 닫기 버튼도 작동하지 않는다. 사용자는 "프로그램이 죽었나?" 생각하게 된다.
SwingWorker로 해결
// ✅ 좋은 예: SwingWorker로 백그라운드 실행
btnAnalyze.addActionListener(e -> {
new SwingWorker<FlowResult, String>() {
@Override
protected FlowResult doInBackground() throws Exception {
// 백그라운드 스레드에서 실행
publish("Java 소스 파싱 중...");
JavaSourceParser parser = new JavaSourceParser();
List<ParsedClass> classes = parser.parseProject(projectPath);
publish("호출 흐름 분석 중...");
FlowAnalyzer analyzer = new FlowAnalyzer();
return analyzer.analyze(projectPath, classes);
}
@Override
protected void process(List<String> chunks) {
// UI 스레드에서 실행 - 진행 상황 표시
lblStatus.setText(chunks.get(chunks.size() - 1));
}
@Override
protected void done() {
// UI 스레드에서 실행 - 결과 표시
try {
FlowResult result = get();
displayResult(result);
} catch (Exception ex) {
showError(ex.getMessage());
}
}
}.execute();
});
SwingWorker의 핵심 메서드
| 메서드 | 실행 스레드 | 용도 |
|---|---|---|
doInBackground() |
백그라운드 | 시간이 오래 걸리는 작업 |
publish(V...) |
백그라운드 | 중간 결과 전달 |
process(List<V>) |
UI(EDT) | 중간 결과 UI에 표시 |
done() |
UI(EDT) | 최종 결과 처리 |
핵심: doInBackground()에서 publish()로 진행 상황을 보내면, process()가 UI 스레드에서 안전하게 화면을 업데이트한다.
3. JEditorPane + HTML - 유연한 결과 표현
CLI에서는 ANSI 색상 코드로 출력했지만, GUI에서는 더 유연한 방법이 필요하다.
왜 JTextArea가 아닌 JEditorPane인가?
| 컴포넌트 | 색상 지원 | 텍스트 선택 | 복사 |
|---|---|---|---|
| JTextArea | ❌ 단일색만 | ✅ | ✅ |
| JLabel | ✅ HTML 지원 | ❌ | ❌ |
| JEditorPane | ✅ HTML 지원 | ✅ | ✅ |
구현
public class ResultPanel extends JPanel {
private JEditorPane resultPane;
// 레이어별 색상 (VS Code 터미널 참고)
private static final String COLOR_CONTROLLER = "#4EC9B0"; // 청록
private static final String COLOR_SERVICE = "#569CD6"; // 파랑
private static final String COLOR_DAO = "#C586C0"; // 보라
private static final String COLOR_SQL = "#CE9178"; // 주황
private void initializePane() {
resultPane = new JEditorPane();
resultPane.setContentType("text/html");
resultPane.setEditable(false);
// 중요: 시스템 폰트 설정 적용
resultPane.putClientProperty(
JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE
);
resultPane.setFont(new Font("Malgun Gothic", Font.PLAIN, 14));
}
// FlowNode: 호출 흐름 분석 결과를 담는 트리 노드
// - className: Controller/Service/DAO 클래스명
// - methodName: 메서드명
// - children: 하위 호출 노드들
public void displayFlow(FlowNode node) {
StringBuilder html = new StringBuilder();
html.append("<html><body style='font-family: Malgun Gothic;'>");
// 색상으로 레이어 구분 (Controller=청록, Service=파랑, DAO=보라)
html.append("<span style='color: ")
.append(COLOR_CONTROLLER).append("'>")
.append(node.getClassName())
.append("</span>");
html.append("</body></html>");
resultPane.setText(html.toString());
}
}
색상 팔레트 선택
다크 테마에서 가독성 좋은 색상을 찾는 것이 중요하다. VS Code 터미널의 ANSI 색상을 참고했다:
// 다크 테마용 색상 팔레트
COLOR_CONTROLLER = "#4EC9B0" // 청록 - Class/Type 느낌
COLOR_SERVICE = "#569CD6" // 파랑 - Keyword 느낌
COLOR_DAO = "#C586C0" // 보라 - Function 느낌
COLOR_SQL = "#CE9178" // 주황 - String 느낌
COLOR_WARNING = "#F44747" // 빨강 - Error 느낌
4. JSplitPane - 리사이즈 가능한 패널
URL 목록(좌측)과 분석 결과(우측)를 나란히 보여주면서, 사용자가 크기를 조절할 수 있어야 한다.
기본 구조
// 좌측: URL 목록
JList<String> urlList = new JList<>();
JScrollPane leftPanel = new JScrollPane(urlList);
// 우측: 분석 결과
ResultPanel resultPanel = new ResultPanel();
// 분할 패널
JSplitPane splitPane = new JSplitPane(
JSplitPane.HORIZONTAL_SPLIT,
leftPanel,
resultPanel
);
splitPane.setDividerLocation(250); // 초기 분할 위치
주의: visibility가 아닌 dividerLocation으로 제어
처음에는 분석 전에 좌측 패널을 숨기려고 setVisible(false)를 사용했지만, 작동하지 않았다.
// ❌ 작동하지 않음
leftPanel.setVisible(false);
leftPanel.setVisible(true);
// ✅ 정상 동작
splitPane.setDividerLocation(0); // 숨기기
splitPane.setDividerLocation(250); // 표시
JSplitPane은 visibility가 아닌 divider 위치로 패널 크기를 제어하도록 설계되어 있다.
5. 트러블슈팅 모음
Issue #009: JTree 한글 깨짐
문제: 트리 뷰에서 한글이 □□□로 표시됨
원인: 기본 폰트(Consolas)가 한글을 지원하지 않음
해결:
// ❌ 한글 미지원
tree.setFont(new Font("Consolas", Font.PLAIN, 14));
// ✅ 한글 지원
tree.setFont(new Font("Malgun Gothic", Font.PLAIN, 14));
Issue #010: 텍스트 선택/복사 불가
문제: JLabel에 HTML로 색상을 넣었더니 텍스트 선택이 안 됨
원인: JLabel은 텍스트 선택 기능을 지원하지 않음
해결: JEditorPane으로 변경
// ❌ 선택 불가
JLabel label = new JLabel("<html><font color='blue'>텍스트</font></html>");
// ✅ 선택 가능
JEditorPane pane = new JEditorPane();
pane.setContentType("text/html");
pane.setEditable(false);
pane.setText("<html><font color='blue'>텍스트</font></html>");
Issue #011: 창 닫아도 프로세스 종료 안 됨
문제: X 버튼으로 창을 닫아도 Java 프로세스가 백그라운드에서 계속 실행
원인: setDefaultCloseOperation만으로는 완전 종료되지 않는 경우가 있음
해결:
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 추가: WindowListener로 확실하게 종료
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
Issue #012: HTML에서 박스 문자 정렬 불일치
문제: CLI에서 박스 문자(├, └, │)로 트리를 그렸는데, HTML로 변환하니 정렬이 어긋남
원인: HTML에서는 고정폭 폰트를 써도 한글과 특수문자 폭이 다름
해결: <table> + CSS border로 트리 구조 표현
<table style='border-collapse: collapse;'>
<tr>
<td style='border-left: 1px solid #666; padding-left: 10px;'>
Service 메서드
</td>
</tr>
</table>
Issue #013: 분석 요약 정렬 어색
문제: 라벨과 숫자 사이 간격이 너무 넓거나 불규칙함
원인: GridLayout 사용 시 컨테이너 전체 폭을 균등 분배
해결: Leader Dots (점선 리더) 패턴 적용
클래스: .............. 4개
Controller: ........... 1개
Service: .............. 2개
커스텀 JPanel에서 paintComponent()로 점선을 그린다.
Ctrl+휠 폰트 크기 조절
사용자 편의를 위해 Ctrl+마우스 휠로 폰트 크기를 조절할 수 있게 했다.
resultPane.addMouseWheelListener(e -> {
if (e.isControlDown()) {
int rotation = e.getWheelRotation();
if (rotation < 0) {
// 휠 위로 = 폰트 크기 증가
fontSize = Math.min(24, fontSize + 1);
} else {
// 휠 아래로 = 폰트 크기 감소
fontSize = Math.max(9, fontSize - 1);
}
resultPane.setFont(new Font("Malgun Gothic", Font.PLAIN, fontSize));
e.consume(); // 스크롤 이벤트 차단
}
});
e.consume() 호출이 중요하다. 이걸 빼먹으면 폰트 크기도 바뀌고 스크롤도 같이 된다.
마치며
Swing은 오래된 기술이지만, FlatLaf + SwingWorker + JEditorPane 조합으로 충분히 모던하고 반응성 좋은 UI를 만들 수 있다.
핵심 정리
| 문제 | 해결책 |
|---|---|
| 촌스러운 UI | FlatLaf 한 줄로 해결 |
| UI 블로킹 | SwingWorker |
| 색상 + 텍스트 선택 | JEditorPane + HTML |
| 리사이즈 패널 | JSplitPane |
| 한글 깨짐 | 맑은 고딕 폰트 |
삽질 포인트
- JSplitPane의 setVisible(): 왜 안 되지? 한참 헤매다가 공식 문서를 봤더니
setDividerLocation()을 쓰라고 되어 있었다. 문서를 먼저 읽자... - JLabel vs JEditorPane: 처음에 JLabel로 색상을 넣었다가 "복사가 안 돼요"라는 피드백에 전면 수정. 요구사항을 먼저 정리했으면 삽질을 줄일 수 있었다.
- 한글 폰트: Consolas가 예뻐서 썼는데 한글이 □□□로 나왔다. 국제화(i18n)를 항상 고려해야 한다는 교훈.
이 글을 쓰며 배운점
- EDT(Event Dispatch Thread) 이해: Swing의 스레드 모델을 제대로 이해하게 됐다. UI 작업은 반드시 EDT에서, 무거운 작업은 백그라운드에서.
- "왜 안 되지?" → "문서에 뭐라고 되어 있지?": JSplitPane 삽질 이후로 문제가 생기면 공식 문서부터 찾아보는 습관이 생겼다.
- 사용자 관점 생각하기: CLI만 있을 때는 "명령어 치면 되잖아"였는데, GUI를 만들고 나니 "버튼 하나로 끝"의 가치를 알게 됐다.
'토스 러너스하이 2기 > 기술' 카테고리의 다른 글
| Gson으로 세션 영속성 구현하기 - 앱을 닫아도 분석 결과가 살아있다 (0) | 2026.01.17 |
|---|---|
| jpackage로 배포하기 - 폐쇄망에서 Java 없이 실행하기 (0) | 2026.01.12 |
| 분석 결과를 Excel로 정리하기 - Apache POI 활용기 (0) | 2026.01.10 |
| 분석 결과를 어떻게 보여줄까? - CLI 출력 구현기 (1) | 2026.01.07 |
| 정적 분석의 한계 - 해결할 수 없는 것들 (0) | 2026.01.04 |