분석은 됐는데, 결과를 어떻게 보여주지?
GUI보다 CLI를 먼저 만든 이유, 그리고 박스 문자, ANSI 색상, Windows 인코딩과의 전쟁을 정리한다.
3줄 요약
- 콘솔 출력은 "터미널용 신택스 하이라이팅"이다. 트리 문자로 구조를, ANSI 색상으로 레이어를 구분한다
- Picocli로 CLI 옵션을 어노테이션 하나로 처리할 수 있다
- Windows 콘솔 인코딩 문제는 UTF-8 PrintStream + 배치 래퍼로 해결했다
1. 들어가며
이전 글에서 정적 분석의 한계를 다뤘다. 분석은 완성됐는데, 결과를 어떻게 보여줄까?
분석은 됐는데, 결과를 어떻게 보여주지?
지금까지 만든 것:
- JavaParser로 Java 코드 파싱
- 재귀 탐색으로 호출 흐름 추적
- iBatis/MyBatis XML에서 SQL 추출
분석 로직은 완성됐다. 그런데 결과가 이렇게 나오면?
UserController.getList calls UserService.findAll
UserService.findAll calls UserDAO.selectList
UserDAO.selectList executes selectUserList
정보는 다 있는데... 이걸 누가 읽어?

비유: 메모장 vs VS Code
코드를 메모장으로 열어본 적 있는가?
public class UserController {
private UserService userService;
public List<User> getList() {
return userService.findAll();
}
}
모든 글자가 똑같은 검은색. 어디가 클래스고, 어디가 메서드인지 한눈에 안 들어온다.
같은 코드를 VS Code로 열면?
public,class,return→ 보라색 (키워드)UserController,UserService→ 초록색 (클래스)"문자열"→ 주황색// 주석→ 회색
색상만으로 구조가 보인다. 이게 신택스 하이라이팅의 힘이다.
콘솔 출력도 마찬가지다:
- 트리 문자 (
├── └──) = 들여쓰기처럼 계층 구분 - ANSI 색상 = 신택스 하이라이팅처럼 레이어 구분
- 인코딩 = 한글 폰트가 깨지지 않게
결국 내가 하려는 건 "터미널용 신택스 하이라이팅"을 만드는 것이다.
내가 원했던 출력
┌──────────────────────────────────────────────────────┐
│ Code Flow Tracer - 호출 흐름 분석 결과 │
└──────────────────────────────────────────────────────┘
[GET] /api/user/list
├── [Controller] UserController.getList()
│ └── [Service] UserService.findAll()
│ └── [DAO] UserDAO.selectList()
│ → SQL: User_SQL.xml / selectUserList
트리 구조로 한눈에 흐름이 보이고, 색상으로 레이어 구분이 되면 좋겠다.
왜 GUI보다 CLI를 먼저 만들었나?
분석 로직이 완성되고 나서, 결과를 보여줄 방법이 필요했다.
GUI와 CLI 중 뭘 먼저 만들까?
CLI를 먼저 선택한 이유:
| 이유 | 설명 |
|---|---|
| 빠른 피드백 | 코드 수정 → 빌드 → 터미널에서 바로 확인. GUI는 창 띄우고 버튼 누르고... 번거롭다 |
| 디버깅 용이 | 결과가 이상하면? 터미널 출력 보면서 바로 추적 |
| 파이프라인 검증 | Parser → Analyzer → Output 흐름이 제대로 동작하는지 CLI로 먼저 확인 |
| 폐쇄망 환경 | 고객사 서버에 SSH로 접속해서 java -jar 한 줄로 분석 가능해야 함 |
| 자동화 | CI/CD에서 GUI는 못 쓴다. CLI면 스크립트 한 줄로 끝 |
결국 GUI는 "CLI를 예쁘게 감싼 것"이 됐다.
CLI가 잘 동작하면 GUI는 그 위에 얹기만 하면 된다.
이 글에서 다루는 것
- CLI란 무엇인가? (CLI vs GUI, 왜 CLI를 먼저 만들었나)
- Picocli 선택 이유와 사용법
- 프로젝트에 적용하기
- 박스 문자로 트리 구조 만들기
- ANSI 색상 코드 사용법
- Windows 콘솔 인코딩 문제 해결 (삽질 주의)
- 한글 폭 계산 문제
2. CLI란 무엇인가?
CLI vs GUI
프로그램과 사용자가 소통하는 방식은 크게 두 가지다.
| CLI (Command Line Interface) | GUI (Graphical User Interface) | |
|---|---|---|
| 입력 | 키보드로 명령어 입력 | 마우스로 버튼 클릭 |
| 출력 | 텍스트 | 창, 버튼, 이미지 |
| 예시 | git commit -m "메시지" |
GitHub Desktop 앱 |
| 학습 곡선 | 명령어 암기 필요 | 직관적, 바로 사용 가능 |
| 자동화 | 스크립트로 자동화 쉬움 | 어려움 |
| 서버 환경 | SSH로 원격 사용 가능 | 불가능 (화면 필요) |
개발자 도구는 대부분 CLI를 먼저 만든다. 왜?
# CLI는 이런 게 가능하다
for project in ./projects/*; do
java -jar analyzer.jar -p $project -o results/
done
GUI로는 100개 프로젝트를 하나씩 클릭해야 한다. CLI는 반복문 한 줄.
Java에서 CLI를 만드는 방법들
Java로 CLI를 만들려면 args를 파싱해야 한다.
방법 1: 직접 파싱
public static void main(String[] args) {
for (int i = 0; i < args.length; i++) {
if ("-p".equals(args[i])) {
path = args[++i];
}
}
}
단점: 옵션이 늘어날수록 코드가 복잡해진다. --help도 직접 구현해야 한다.
방법 2: Apache Commons CLI
Options options = new Options();
options.addOption("p", "path", true, "프로젝트 경로");
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(options, args);
String path = cmd.getOptionValue("p");
단점: 설정 코드가 장황하다. 2002년에 만들어져서 모던하지 않다.
방법 3: Picocli ← 내가 선택한 것
@Option(names = {"-p", "--path"}, description = "프로젝트 경로")
private Path path;
어노테이션 한 줄로 끝. 가장 모던하고 간결하다.
왜 Picocli를 선택했나?
| 기준 | 직접 파싱 | Commons CLI | Picocli |
|---|---|---|---|
| 코드량 | 많음 | 보통 | 적음 |
--help 자동 생성 |
❌ | ⚠️ 수동 | ✅ 자동 |
| 타입 변환 | 직접 구현 | 직접 구현 | 자동 (String→Path) |
| 컬러 출력 | 직접 구현 | ❌ | ✅ 지원 |
| 서브커맨드 | 복잡 | 복잡 | ✅ 쉬움 |
| GraalVM 네이티브 | - | ❌ | ✅ 지원 |
결정적 이유:
- 어노테이션 기반 - 선언적이고 읽기 쉽다
- 자동 도움말 -
--help한 줄로 끝 - 타입 안전 -
String을Path로 자동 변환 - 확장성 - 나중에 네이티브 컴파일도 가능
"왜 이 기술을 선택했는가?"에 대한 답이 명확해야 한다.
Picocli는 "적은 코드로 많은 기능"을 주는 라이브러리다.
3. Picocli 사용법
Picocli란?
Picocli(피코클리)는 Java CLI 애플리케이션을 만드는 라이브러리다.
피코(pico) = 10^-12 (아주 작은)
CLI = Command Line Interface
이름처럼 아주 적은 코드로 CLI를 만들 수 있다.
의존성 추가
// build.gradle
dependencies {
implementation 'info.picocli:picocli:4.7.5'
}
기본 구조
@Command(
name = "mytool", // 명령어 이름
mixinStandardHelpOptions = true, // --help, --version 자동 생성
version = "1.0.0",
description = "내 도구 설명"
)
public class MyTool implements Callable<Integer> {
@Option(names = {"-p", "--path"}, description = "경로")
private Path path;
@Override
public Integer call() {
// 여기에 실제 로직
System.out.println("경로: " + path);
return 0; // 종료 코드 (0 = 성공)
}
public static void main(String[] args) {
int exitCode = new CommandLine(new MyTool()).execute(args);
System.exit(exitCode);
}
}
핵심 포인트:
@Command- 명령어 메타정보@Option- 옵션 정의 (어노테이션만 붙이면 자동 파싱)Callable<Integer>- 반환값이 종료 코드mixinStandardHelpOptions = true- 이 한 줄로--help,--version자동 생성
실행 예시
# 도움말 (자동 생성됨)
$ java -jar mytool.jar --help
Usage: mytool [-hV] [-p=<path>]
내 도구 설명
-h, --help Show this help message and exit.
-p, --path=<path> 경로
-V, --version Print version information and exit.
# 실제 사용
$ java -jar mytool.jar -p /home/user/project
경로: /home/user/project
--help 메시지를 한 글자도 작성하지 않았는데 자동으로 만들어진다.
4. 프로젝트에 적용하기
이제 실제 프로젝트에 어떻게 적용했는지 보자.
Code Flow Tracer의 CLI 옵션
public static void main(String[] args) {
String path = null;
String style = "normal";
for (int i = 0; i < args.length; i++) {
if ("-p".equals(args[i]) || "--path".equals(args[i])) {
path = args[++i]; // ArrayIndexOutOfBounds 조심...
} else if ("-s".equals(args[i])) {
style = args[++i];
} else if ("--help".equals(args[i])) {
printHelp(); // 이것도 직접 구현해야 함
return;
}
}
// 필수 옵션 검증도 직접...
}
옵션이 늘어날수록 코드가 복잡해진다. --help 메시지도 직접 관리해야 하고... 귀찮다.
Picocli의 마법
@Command(
name = "code-flow-tracer",
mixinStandardHelpOptions = true, // --help, --version 자동 생성!
version = "1.0.0",
description = "레거시 코드 호출 흐름 분석 도구"
)
public class Main implements Callable<Integer> {
@Option(names = {"-p", "--path"}, description = "분석할 프로젝트 경로")
private Path projectPath;
@Option(names = {"-s", "--style"},
description = "출력 스타일: compact, normal, detailed",
defaultValue = "normal")
private String style;
@Option(names = {"--no-color"}, description = "색상 출력 비활성화")
private boolean noColor;
@Override
public Integer call() {
// projectPath, style, noColor가 이미 파싱되어 있음!
// 비즈니스 로직만 작성하면 됨
}
}
어노테이션만 붙이면 옵션 파싱, 타입 변환, 기본값, 도움말이 다 된다.
자동 생성되는 도움말
$ java -jar code-flow-tracer.jar --help
Usage: code-flow-tracer [-hV] [--no-color] [--gui] [-p=<path>] [-s=<style>]
레거시 코드 호출 흐름 분석 도구
-h, --help Show this help message and exit.
-p, --path=<path> 분석할 프로젝트 경로
-s, --style=<style> 출력 스타일: compact, normal, detailed
--no-color 색상 출력 비활성화
-V, --version Print version information and exit.
mixinStandardHelpOptions = true 한 줄로 --help, --version이 자동 생성된다.
5. 박스 문자로 트리 그리기
VS Code에서 들여쓰기로 코드 구조를 보여주듯, 터미널에서는 ├── └──로 계층을 표현한다.
핵심 문자들
// 트리 출력용 문자
private static final String TREE_BRANCH = "├── "; // 중간 노드
private static final String TREE_LAST = "└── "; // 마지막 노드
private static final String TREE_VERTICAL = "│ "; // 세로 연결선
private static final String TREE_SPACE = " "; // 빈 공간
이 문자들을 조합하면:
├── 첫 번째
│ ├── 자식 1
│ └── 자식 2
└── 두 번째
└── 자식 3
재귀적으로 트리 출력하기
private void printNode(FlowNode node, String prefix, boolean isLast) {
// 1. 현재 노드 출력
String connector = isLast ? TREE_LAST : TREE_BRANCH; // └── 또는 ├──
out.println(prefix + connector + formatNode(node));
// 2. 자식 노드들을 위한 prefix 준비
String childPrefix = prefix + (isLast ? TREE_SPACE : TREE_VERTICAL);
// " " 또는 "│ "
// 3. 자식 노드 재귀 출력
List<FlowNode> children = node.getChildren();
for (int i = 0; i < children.size(); i++) {
boolean childIsLast = (i == children.size() - 1);
printNode(children.get(i), childPrefix, childIsLast);
}
}
핵심 아이디어:
isLast가 true면└──, false면├──- 자식에게 전달할 prefix: 부모가 마지막이면 공백(
), 아니면 세로선(│)
세로선이 정확하게 이어지면서 계층 구조가 명확해진다.
6. ANSI 색상 코드
VS Code에서 키워드는 보라색, 클래스는 초록색으로 표시되듯, 터미널에서도 ANSI 코드로 색상을 입힐 수 있다.
ANSI 이스케이프 시퀀스
터미널에서 색상을 표시하는 표준 방식이다.
// ANSI 색상 코드
private static final String RESET = "\u001B[0m"; // 색상 초기화
private static final String GREEN = "\u001B[32m";
private static final String BLUE = "\u001B[34m";
private static final String PURPLE = "\u001B[35m";
private static final String RED = "\u001B[31m";
사용 방법
private String color(String text, String colorCode) {
if (useColors) {
return colorCode + text + RESET; // 색상 시작 + 텍스트 + 초기화
}
return text;
}
// 사용 예시
out.println(color("[Controller]", GREEN) + " UserController");
주의: 반드시 RESET으로 색상을 초기화해야 한다.
안 그러면 이후 모든 출력이 그 색상이 된다. (경험담)
레이어별 색상 적용
switch (type) {
case CONTROLLER: return color(tag, GREEN); // 녹색
case SERVICE: return color(tag, BLUE); // 파란색
case DAO: return color(tag, PURPLE); // 보라색
}
HTTP 메서드도 REST 컨벤션을 따라:
- GET → 녹색 (안전)
- POST → 노란색 (주의)
- DELETE → 빨간색 (위험)
7. Windows 인코딩 문제 (삽질의 기록)
메모장에서 한글 파일을 열었더니 깨지는 경험, 해본 적 있을 것이다. Windows 콘솔도 마찬가지다.
문제 상황
Windows에서 실행하면:
PS C:\> java -jar code-flow-tracer.jar --help
# 출력 (깨짐)
?덇굅??肄붾뱶 ?먮쫫 遺꾩꽍 ?꾧뎄 - Controller ??Service ??DAO
박스 문자도 깨지고, 한글도 깨진다. 뭐가 문제야?
원인: 인코딩 불일치
[인코딩 체인]
Java 프로그램 (UTF-8로 문자열 생성)
↓
System.out (기본 인코딩으로 바이트 변환)
↓
Windows 콘솔 (CP949로 바이트 해석)
↓
깨진 문자 출력 💀
Windows 콘솔의 기본 인코딩은 CP949(한글) 또는 CP1252(영문).
Java가 UTF-8로 출력하면 콘솔이 잘못 해석한다.
메모장 비유로 돌아가면: UTF-8 파일을 ANSI로 열었을 때 깨지는 것과 같다.
해결 1: UTF-8 PrintStream
// 기존: System.out 직접 사용
System.out.println("한글 출력"); // 깨짐
// 개선: UTF-8 PrintStream 사용
PrintStream utf8Out = new PrintStream(System.out, true, StandardCharsets.UTF_8);
utf8Out.println("한글 출력"); // 정상 (IDE에서)
하지만 실제 Windows CMD/PowerShell에서는 여전히 깨진다.
콘솔 자체의 코드 페이지가 CP949이기 때문.
해결 2: 스마트 인코딩 감지 (Java 17+)
private static PrintStream getSmartOutputStream() {
Console console = System.console();
if (console == null) {
// IDE에서 실행 중 → UTF-8
return new PrintStream(System.out, true, StandardCharsets.UTF_8);
}
// 실제 터미널 → 콘솔 인코딩 감지
Charset consoleCharset = console.charset(); // Java 17+
return new PrintStream(System.out, true, consoleCharset);
}
System.console()이 null이면 IDE에서 실행 중이라는 의미.
해결 3: 배치 파일 래퍼
Windows CMD에서 완벽하게 동작하려면:
@echo off
chcp 65001 > nul 2>&1
java -jar code-flow-tracer.jar %*
삽질 포인트: Java 코드에서 Runtime.exec("chcp 65001") 하면 안 됨!
왜? 자식 프로세스의 코드 페이지만 바뀌고, 부모 콘솔은 그대로.
배치 파일에서 실행해야 같은 콘솔에서 코드 페이지가 바뀐다.
해결 4: Picocli 스트림 설정
우리가 만든 ConsoleOutput은 UTF-8 PrintStream을 쓰니까 괜찮다.
그런데 Picocli의 --help가 깨진다!
원인: Picocli는 System.out을 직접 사용.
public static void main(String[] args) {
PrintStream smartOut = getSmartOutputStream();
CommandLine cmd = new CommandLine(new Main());
cmd.setOut(new PrintWriter(smartOut, true)); // 여기!
cmd.setErr(new PrintWriter(smartOut, true));
int exitCode = cmd.execute(args);
System.exit(exitCode);
}
라이브러리도 우리 스트림을 쓰게 만들어야 한다.
8. 한글 폭 계산
또 다른 문제
┌──────────────────────────────────────────────────────┐
│ Code Flow Tracer - 호출 흐름 분석 결과 │
└──────────────────────────────────────────────────────┘
제목을 박스 안에서 가운데 정렬하고 싶다.
String title = " Code Flow Tracer - 호출 흐름 분석 결과 ";
int padding = (boxWidth - title.length()) / 2; // 틀림!
title.length()는 문자 개수를 반환한다.
그런데 터미널에서 한글은 2칸을 차지한다!
해결: 표시 폭 계산
private int getDisplayWidth(String text) {
int width = 0;
for (char c : text.toCharArray()) {
if (isWideChar(c)) {
width += 2; // 한글, 한자 등은 2칸
} else {
width += 1; // ASCII는 1칸
}
}
return width;
}
private boolean isWideChar(char c) {
Character.UnicodeBlock block = Character.UnicodeBlock.of(c);
return block == Character.UnicodeBlock.HANGUL_SYLLABLES
|| block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
// ... 기타 CJK 문자들
}
Character.UnicodeBlock으로 한글/한자/일본어를 판별하고 폭 2로 계산.
9. 프로젝트에서 활용
출력 스타일 3가지
| 스타일 | 용도 | 정보량 |
|---|---|---|
| COMPACT | 빠른 확인 | 클래스.메서드만 |
| NORMAL | 일반 사용 | 타입 + URL + 인터페이스 |
| DETAILED | 상세 분석 | 파라미터, SQL 전체 |
[COMPACT]
├── UserController.getList()
│ └── UserService.findAll()
[NORMAL]
[GET] /api/user/list
├── [Controller] UserController.getList()
│ └── [Service] UserService.findAll() ← UserService
다중 구현체 경고
├── [Service] UserServiceImpl.findAll() ← UserService (외 UserServiceV2, V3)
정적 분석의 한계(어떤 구현체가 주입되는지 모름)를 사용자에게 명확히 알린다.
10. 마치며
이 글에서 배운 것
| 주제 | 핵심 |
|---|---|
| Picocli | 어노테이션 하나로 CLI 옵션 처리, --help 자동 생성 |
| 박스 문자 | ├── └── │로 트리 구조 표현, 재귀 함수로 출력 |
| ANSI 색상 | \u001B[32m + 텍스트 + \u001B[0m, 반드시 RESET |
| 인코딩 | UTF-8 PrintStream + 배치 래퍼 + Picocli 스트림 설정 |
| 한글 폭 | Character.UnicodeBlock으로 CJK 판별, 폭 2로 계산 |
삽질 포인트 정리
- Java에서 chcp 실행해도 안 됨 - 자식 프로세스 코드 페이지만 바뀜
- Picocli가 System.out 직접 사용 -
cmd.setOut()으로 스트림 교체 필요 - 한글 정렬 어긋남 -
String.length()대신 표시 폭 계산 필요
이 글을 쓰며 성장한 점
- 문제를 작게 나누기: 인코딩 문제를 4단계로 분리해서 각개격파
- 라이브러리 선택 기준 세우기: "왜?"라는 질문에 답할 수 있어야 좋은 선택
- 삽질의 가치: chcp가 안 됐던 이유를 파고들며 프로세스 격리 개념을 배움
'토스 러너스하이 2기 > 기술' 카테고리의 다른 글
| Swing으로 모던한 GUI 만들기 - CLI를 넘어서 (1) | 2026.01.12 |
|---|---|
| 분석 결과를 Excel로 정리하기 - Apache POI 활용기 (0) | 2026.01.10 |
| 정적 분석의 한계 - 해결할 수 없는 것들 (0) | 2026.01.04 |
| DAO에서 SQL까지 - XML 파싱으로 연결하기 (0) | 2025.12.29 |
| 호출 흐름을 어떻게 따라가나? - 재귀와 백트래킹 (0) | 2025.12.26 |