분석 결과를 어떻게 보여줄까? - CLI 출력 구현기

2026. 1. 7. 23:19·토스 러너스하이 2기/기술

분석은 됐는데, 결과를 어떻게 보여주지?
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는 그 위에 얹기만 하면 된다.


이 글에서 다루는 것

  1. CLI란 무엇인가? (CLI vs GUI, 왜 CLI를 먼저 만들었나)
  2. Picocli 선택 이유와 사용법
  3. 프로젝트에 적용하기
  4. 박스 문자로 트리 구조 만들기
  5. ANSI 색상 코드 사용법
  6. Windows 콘솔 인코딩 문제 해결 (삽질 주의)
  7. 한글 폭 계산 문제

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 네이티브 - ❌ ✅ 지원

결정적 이유:

  1. 어노테이션 기반 - 선언적이고 읽기 쉽다
  2. 자동 도움말 - --help 한 줄로 끝
  3. 타입 안전 - String을 Path로 자동 변환
  4. 확장성 - 나중에 네이티브 컴파일도 가능

"왜 이 기술을 선택했는가?"에 대한 답이 명확해야 한다.
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로 계산

삽질 포인트 정리

  1. Java에서 chcp 실행해도 안 됨 - 자식 프로세스 코드 페이지만 바뀜
  2. Picocli가 System.out 직접 사용 - cmd.setOut()으로 스트림 교체 필요
  3. 한글 정렬 어긋남 - String.length() 대신 표시 폭 계산 필요

이 글을 쓰며 성장한 점

  1. 문제를 작게 나누기: 인코딩 문제를 4단계로 분리해서 각개격파
  2. 라이브러리 선택 기준 세우기: "왜?"라는 질문에 답할 수 있어야 좋은 선택
  3. 삽질의 가치: 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
'토스 러너스하이 2기/기술' 카테고리의 다른 글
  • Swing으로 모던한 GUI 만들기 - CLI를 넘어서
  • 분석 결과를 Excel로 정리하기 - Apache POI 활용기
  • 정적 분석의 한계 - 해결할 수 없는 것들
  • DAO에서 SQL까지 - XML 파싱으로 연결하기
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
분석 결과를 어떻게 보여줄까? - CLI 출력 구현기
상단으로
목차

    티스토리툴바