Gson으로 세션 영속성 구현하기 - 앱을 닫아도 분석 결과가 살아있다

2026. 1. 17. 21:17·토스 러너스하이 2기/기술

"분석 완료! 결과 확인! ...앱 종료. 다시 실행하면? 처음부터 다시?"
그게 싫었다. 분석 결과를 저장해서 다시 열어도 그대로 보고 싶었다.
Gson으로 세션 영속성을 구현하면서 배운 것들.


3줄 요약

  • Gson은 한 줄로 Java 객체를 JSON으로 변환하는 가벼운 라이브러리다
  • LocalDateTime 같은 Java 8+ 타입은 TypeAdapter를 직접 구현해야 한다
  • 저장 방식을 통합하지 않으면 이중 저장이라는 기술 부채가 생긴다

1. 들어가며

이전 글에서 Swing GUI를 완성했다. 폴더 선택, 분석 실행, 결과 확인까지 깔끔하게 동작했다.

그런데 하나 빠진 게 있었다.

사용 시나리오:
1. 앱 실행
2. 프로젝트 폴더 선택
3. 분석 실행 (30초 소요)
4. 결과 확인
5. 앱 종료

6. 다음 날, 앱 다시 실행
7. "분석 결과가 없습니다" ← 어제 분석한 건?!

분석에 30초가 걸리는 프로젝트라면? 매번 다시 분석하는 건 말이 안 된다.


비유: 게임 세이브

구분 세이브 없는 게임 세이브 있는 게임
종료 후 처음부터 다시 이어하기
사용자 경험 절망 편안

세션 영속성은 게임의 세이브 기능과 같다. 앱을 닫아도 상태가 유지되어야 한다.


뭘 저장해야 하나?

세션에 저장해야 할 데이터를 정리했다:

항목 설명 예시
projectPath 마지막 분석한 프로젝트 경로 C:\workspace\legacy-app
analyzedAt 분석 시간 2025-12-30T14:30:00
flowResult 분석 결과 (핵심!) Controller→Service→DAO 호출 트리
recentPaths 최근 열었던 프로젝트 목록 최대 10개
urlFilter URL 필터 값 /api/*
outputStyle 출력 스타일 compact, normal, detailed

핵심은 flowResult다. 이 객체 하나에 전체 분석 결과가 담겨있다.


2. 라이브러리 선택: Gson vs Jackson

Java에서 JSON 직렬화 라이브러리는 크게 두 가지가 있다:

항목 Gson Jackson
개발사 Google FasterXML
크기 ~300KB ~1.5MB (core만)
설정 거의 없음 많음 (ObjectMapper 설정)
성능 보통 빠름
학습 곡선 낮음 높음

Gson을 선택한 이유:

  1. 목적에 적합: 세션 저장은 고성능이 필요 없다. 앱 시작/종료 시 한 번씩만 호출.
  2. 단순함: 설정 없이 바로 사용 가능. new Gson().toJson(obj) 끝.
  3. 가벼움: 300KB면 충분. Jackson은 의존성이 복잡하다.
// build.gradle
dependencies {
    implementation 'com.google.code.gson:gson:2.10.1'
}

3. 기본 사용법

직렬화 (객체 → JSON)

Gson gson = new Gson();

// 간단한 객체
User user = new User("Kim", 25);
String json = gson.toJson(user);
// {"name":"Kim","age":25}

// 컬렉션도 가능
List<String> paths = Arrays.asList("/path1", "/path2");
String pathsJson = gson.toJson(paths);
// ["/path1","/path2"]

역직렬화 (JSON → 객체)

String json = "{\"name\":\"Kim\",\"age\":25}";
User user = gson.fromJson(json, User.class);
// user.getName() → "Kim"

정말 간단하다. 한 줄이면 끝.


보기 좋게 출력 (PrettyPrinting)

// 기본: 한 줄로 압축
{"projectPath":"/path","analyzedAt":"2025-12-30T14:30:00"}

// PrettyPrinting 적용
Gson gson = new GsonBuilder()
    .setPrettyPrinting()
    .create();
{
  "projectPath": "/path",
  "analyzedAt": "2025-12-30T14:30:00",
  "flowResult": {
    "flows": [...]
  }
}

세션 파일을 텍스트 에디터로 열어볼 때 보기 좋다. 디버깅에 필수.


4. 트러블슈팅: LocalDateTime 직렬화 문제

문제 발생

세션 데이터에 분석 시간을 저장하려고 했다:

public class SessionData {
    private LocalDateTime analyzedAt;  // 분석 시간
    // ...
}

저장은 됐는데... 불러올 때 에러가 발생했다.

java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING

원인: Gson은 Java 8 날짜를 모른다

Gson은 Java 8 이전에 만들어졌다. LocalDateTime, LocalDate 같은 Java 8 날짜 API를 기본 지원하지 않는다.

// Gson이 LocalDateTime을 직렬화하면
{
  "analyzedAt": {
    "date": { "year": 2025, "month": 12, "day": 30 },
    "time": { "hour": 14, "minute": 30, ... }
  }
}
// 복잡한 중첩 객체로 변환됨!

역직렬화할 때 이 구조를 다시 LocalDateTime으로 복원하지 못한다.

해결: TypeAdapter 구현

private static class LocalDateTimeAdapter extends TypeAdapter<LocalDateTime> {
    private static final DateTimeFormatter FORMATTER =
        DateTimeFormatter.ISO_LOCAL_DATE_TIME;

    @Override
    public void write(JsonWriter out, LocalDateTime value) throws IOException {
        if (value == null) {
            out.nullValue();
        } else {
            out.value(FORMATTER.format(value));  // "2025-12-30T14:30:00"
        }
    }

    @Override
    public LocalDateTime read(JsonReader in) throws IOException {
        String value = in.nextString();
        if (value == null || value.isEmpty()) {
            return null;
        }
        return LocalDateTime.parse(value, FORMATTER);
    }
}

핵심 아이디어:

  • 직렬화: LocalDateTime → ISO 문자열 ("2025-12-30T14:30:00")
  • 역직렬화: ISO 문자열 → LocalDateTime.parse()
Gson gson = new GsonBuilder()
    .setPrettyPrinting()
    .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
    .create();

이제 깔끔하게 저장된다:

{
  "projectPath": "/workspace/legacy-app",
  "analyzedAt": "2025-12-30T14:30:00",
  "flowResult": { ... }
}

5. 저장 경로 설계

어디에 저장할까?

위치 장점 단점
설치 폴더 (Program Files) 앱과 함께 권한 문제, UAC
현재 작업 폴더 간단 어디서 실행하냐에 따라 달라짐
사용자 홈 폴더 권한 없음, 일관됨 직접 찾아야 함

사용자 홈 폴더를 선택했다:

// ~/.code-flow-tracer/session.json
private static final Path SESSION_DIR = Paths.get(
    System.getProperty("user.home"), ".code-flow-tracer");
private static final Path SESSION_FILE = SESSION_DIR.resolve("session.json");

왜 홈 폴더인가?

  1. 권한 문제 없음: Program Files는 관리자 권한 필요
  2. 크로스 플랫폼: Windows, macOS, Linux 모두 user.home 사용 가능
  3. 관례: .config/, .local/, .앱이름/ 형태가 일반적
Windows: C:\Users\사용자\.code-flow-tracer\session.json
macOS:   /Users/사용자/.code-flow-tracer/session.json
Linux:   /home/사용자/.code-flow-tracer/session.json

6. SessionManager 구현

전체 구조

public class SessionManager {

    private static final Path SESSION_DIR = Paths.get(
        System.getProperty("user.home"), ".code-flow-tracer");
    private static final Path SESSION_FILE = SESSION_DIR.resolve("session.json");

    private final Gson gson;

    public SessionManager() {
        this.gson = new GsonBuilder()
            .setPrettyPrinting()
            .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
            .create();
    }

    // 세션 저장
    public boolean saveSession(SessionData data) { ... }

    // 세션 로드 (분석 결과 포함)
    public SessionData loadSession() { ... }

    // 설정만 로드 (분석 결과 없어도 됨)
    public SessionData loadSettings() { ... }
}

저장 메서드

public boolean saveSession(SessionData data) {
    if (data == null) {
        log.warn("세션 데이터가 null입니다.");
        return false;
    }

    try {
        // 디렉토리 생성 (없으면)
        if (!Files.exists(SESSION_DIR)) {
            Files.createDirectories(SESSION_DIR);
            log.info("세션 디렉토리 생성: {}", SESSION_DIR);
        }

        // JSON으로 직렬화 및 저장
        String json = gson.toJson(data);
        Files.writeString(SESSION_FILE, json, StandardCharsets.UTF_8);

        log.info("세션 저장 완료: {} ({} flows)",
            SESSION_FILE,
            data.getFlowResult() != null ? data.getFlowResult().getFlows().size() : 0);
        return true;

    } catch (IOException e) {
        log.error("세션 저장 실패: {}", e.getMessage(), e);
        return false;
    }
}

포인트:

  • 디렉토리가 없으면 자동 생성 (createDirectories)
  • UTF-8 인코딩 명시 (한글 경로 대비)
  • 실패해도 앱이 죽지 않게 try-catch

로드 메서드

public SessionData loadSession() {
    if (!Files.exists(SESSION_FILE)) {
        log.debug("세션 파일 없음: {}", SESSION_FILE);
        return null;
    }

    try {
        String json = Files.readString(SESSION_FILE, StandardCharsets.UTF_8);
        SessionData data = gson.fromJson(json, SessionData.class);

        if (data != null && data.isValid()) {
            log.info("세션 로드 완료: {} ({} flows)",
                data.getProjectPath(),
                data.getFlowResult().getFlows().size());
            return data;
        } else {
            log.warn("유효하지 않은 세션 데이터");
            return null;
        }

    } catch (Exception e) {
        log.error("세션 로드 실패: {}", e.getMessage(), e);
        return null;
    }
}

7. GUI에서 세션 사용

앱 시작 시 복원

public class MainFrame extends JFrame {

    private SessionManager sessionManager = new SessionManager();

    private void initializeSession() {
        // 세션 복원 시도
        SessionData session = sessionManager.loadSession();

        if (session != null && session.isValid()) {
            // 이전 분석 결과 복원
            currentResult = session.getFlowResult();
            projectPathField.setText(session.getProjectPath());

            // 필터 설정 복원
            if (session.getUrlFilter() != null) {
                urlFilterField.setText(session.getUrlFilter());
            }

            // 결과 화면 표시
            displayResult(currentResult);

            log.info("세션 복원: {} ({} flows)",
                session.getProjectPath(),
                currentResult.getFlows().size());
        }
    }
}

분석 완료 시 저장

private void onAnalysisComplete(FlowResult result) {
    currentResult = result;
    displayResult(result);

    // 세션 저장
    sessionManager.saveSession(
        projectPathField.getText(),
        result,
        urlFilterField.getText(),
        outputStyleCombo.getSelectedItem().toString()
    );
}

앱 종료 시 저장

// 창 닫기 이벤트에서 저장
addWindowListener(new WindowAdapter() {
    @Override
    public void windowClosing(WindowEvent e) {
        // 현재 상태 저장
        saveCurrentSession();
        System.exit(0);
    }
});

8. Issue #020: 이중 저장 문제

발견: 테스트 중 이상한 점

테스트 코드를 작성하다가 발견했다:

@Test
void 설정_저장_테스트() {
    // Registry에서 읽기
    String urlFilter1 = prefs.get("urlFilter", "");

    // JSON에서 읽기
    SessionData session = sessionManager.loadSession();
    String urlFilter2 = session.getUrlFilter();

    // 둘 다 같은 값이 있다?!
}

원인: 점진적 기능 추가의 부작용

┌─────────────────────────────────────────────────────────────┐
│                    설정 저장 구조 (문제)                      │
├─────────────────────────────────────────────────────────────┤
│  Registry (Preferences API)    │    JSON 파일               │
│  • 최근 경로                    │    • 분석 결과              │
│  • URL 필터  ◄─────────────────┼──► URL 필터 (중복!)         │
│  • 출력 스타일 ◄───────────────┼──► 출력 스타일 (중복!)      │
└─────────────────────────────────────────────────────────────┘

왜 이렇게 됐나?

  1. Session 13: GUI 구현 시 설정 저장 필요 → Preferences API 선택 (Java 표준)
  2. Session 18: 분석 결과 저장 필요 → Gson JSON 추가
  3. 문제: Session 18에서 기존 Preferences 코드를 제거하지 않고 JSON에도 같은 필드 추가

기능은 동작했지만, 기술 부채가 생겼다.

해결: JSON 단일 저장으로 통합

의사결정 과정:

  1. "얼마나 걸릴까?" → 코드 훑어보니 30분이면 될 것 같다
  2. "지금 할까, 나중에 할까?" → 지금 하자 (컨텍스트가 머릿속에 있을 때)

비교:

항목 Registry (Preferences) JSON 파일
플랫폼 Windows만 레지스트리, 다른 OS는 파일 모든 OS 동일
복잡한 객체 ❌ 문자열/숫자만 ✅ 객체 직렬화 가능
백업/이동 ❌ regedit 필요 ✅ 파일 복사
디버깅 ❌ 레지스트리 편집기 ✅ 텍스트 에디터로 바로 확인

결론: JSON이 모든 면에서 우수

// 통합 전
Preferences prefs = Preferences.userNodeForPackage(MainFrame.class);
prefs.put("urlFilter", filter);       // Registry
sessionManager.saveSession(...);       // JSON

// 통합 후
sessionManager.saveSession(...);       // JSON만 사용
// Preferences 코드 전부 삭제

배운 점

  1. "나중에" vs "지금": 어렵지 않으면 지금 하자. 컨텍스트 손실 방지.
  2. 이중 저장 = 기술 부채: 동작해도 문제. 언젠가 동기화 버그로 돌아온다.
  3. 설계 단계에서 통일: 새 기능 추가 시 기존 구조와의 충돌 검토 필요.

9. 최종 세션 데이터 구조

public class SessionData {

    private String projectPath;           // 분석한 프로젝트 경로
    private LocalDateTime analyzedAt;     // 분석 시간
    private FlowResult flowResult;        // 분석 결과 (핵심!)
    private String urlFilter;             // URL 필터
    private String outputStyle;           // 출력 스타일
    private List<String> recentPaths;     // 최근 프로젝트 경로 (최대 10개)
    private String endpointFilter;        // 엔드포인트 검색 필터
    private List<String> sqlTypeFilter;   // SQL 타입 필터
    private int selectedTabIndex;         // 선택된 탭 인덱스
    private String selectedEndpoint;      // 선택된 엔드포인트
    private String selectedTable;         // 선택된 테이블명

    // 유효한 세션인지 확인
    public boolean isValid() {
        return projectPath != null && !projectPath.isEmpty()
                && flowResult != null
                && flowResult.getFlows() != null;
    }

    // 최근 경로 추가 (중복 제거, 최대 10개)
    public void addRecentPath(String path) {
        if (recentPaths == null) {
            recentPaths = new ArrayList<>();
        }
        recentPaths.remove(path);  // 이미 있으면 제거
        recentPaths.add(0, path);  // 맨 앞에 추가
        while (recentPaths.size() > 10) {
            recentPaths.remove(recentPaths.size() - 1);
        }
    }
}

포인트:

  • isValid(): 분석 결과가 있어야 유효한 세션
  • addRecentPath(): MRU(Most Recently Used) 패턴

10. 마치며

요약

문제 해결
앱 종료 시 분석 결과 소실 Gson JSON으로 세션 저장
LocalDateTime 직렬화 실패 TypeAdapter 커스텀 구현
저장 경로 권한 문제 사용자 홈 폴더 (~/.code-flow-tracer/)
이중 저장 (Registry + JSON) JSON 단일 저장으로 통합

삽질에서 배운 점

  1. 라이브러리 선택은 목적에 맞게: 고성능이 필요 없으면 단순한 게 좋다. Jackson보다 Gson.
  2. Java 8+ 날짜 타입 주의: LocalDateTime, LocalDate는 대부분의 JSON 라이브러리에서 기본 지원 안 함. TypeAdapter 필요.
  3. 저장 경로는 홈 폴더: Program Files는 권한 문제, 현재 폴더는 일관성 문제. 홈 폴더가 정답.
  4. 기술 부채는 빨리 갚자: "동작하니까 괜찮아"는 위험. 발견 즉시 정리하는 게 나중보다 낫다.

다음 글 예고

다음 글에서는 CRUD 분석과 테이블 영향도를 다룬다. "이 테이블을 수정하면 어떤 API가 영향받지?"라는 질문에 답하기 위해 Bottom-Up 분석을 구현한 이야기다.


시리즈

  • 기술 1편: JavaParser로 Java 코드 분석하기
  • 기술 2편: 호출 흐름을 어떻게 따라가나? - 재귀와 백트래킹
  • 기술 3편: DAO에서 SQL까지 - XML 파싱으로 연결하기
  • 기술 4편: 정적 분석의 한계 - 해결할 수 없는 것들
  • 기술 5편: 분석 결과를 어떻게 보여줄까? - CLI 출력 구현기
  • 기술 6편: 분석 결과를 Excel로 정리하기 - Apache POI 활용기
  • 기술 7편: Swing으로 모던한 GUI 만들기 - CLI를 넘어서
  • 기술 8편: jpackage로 배포하기 - 폐쇄망에서 Java 없이 실행하기
  • 기술 9편: Gson으로 세션 영속성 구현하기 - 앱을 닫아도 분석 결과가 살아있다 (현재 글)

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

CRUD 분석과 테이블 영향도 - Bottom-Up 분석의 힘  (0) 2026.01.18
jpackage로 배포하기 - 폐쇄망에서 Java 없이 실행하기  (0) 2026.01.12
Swing으로 모던한 GUI 만들기 - CLI를 넘어서  (1) 2026.01.12
분석 결과를 Excel로 정리하기 - Apache POI 활용기  (0) 2026.01.10
분석 결과를 어떻게 보여줄까? - CLI 출력 구현기  (1) 2026.01.07
'토스 러너스하이 2기/기술' 카테고리의 다른 글
  • CRUD 분석과 테이블 영향도 - Bottom-Up 분석의 힘
  • jpackage로 배포하기 - 폐쇄망에서 Java 없이 실행하기
  • 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
Gson으로 세션 영속성 구현하기 - 앱을 닫아도 분석 결과가 살아있다
상단으로
목차

    티스토리툴바