스프링캠프 2026을 다녀와서

2026. 6. 25. 22:43·TIL

요즘은 어떤 개발 관련 행사들이 있는지 Dev-Event 를 보던 중, 스프링캠프 2026를 발견했다.

 

얼핏 '스프링캠프' 이름에서 느껴질 수 있듯이, 타임라인을 보니 모두 스프링과 관련된 주제들로 세션들이 꽉꽉 채워져 있었고, 최근에 여러 기술 블로그들에서 봤던 익숙한 키워드들도 있어서 더 반갑게 느껴졌다!

 

신청하려 보니 생각보다 높은 참가비에 흠칫 했지만.. 

대부분의 세션 주제들이 재밌어보였고 스프링 행사 중 인지도가 높은 행사였기 때문에 몰랐던 많은 키워드에 대해서 듣고 얻어갈 수 있을 것 같았다. 그리고 또 오프라인이 주는 현장감과 특유의 몰입감도 있기에 바로 신청했다!

 

 


1. Spring I/O 2026 — 변화하는 스프링, 떠오르는 AI, 그리고 우리의 준비

첫 번째 세션은 유럽에서 열린 Spring I/O 2026을 직접 다녀오고,

여기에서 얻은 인사이트들을 카카오뱅크 실무에서 어떻게 적용하고 풀어나가고 있는지에 대한 방향성을 공유해주셨다.

 

주제는 크게 다음과 같은 세 개의 영역에서 진행되었다.

  • Runtime (Virtual thread, WebFlux)
  • Architecture (Modular Monolith, Spring Modulith)
  • Development (Spring AI)

 

최근에 회사 기존 프로젝트의 Java 21+과 Spring Boot 4.0+ 이상으로 마이그레이션을 검토하면서, 가상 스레드 개념에 대해서 정리를 한 적이 있었는데 해당 주제가 나와 반가웠다! 특히 가상 스레드와 WebFlux의 비동기 프로그래밍과 연관을 지어서 생각하지는 못했는데, 이 부분에 대해서도 설명해주셔 너무 좋았다!

 

그리고 아키텍처 측면에서도 `Modular Modulith` 아키텍처와 `Spring Modulith` 라는 키워드도 들었는데, 강연이 끝나고 관련 기술 블로그와 개념들을 찾아보니 작년에 진행했던 Blolet 프로젝트가 해당 아키텍처였다..😳

당시에는 이런 키워드를 알고 도입한 건 아니었지만, MSA 인프라 비용을 사이드 프로젝트 단계에서 감당하기도 어려웠고, 추후에 MSA로 쉽게 전환할 수 있도록 설계했던 아키텍처였었는데.. 이를 가리키는 공식 용어가 있었다..!

 

 

1) Runtime

WebFlux를 버리는 게 아니라, MVC를 다시 선택할 수 있게 만든 것

 

 

Java 21 부터 '가상 스레드(VT, Virtual Thread)'가 개념이 등장했다.

  • 기존의 자바 스레드 모델에서는 OS의 커널 스레드를 단순히 래핑해서 사용했다.
  • 따라서 스레드를 매번 생성할 때마다 OS의 스택 메모리를 점유하게 되고, 애플리케이션이 생성할 수 있는 스레드 수는 OS의 RAM 사양에 종속적일 수밖에 없었다.

 

가상 스레드 이전, 빠른 동시성과 비동기를 제공하기 위해 카카오뱅크에서는 WebFlux로 오픈뱅킹 서비스를 구현했다.

하지만 실제 운영을 해보니 기대와 현실은 달랐다..!

  1. Non-blocking 으로 높은 처리량
    • DB I/O, Redis, Spring Batch 등 블로킹이 발생할 수 있는 지점들이 너무 많았다.
    • 블로킹이 하나라도 있으면 Reactive 이점이 크게 줄어든다.
  2. 명확한 장애 진단 (APM, Sentry)
    • Reactive 내부 동작이 수행하면서, 예외가 발생했을 때 스택 트레이스에서 본래의 비즈니스 로직을 가리키지 않게 된다.
    • 이로 인한 원인 추적 파악이 어렵게 된다.

 

확실히 예전 졸업 프로젝트에서 최적화 길 찾기 알고리즘을 구현했을 때, 각 노드 사이의 거리를 구하기 위해 많은 외부 API 호출을 필요로 해 WebFlux를 사용한 적이 있었는데, 당시에 Reactive 프로그래밍 개념과 문법에 대한 러닝 커브와 에러가 발생하면 스택 트레이스가 온통 Reactive 내부 메서드로 출력되어 원인을 파악하기 어려웠던 기억이 난다..

 

하지만 가상 스레드가 등장하고 나서 상황이 바뀌게 되었다.

블로킹이 발생했을 때 가벼운 가상 스레드를 사용해서 블로킹이 끝날 때까지 대기하고, 진짜 OS 스레드는 비동기적으로 계속해서 동작하게 된다. 따라서 OS 스레드 점유 방지를 목적으로 하는 비즈니스 로직에서는 굳이 Webflux를 사용할 이유가 없어졌다.

 

 

그럼에도 WebFlux가 필요한 순간들이 있다.

기존의 자바 스레드 모델보다 가상 스레드의 생성/사용 비용이 저렴하긴 해도 공짜는 아니다!

예를 들어 커넥션이 10만 개일 경우, 커넥션마다 가상 스레드를 생성해야 되고 각 가상 스레드마다 상태를 저장하는 스택 메모리와 OS 스레드에 mount/unmount 하는 비용이 발생하게 된다. 따라서 아래의 상황들과 같이 블로킹이 되는 지점이 없고, 높은 동시성을 요구하는 기능의 경우 WebFlux를 사용하는 것이 여전히 좋다!!

 

  1. 대규모 서비스의 동시 커넥션을 받는 edge 지점을 구현하는 경우 (inbound)
    • ex. API Gateway → 수만 개의 동시 커넥션 받음 → 내부 서비스로 라우팅
  2. 외부 API Fan-out + Backpressure 지점을 구현하는 경우 (outbound)
    • Fan-out: 하나의 요청/이벤트를 여러 외부 호출/소비자로 퍼뜨리는 것
    • Backpressure: 생산 속도가 소비 속도보다 빠를 때, 메모리 폭증과 지연 시간 증가를 방지하기 위해 생산 속도를 조절하는 신호
    • ex. 사용자 전체 계좌 불러오기 → 전체 은행 API 동시 호출
  3. SSE, WebSocket과 같은 단방향/양방향 스트리밍 서비스를 구현하는 경우
    • ex. 실시간 주식 가격 업데이트, 방송 서비스

 

 

2) Architecture

지저분한 모놀리스에 대한 답은 분산이 아니라 구조다

 

 

많은 기업들이 서비스가 점점 커지고 프로젝트 내에서 도메인 간의 의존성이 복잡해질 때 MSA를 고려하게 된다.

이 때, 각 서비스를 쪼개서 관리하는 MSA보다는 하나의 애플리케이션 내에서 도메인 간의 경계와 의존성을 명확히 하는 방법으로 `Modular Monolith` 아키텍처와 이를 컴파일 단계에서 강제할 수 있는 가드레일인 `Spring Modulith`를 사용할 수 있다.

 

카카오뱅크에서는 '수신상품(예금성 상품)'을 기준으로 서로 다른 상품일 경우 직접적인 참조를 차단하고, 공통 모듈으로는 전자서명 기능을 정의하여 외부 모듈에서 공개된 API 뿐만 아니라 내부까지 접근해서 사용할 수 있도록 설계했다.

 

 

자세한 내용은 카카오뱅크 기술 블로그에서 소개하고 있어 참고하면 좋을 것 같다.

(개인적으로 블로그 내용을 정리한 내용도 첨부했다)

  • [카카오뱅크] MSA로의 여정에서 만난 Spring Modulith 체리픽 해본 후기 🍒
  • 블로그 내용 정리

 

추가로 최근에 여기어때에서 작성한 Spring Modulith 에 관한 기술 블로그 내용도 있어 함께 참고하면 좋을 것 같다!

  • [여기어때] MSA도, 모놀리스도 아닌 제3의 선택 — Spring Modulith
  • 블로그 내용 정리

 

 

3) Development

많은 사람들의 인식과는 달리 Java 언어에서 AI 기능(mcp 서버)을 더했을 때, 다른 언어에 비해 뒤처지기는커녕 좋은 퍼포먼스를 보였다..!

 

https://www.tmdevlab.com/mcp-server-performance-benchmark.html

 

그리고 스프링 환경에서 AI 기능을 추가하는 것도 생각했던 것보다 놀라울 정도로 간편했다!😲

먼저 사용자가 입력한 프롬프트를 LLM에 전달하고, MCP tool과 아래의 로컬에서 정의한 @Tool을 호출한 뒤 응답을 원하는 형태로 반환하는 역할을 하는 ChatClient를 정의한다. 그리고 비즈니스 로직에서 @Tool 어노테이션 하나만 붙이면 해당 메서드가 ChatClient가 호출 가능한 tool로 등록되어 스프링 환경에서 편하게 통합할 수 있었다.

 

강의를 들으면서..  오.. 회사 챗봇 시스템에 붙여서 개인화 된 기능들도 제공할 수 있겠다 싶었는데

아니나 다를까 연사자님이 아직 Spring AI가 계속해서 기능이 추가되고 안정화가 진행 중인 단계라서, 바로 프로덕션에 적용하라는 의미가 아니라 '방향' 측면에서  Spring이 이렇게 AI 친화적이게 발전하고 있음을 설명해주셨다..🤣

 

 

[실제 동작 과정 정리]

  1. chatClient에서 LLM 으로 전송
    • 사용자 프롬프트(자연어) + 사용 가능한 tool들에 대한 명세 (name / description / parameter schema)
  2. LLM에서 프롬프트에 맞는 tool 선정 및 구조화 된 요청 JSON 출력
  3. 애플리케이션에서 실제로 해당 tool 메서드를 실행 후 응답 결과 획득
  4. 결과를 다시 LLM에 전달
  5. LLM에서 결과를 바탕으로 최종 자연어 응답 생성
    • 필요할 경우 LLM이 tool을 사용하여 2~4 번 과정을 반복될 수 있다.
this.chatClient = builder
    .defaultSystem("You are an AI assistant...")  // ① System Prompt
    .defaultToolCallbacks(skillsTool, mcpTools)   // ② Agent Skills + MCP
    .defaultAdvisors(
        new ToolCallAdvisor(),   // ③ Tool Calling 루프를 Advisor 체인에서 처리
        memoryAdvisor            // ④ Chat Memory — ③의 도구 호출까지 기록
    )
    .build();
// 기존 서비스에 @Tool 하나 추가 → AI 에이전트가 호출 가능한 도구로 노출
@Service
class AccountService {
    @Tool(description = "고객의 계좌 잔액 조회")
    AccountBalance getBalance(String accountId) {
        var user = SecurityContextHolder.getContext().getAuthentication().getName(); // ← 기존 OAuth2 그대로
        return accountRepository.findBalance(accountId, user);
        //     누가 AI를 통해 어떤 데이터에 접근했는지 감사(audit) 유지
    }
}

 

 

4) 더하여..

이외에도 다음과 같은 내용들을 배울 수 있었다.

 

 

클라우드 서비스가 당신의 서비스의 안정성을 통제하게 두지 마세요!

 

 

[Resilience]

  • 외부(클라우드) 서비스에 장애가 발생했을 때 이를 격리하고 회복할 수 있도록 설계해야 함

  • 기존 Resilience4j 의존성 → Spring Core 모듈 내 라이브러리로 통합
    • Circuit Breaker: 외부 서비스가 죽으면 빠르게 차단하고 폴백, 준비해둔 대체 동작으로 대응
    • Timeout & Retry: 외부 호출이 무한정 내 스레드를 잡아먹지 않게끔 대기 시간과 재시도를 제한
    • Bulkhead: 외부 호출용 리소스 풀을 분리해 특정 서비스 장애가 전체 서비스 장애로 전파되지 않도록 하는 패턴

    • @ConcurrencyLimit: 메서드 or 클래스 단위로 동시에 실행 가능한 스레드 수를 제한하는 기능
    • DB 커넥션 풀이나 외부 API Rate Limit에 맞춰 접근 상한을 설정
      • 가상 스레드는 생성 비용이 저렴하고 기존 플랫폼 스레드처럼 풀 크기 제한이 없어서, 동시 요청이 몰리면 거의 무제한으로 생성될 수 있음
      • 반면 DB 커넥션 풀이나 외부 API의 처리 용량은 그대로 한정적이므로, 한정된 자원에 비해 훨씬 많은 가상 스레드가 동시에 접근을 시도하며 대기 상태로 쌓임
      • 대기 자체는 상대적으로 저렴할 수 있지만 timeout → retry로 이어지면, 실패한 요청들이 또 다시 새로운 가상 스레드를 만들어 동일한 자원에 재접근을 시도 → 대기열이 계속 누적·증폭되는 악순환이 발생함

 

 

 

AI는 컴파일러가 아니다. 비결정적이다. 결과물의 책임은 당신에게 있다

 

 

[Spec-Driven Development]

  • 팀 내 AI 기반 개발 프로세스 정의
    • 먼저 명세(Spec)을 작성하고, AI가 다이어그램·모델·코드·테스트 생성
    • Green field
      • 신규 기능: 요구사항 → 명세 작성 → AI 코드 개발
    • Brown field
      • 기존 기능: 코드 → AI 명세·유스케이스 역추출 → 현대화 마이그레이션
    • Skill 정의
      • 특정 라이브러리를 사용하는 팀 관례, 컨벤션 등 사용법을 Skill로 명시적으로 정의
  • `github spec-kit` 도입
specs/
└── L003-account-lock-check/      ← 기능(feature) 하나 = 폴더 하나
  ├── spec.md         # 1단계: 무엇을(What) — 기능 요건·시나리오
  ├── research.md     # 2단계: 코드 조사 결과 (Brownfield 핵심)
  ├── data-model.md   # 3단계: 데이터 설계 — 엔티티·ERD
  ├── plan.md         # 4단계: 어떻게(How) — 구현 계획
  └── tasks.md        # 5단계: AI가 실제로 실행할 잘게 쪼갠 태스크
## User Story 1 (P1)                  
IAM 관리자가 계정의 로그인 가능 여부를 조회하고 불가 사유를 즉시 파악한다.
- 누가(IAM 관리자) — 무엇을(조회) — 왜(즉시 파악) 형식. 전형적인 유저 스토리.

### Acceptance Scenarios (인수 시나리오)      
- Given 비밀번호 만료 계정 선택
  When 로그인 가능 여부 조회 실행
  Then "로그인 불가 - 비밀번호 변경 필요" 표시
- Given 여러 불가 조건에 해당하는 계정
  When 조회 실행
  Then 불가 사유 전체 목록 반환

### FR (Functional Requirement, 기능 요구사항) 
- FR-001 결과는 가능 여부(boolean)와 불가 사유 목록을 포함해야 한다 (MUST)
- FR-002 ROLE_READ 이상 권한만 접근 가능 (MUST)

### SC (Success Criteria, 성공 기준)
- SC-001 3번의 클릭 이내에 결과 확인 가능
- SC-002 사유 메시지만으로 해결 방법 파악 가능

 

 


2. 출렁이지 않는 편안함 - AOT 빌드와 Project Leyden

이 세션은 아쉽게도 직접 듣지는 못했다..😢

타임라인을 처음 봤을 때 고민이 되었지만, 하네스를 주제로 하는 세션과 같은 시간대에 진행이 됐고, 지난 K-Devcon에 이어 카카오에서는 하네스를 어떻게 구축해서 사용하고 있는지 궁금해서 결국 하네스 세션을 들었다..

더보기

하네스 세션에 대해서 짧게 정리를 해보자면, 조직 환경에서의 하네스 구축보다는 1인 개발 환경에서의 역할별 Agent/Skill을 정의하고 오케스트레이션하는 세션이었다. 하네스 구축에 대한 설명을 들으면서, Claude Code에서 사용하는 omc나 superpowers 같은 플러그인을 직접 만드는 것과 비슷하지 않을까 하는 생각이 들었다. 프로젝트 목적에 맞게 Agent/Skill을 직접 커스텀하고 '설계-구현-검증-테스트'까지의 파이프라인을 정의하면 나만의 하네스가 되고, 이를 팀에 맞게 다듬고 공통화 하면 팀의 하네스가 되지 않을까 싶다..💭

 

그렇지만 스프링캠프가 끝나고 듣지 못했던 다른 세션의 발표자료도 보면서,

JIT 컴파일 방식과 AOT 컴파일 방식 이외에, Project Leyden의 새로운 방식을 소개해주셔 흥미롭게 봤다!

 

세션에 포함된 JIT, AOT 컴파일의 구체적인 내부 동작 방식까지는 정리는 못했지만, 전체적으로 지금까지의 자바 컴파일 방식을 리마인드하고 정리할 겸 추가로 작성했다!

 

 

1) 문제 배경

우선 대부분의 프로젝트에서 표준으로 사용되는 JIT 컴파일 방식의 고질적인 단점이 있다.

초기 애플리케이션 실행 시, 실행 시간과 웜업(Warm-up)이 오래 걸린다는 것이다.

 

그러면 왜 이렇게 오래 걸릴까??

  • JVM 실행
  • 클래스 로딩
  • JIT 컴파일 

먼저 자바 애플리케이션의 `.java` 소스코드를 컴파일을 하면 중간 코드인 `.class` 바이트코드가 산출되게 되고, 이를 읽고 기계어로 번역하기 위해서 JVM 실행이 필요하게 된다.

그리고 `.class` 바이트코드를 JVM에 올리는 과정에도 지연이 발생한다.

마지막으로 내부적으로 런타임에 클라이언트 호출에 따라 동적으로 JIT 컴파일러를 통해 최적화가 일어난다. 따라서 초기에는 Cold 상태로 느리게 동작하다가 JIT 컴파일을 거치고 나서 서서히 Warm이 되어가며 빠른 성능에 도달하게 된다. 하지만 이 과정에서도 JIT 컴파일로 인한 서버의 리소스 사용량이 증가되고 주요 지연 중 하나로 꼽히게 된다.

 

추가로 예전에 자바 JIT 컴파일러와 GraalVM 네이티브 이미지에 대해 궁금해서 찾아봤었는데, 망나니개발자 님 블로그에서 정말 잘 설명해주셔 참고하면 도움이 될 것 같다!!

  • [Java] Hotspot VM의 한계(JIT, Just-In-Time 컴파일러)와 이를 극복하기 위한 GraalVM의 등장
  • [Java] GraalVM이 제공하는 네이티브 이미지(Native Image)
  • 블로그 내용 정리

 

이로 인해 발생하는 실제 운영 환경에서 다음과 같은 장애 시나리오가 발생할 수 있다!😳

  1. 사용자 트래픽 증가
  2. 서버 스케일 아웃 시도
  3. 서버 컨테이너 실행 지연 발생 (JVM 실행, 클래스 로딩)
  4. 실행 직후 웜업으로 인한 응답 지연 발생 (JIT 컴파일)
  5. 스케일 지연으로 인한 기존 서버들에 부하가 집중되어 연쇄적으로 다운..

 

 

2) 기존 해결 방식

많은 기업에서는 이 문제를 해결하기 위해 Warm-up 스크립트를 적용했다.

 

관련 기술 블로그 및 세션 (시간순 배치)

  • [라인] Spring Boot + Kubernetes 기반에서 웜업 적용하기
  • [올리브영] 커스텀 어노테이션과 리플렉션으로 구현한 Spring Boot 웜업 로직 최적화
  • [네이버] API 호출식 웜업의 부작용을 넘어서 : 라이브러리만 데우는 JVM 웜업
    • 세션 정리
  • [카카오페이] 배포 직후 발생하는 응답 지연을 해결하기 위한 여정 (feat. JVM 웜업) ⭐️⭐️⭐️

 

배포 직후에 서버에 인위적으로 트래픽을 보내 JIT 컴파일을 최적화하는 방식이지만, 다음과 같은 단점들이 있다.

  • 배포 파이프라인이 복잡해짐
  • Warm-up 하는 과정이 포함되면서 전체 배포 시간이 늘어나게 됨 → 앞서 설명한 장애 상황 시 사용하기가 어려움
  • 실제 운영 환경에서 발생하는 트래픽이기 때문에, 재현할 수 있는 트래픽의 한계가 있음

 

 

3) GraalVM의 네이티브 이미지

네이티브 이미지는 기존 자바 애플리케이션의 느린 Bootstrap, 높은 리소스 점유를 해결하기 위해 만든 이미지이다.

`.class` 바이트코드를 AOT(Ahead-Of-Time, 미리) 컴파일하여 네이티브 바이너리 파일, 즉, 자바 애플리케이션을 JVM 없이도 실행이 가능한 단독 실행 파일로 만든 것이다.

 

 

하지만 GraalVM 네이티브 이미지는 한 가지 강력한 가정을 하고 들어간다.

 

Closed World Assumption(닫힌 세계 가정) 

 

"런타임에 필요한 모든 클래스, 메서드와 그 라이브러리 의존성이 빌드 시점에 100% 확정되어 있다는 가정"

 

 

이 가정에 따라 AOT 컴파일 단계에서 자바 애플리케이션을 정적으로 분석해, 애플리케이션 실행에 필요한 부분만을 포함해 최소한의 바이너리 실행파일로 만든다.

 

덕분에 기존 JVM 애플리케이션에서 필요한 무거운 과정들은 모두 사라지게 되고,

초기에 빠른 실행과 요청 처리량(QPS)과 메모리 사용량(RSS), 이미지 용량 측면에서 높은 성능을 가져가면서 장점을 취할 수 있다.

  • JVM 실행 → 간단한 GC, 스레드 관리만 하는 substrate VM 실행
  • 클래스 로딩 → 정적 분석으로 런타임에 필요한 라이브러리, 클래스, 메서드만 포함해 바이너리 파일 생성
  • JIT 컴파일 → 모든 최적화를 미리 끝냄
    • JIT 컴파일에서 진행되는 동적 프로파일을 수집하고 최적화하는 것을, Graal VM에서는 네이티브 이미지를 빌드하기 전에 미리 특정 상황을 만들어서 프로파일링을 진행해 최적화를 진행하고 있다.

 

하지만 그에 상응하는 단점과 문제도 발생하게 된다.

  • 빌드 시간이 오래 걸림
  • 런타임에 유연하게 최적화 할 수 있는 JIT 컴파일 대비 퍼포먼스 고점이 낮음
  • 자바의 동적 기능을 사용할 경우, 빌드 시점에 메타 데이터(config)로의 제공이 필요함
    • 자바는 동적 언어로 Reflection, Dynamic proxy와 같은 여러 동적 기능들을 제공하고 있지만, GraalVM 네이티브 이미지에서는 동작하지 않게 된다. 따라서 네이티브 이미지를 빌드할 때는 이러한 동적 기능들에 대한 정보를 런타임 힌트로 미리 제공해줘야 한다.
    • 참고로 Spring 내부의 동적 기능들에 대해서 자동으로 빌드 시점에 정적 자바 코드로 변환하거나 런타임 힌트를 생성하는 등의 지원을 해주고 있다. 따라서 개발자가 직접 정의하는 동적 기능에 대한 런타임 힌트만 제공해줘도 된다.
      • Spring Bean 생성과 같이 리플렉션 기반으로 동작하는 기능은 new를 사용하는 정적 초기화 코드로 대체
      • `@Transactional`와 같은 AOP 프록시 생성처럼 정적으로 대체하기 어려운 기능은 런타임 힌트 생성

 

 

4) Project Leyden

Project Loom에서도 나왔지만, Loom / Leyden 프로젝트들의 네이밍은 뭐지 싶었다.

찾아보니 JDK가 버전업하면서 여러 기능들이 개발되는데, 해당 기능이 어떤 프로젝트 이름으로 진행되었음을 의미하는 거였다..😲

  • Project: 여러 관련 기능들을 묶어서 장기적으로 추진하는 큰 개발 프로젝트
    • Amber: 자바 문법 개선 (Records, Switch Expressions, var)
    • Loom: 동시성 개선 (Virtual Thread, Structured Concurrency, Scoped Values)
    • Leyden: 시작 시간, 웜업, 메모리 사용량 개선 (AOT Class Loading)
  • JEP: 개별 기능 단위 명세서
    • JEP 395: Records
    • JEP 444: Virtual Thread
    • JEP 483: AOT Class Loading

 

각설하고!

Project Leyden은 GraalVM 네이티브 이미지의 한계(동적 기능 제약, 낮은 Peak 성능 등)를 피하면서,
앞서 살펴본 자바 애플리케이션의 고질적인 문제인 초기 시작 시간과 최고 성능 도달 시간(웜업), 메모리 사용량을 개선하려는 프로젝트다.

 

 

GraalVM 네이티브 이미지와 가장 큰 차이점은 JVM 기반 실행 환경과 그 특징을 온전히 유지한다는 점이다!

  • 자바 언어의 동적 기능 사용
  • 런타임 JIT 컴파일러 사용

 

주요 동작 방식과 간단히 정리하면 다음과 같다.

  1. Shift: 런타임에 실행하는 클래스 로딩, JIT 컴파일 등의 무거운 작업들을 빌드 타임으로 앞당겨 수행한다.
    • Training Run: 애플리케이션을 사전에 실행시켜 JIT 컴파일을 위한 프로파일링 정보도 함께 수집하여, 실행 이후 JIT 컴파일이 빠르게 수행할 수 있도록 한다. (추후 JDK 버전업 시, JIT 컴파일로 최적화 된 기계어도 캐싱이 된다면 완벽한 AOT 환경이 됨!)
  2. Constrain: 빌드 타임에 수행된 결과를 런타임에 임의로 변경되지 못하도록 한다.
  3. Condenser: 빌드 산출물을 AOT Cache에 캐시 파일로 압축하여 저장한다.

이렇게 생성된 AOT Cache를 사용하면서, 런타임 초기에 발생하는 과정들을 줄여 초기 시작 시간과 웜업 지연, 메모리 사용량을 최소화한다.

 

 

그래 AOT Cache가 좋은 건 이해했다! 그럼 어떻게 적용할까??

생각보다 간단하다.

 

먼저 Training Run 단계를 통해 AOT Cache를 만들어야 한다.

따라서 프로젝트 실행 파일(`my-project.jar`)를 실제로 실행해, 클래스 로딩 데이터와 JIT 컴파일을 위한 프로파일링 정보를 수집하고, 그 결과를 `my-project.aot` 캐시 파일로 만든다.

java \ 
  -XX:AOTCacheOutput=my-project.aot \
  -jar my-project.jar

 

그리고 이제 운영에서 배포 실행을 할 때, AOT Cache 파일을 사용해서 실행하면 된다.

java \
  -XX:AOTCache=my-project.aot \
  -jar my-project.jar

 

 

마지막으로 AOT Cache를 적용할 때 주의사항만 환기하고 넘어가자!

  • AOT Cache를 만드는 Training Run 환경과 운영 배포 환경이 동일해야 한다.
  • JDK 25 버전 이하 환경에서는 동일한 GC 정책을 사용해야 한다.
    • Training Run 에서는 Serial GC, 운영에서는 G1 GC로 실행하면 캐시 무효화가 발생한다.

 

 


3. 워크플로우 코드 왜 항상 복잡할까? - Temporal이 보여준 다른 방법

`Temporal` 관련해서는 무신사 기술 블로그 글에서 처음 보고, '오.. 실무에서의 복잡한 워크플로우는 이렇게 설계할 수 있구나' 하면서 한 번 정리한 적이 있었다. 당시에도 인상깊게 읽었어서, 스프링 캠프 타임라인에서 Temporal 주제를 봤을 때부터 기대됐던 강연 중 하나였다!

역시나 들으면서 주제에 대한 예시 상황이 너무 좋아 이해하기 수월했고, 연사님의 딕션이나 전달력 또한 와.. 하면서 들었던 강연이었다. 그리고 복잡한 기능 흐름과 요구사항들이 이렇게 정의되고 개발되고 있다는 점이 설레고 재밌게 느껴졌다..!

 

 

1) 무신사에서의 Temporal

먼저 무신사에서의 글을 간략히 복기해보자면, 무신사에서는 아래의 문제를 해결하기 위해 Temporal을 도입하여 '물류 출고' 워크플로우를 개선하는 작업이었다.

  • 'Jenkins cron 으로 일정 시간대에 출고를 지시하는 job' 이 실패함
  • 어디에서 에러가 발생했는지 파악하기 위해 `Jenkins 콘솔 로그 → 애플리케이션 로그 → DB 이력` 을 확인하면서 원인을 파악하고, job을 수동으로 재실행해야 함

 

 

2) 토스 증권에서의 Temporal

연사님은 토스 증권에서의 실무에 맞춰서인지 '고객 보상' 워크플로우로 Temporal을 적용하는 예시를 들어주셨다.

참고로 강연에서는 실제 애플리케이션 레벨에서 어떻게 구현하는지도 소개를 해주셨지만, 큰 그림에서 이론적인 기능들과 인사이트들을 위주로 정리를 했다!🤔

 

 

첫 번째 분석 단계에서 각 서비스마다 다른 정책(최대 TPS, 재시도, 타임아웃)들을 갖고 있다.

예를 들어 주문·결제 내역 서비스만 성공하고 나머지 서비스가 실패될 경우, 분석 단계를 재시작 과정에서 성공한 서비스에 대한 재호출은 방지되어야 한다. 이를 위해 기존에 성공한 결과를 영속화 기능이 요구된다.

→ 영속화, 재시도, 타임아웃

 

두 번째 매니저 검토 단계는 일상에서 문의 응답, 환불 승인, 보상 승인과 같은 프로세스를 생각해보면 이해하기 쉬울 것 같다. 매니저의 '최대 n일 승인 대기' 같은 서비스는 당연하게도 워크플로우가 계속 실행 상태를 유지하면서 기다리지는 않는다.

이벤트 기반 설계처럼 매니저가 승인하면 이를 감지하고 처리하는 기능이 필요해진다.

→ 타이머, 신호처리

 

마지막 고객에게 크레딧을 지급하는 단계이다.

이 때 '지급 준비 기록 - 지급 - 지급 완료 기록'은 트랜잭션의 원자성 특성과 같이 All or Nothing 으로 수행되어야 한다. 어느 한 단계에서 실패가 났을 때는 보상을 통해 원복시켜야 한다.

→ 원자성, 보상

 

이 워크플로우의 핵심은 "고객 데이터를 수집해서 보상 금액을 계산한 후 매니저 승인을 받아 지급한다." 이지만

각 단계별 시나리오별을 구현하기 위해 영속화, 재시도, 타임아웃 / 타이머, 신호처리 / 원자성, 보상 기능이 필요하게 되고 이를 Temporal이 수행하게 된다.

Temporal 공식 문서에서 이 외에도 제공하는 여러 기능들을 확인할 수 있다.

 

 

3) Temporal 구성요소

위에서 정의한 워크플로우의 개념 모델을 논리 모델로 구체화하기 위해 알아야 할 Temporal의 주요 구성요소까지 정리해보자!

 

  • Temporal Server
    • 영속화 기능과 같이, 워크플로우의 진행 상태를 저장하고 관리한다. (with. Task Queue, Event History)
  • Workflow
    • '데이터 수집 → 금액 평가 → 매니저 검토 → 보상 지급'과 같이 비즈니스 로직 흐름만 정의한다.
    • 어느 때 실행해도 항상 동일한 결정이 나오도록 정의해야 한다.
    • 외부 API 호출이나 DB 호출과 같은 세부적인 일의 수행은 Activity에게 위임한다.
  • Worker
    • Workflow와 Activity를 실행하는 프로세스다.
    • Temporal Server에서 관리하고 있는 Task Queue에서 폴링을 해 실행한다.
  • Activity
    • 외부 API 호출이나 DB 호출 등 비결정적인 로직을 구현하고 실행한다.
    • 실패 시, 자동 재시도나 타임아웃과 같은 기능을 여기에서 정의한다.

 

여기에서 Workflow는 결정적, Activity는 비결정적으로 동작한다는 점에서 물음표가 생길 수 있는데, 연사님께서도 이 부분에 대해서 한 번 짚고 넘어가주셨다!👍

 

Workflow에서 A, B, C Activity를 차례로 실행하던 중 C Activity에서 갑자기 어떤 장애로 인해 죽을 수 있다.

이 때 위에서 말했던 A, B Activity의 비결정적 재호출을 방지하기 위해, Activity의 실행 결과를 Event History에 영속화 해 Workflow의 결정적인 흐름을 방해받지 않고 보장할 수 있다.

 

아! 강연에서 놓친 부분이 있는 것 같아 더 찾아보니 Workflow가 재시작 할 때는 중단된 지점부터가 아니라 처음부터 다시 실행된다. 그리고 실행하면서 Activity 호출 지점에 도달하면, 이 때 Event History를 확인하고 가져오는 것이다. 즉, Workflow 정의에서 `date.now()`나 `random()`, `uuId` 같이 실행할 때마다 비결정적인 값을 반환하는 로직을 정의해서는 안된다.

 

 

4) 결론

결론을 정리해보자!

  • Temporal은 핵심 비즈니스의 흐름에만 집중하게 해준다.
    • think. 어째 헥사고날 아키텍처의 도메인 중심 설계와 맥락이 비슷한 것 같다.
                결국 도메인/비즈니스 기능을 잘 정의하고, 외부 기능들과의 경계를 잘 구분하는 것이 핵심이지 아닐까 싶다
  • 서버의 환경이 불안정해서 생긴 에러 상황에 대해 재시도, 타임아웃, 이벤트 처리, 보상 트랜잭션과 같은 부수적이지만 반드시 필요한 기능들을 분리해서 관리할 수 있도록 해준다.
  • 워크플로우의 실행 흐름과 상태 파악을 위한 모니터링 GUI도 제공해준다. (무신사 기술 블로그)

 

하지만 공짜는 아니다! 

  • Temporal을 사용하기 위해서는 별도의 Temporal Server를 운영하기 위한 별도의 인프라가 필요하다. (직접 운영 or Cloud 사용)
  • 워크플로우 코드는 항상 결정적이어야 한다는 제약이 있다.
  • 이미 실행 중인 워크플로우를 위한 버저닝 관리가 필요하다.
  • 팀 내 러닝 커브

 

 

5) 질문

Q. Temporal Server에서의 개인정보 관리

강연을 듣고 Temporal Cloud를 사용하는 경우, 사용자의 정보가 Temporal이 관리하는 서버에 저장이 되는데 보안 측면에서 어떻게 관리하고 계시는지 궁금해 연사님께 질문을 드렸다. 우선 연사님 조직 내에서는 Cloud를 사용하지 않고 Cluster 인프라를 직접 운영하고 있지만, Cluster를 운영하더라도 아무래도 중앙에서 관리되는 서버다보니 별도로 암호화를 진행해서 저장을 하고 있다고 말씀해주셨다..!

 

 

Q. exactly once 보장

연사님께 질문을 하러 갔다가, 다른 분들의 질문을 같이 듣고 '오..!! 질문 너무 좋다' 싶어 정리했다.

"at least once 구현은 가능할 것 같은데, exactly once 구현도 가능할까요?? 결국 worker가 실패를 했을 때 다시 시도를 하면서 두 번 실행이 될 것 같은데.." 라는 질문이었다. 실제로 질문에 대한 답변도 "exactly once는 temporal이 보장해주는 게 아니라서 불가능 한 문제이다. at least once 실행을 전제로 activity를 항상 멱등하게 설계해서 구현해야 한다." 고 말씀해주셨다.

 

즉, activity가 여러 번 실행이 되더라도 시스템의 상태 변경은 한 번만 실행했을 때와 동일해야 한다는 것이다.

그래야만 Credit 보상 지급이나 상품 결제가 중복해서 일어나지 않아야 된다.

 

아 그러면 멱등하게 설계하면 exactly once가 보장되지 않을까?? (제가 그렇게 생각했습니다.. 큽.. 🤣)

아래 예시를 보면 더 이상의 첨언 없이도 바로 이해가 될 수 있다.

def process_order_activity(idempotency_key, order_id, amount):
    # 결제 처리
    payment_api.charge(amount, idempotency_key=idempotency_key)
1차 시도: 결제 성공 → 이메일 발송 성공 → 응답 받기 전 워커 에러
2차 시도: 결제 호출 → "이미 처리된 키네" → 결제는 중복 안 됨   (멱등성 OK)

결과: 결제 activity가 두 번 호출되었지만, 멱등하게 설계를 할 경우 '실제 결제' effect는 1번만 일어나게 된다.

 

 


소감 마무리!

 

좋았다! 이렇게 Java Spring만을 주제로 하는 컨퍼런스는 처음 와봤는데, 모든 세션이 Java Spring을 배경으로 하다보니 그동안 공부했던 개념들이나 경험들을 떠올리면서 더욱 와닿으면서 재밌게 들었다👍

 

그리고 솔직히 현장에 있을 때는, 4-50분이라는 시간에 많은 이야기들을 전달하다보니, 압축된 내용으로 빠르게 지나가서 ‘음.. 그렇구나’하고 넘어가는 부분들도 많았는데, 행사가 끝나고 천천히 발표자료를 보면서 모르는 부분들은 AI에게 물어보고 글로 정리하면서 천천히 곱씹어보니, 현장에서 느꼈던 것보다 슬라이드 한 장 한 장, 한마디 한마디가 훨씬 더 밀도있게 느껴졌다..!!

 

이번 캠프를 들으면서 분명히 WebFlux, Virtual Thread, Spring AI, Modular Monolith, MSA, Temporal.. 많은 기술들에 대해 리뷰할 수 있었다.

그럼 실무, 내가 진행하고 있는 프로젝트들에 적용할 수 있을까? 도 생각해보았다.

아쉽지만 많이 없을 것 같다..

프로젝트의 규모나 복잡성을 생각해보면, 팀 차원에서는 오히려 오버 엔지니어링이 될 수 있을 것 같다..

 

그래도 이런 개념들을 배우는 데서 그치지 않고, 실무 도입을 검토하고 적용해보면서 어떤 트레이드오프를 만나고 해결하면서 그 개념과 경험이 성숙해진다고 생각한다. 하지만 요즘은 표면적인 앎에서 그치는 순간들이 많은 것 같아 아쉽게 느껴진다..

서비스를 계속해서 발전시키면서, 더 많은 문제와 기술적 고민들을 하고 싶다는 목소리가 요즘 마음 깊숙이서 계속 외치고 있다.

 

아! 마지막으로 한 가지 놀라웠던 점이 있었는데 캠프가 끝나고 참가자들의 경력 비율을 알려주셨다. 

대략적으로 취준생 20%, 1~3년차 20%, 4~6년차 40%, 7년차 이상 20% 정도였는데, 무언가 취준생과 주니어 개발자 분들이 많을 것이라고 생각했는데 의외로 고연차 개발자 분들이 많으셔서 놀랐었다. 역시 이렇게 시간을 내서 컨퍼런스에 참석하고 꾸준히 공부한다는 게 멋있기도 했고 한편으로는 ‘역시 이게 개발자지’라는 생각도 들었다🤣

 

 

각설하고!

다시 한 번 말하지만 정말 좋았던 컨퍼런스였다!

유튜브로도 볼 수는 있지만 오프라인만의 몰입감과 세션 주제에 대한 내적 친밀감도 함께 올라간 것 같고, 나중에 적용할 일이 있을 때 유튜브 강의랑 이 블로그 글도 다시 리뷰해봐야겠다.

 

저작자표시 비영리 변경금지 (새창열림)

'TIL' 카테고리의 다른 글

SIPE 사담콘 5기를 다녀와서  (0) 2026.07.18
K-Devcon 3월 밋업을 다녀와서  (0) 2026.03.29
AWS SAA 후기  (0) 2026.03.09
AWS AIF 후기  (0) 2026.03.02
블로그 글쓰기의 '이미지 업로드'는 어떻게 동작할까??  (0) 2025.12.17
'TIL' 카테고리의 다른 글
  • SIPE 사담콘 5기를 다녀와서
  • K-Devcon 3월 밋업을 다녀와서
  • AWS SAA 후기
  • AWS AIF 후기
wch_t
wch_t
  • wch_t
    끄적끄적(TIL)
    wch_t
  • 글쓰기 관리
  • 전체
    오늘
    어제
    • 분류 전체보기 (188)
      • Algorithm (72)
        • 정리 (12)
        • Math (4)
        • Simulation (2)
        • Data Structure (4)
        • DP (6)
        • Brute Fource (10)
        • Binary Search (7)
        • Greedy (2)
        • Graph (10)
        • Mst (0)
        • Shortest path (10)
        • Two Pointer (1)
        • Tsp (3)
        • Union Find (1)
        • Mitm (0)
      • CS (12)
        • 데이터베이스 (5)
        • 네트워크 (5)
      • DB (9)
      • DevOps (15)
        • AWS (8)
        • Docker (1)
        • CI-CD (4)
      • Spring (60)
        • 끄적끄적 (5)
        • 기본 (9)
        • MVC 1 (7)
        • MVC 2 (11)
        • ORM (9)
        • JPA 1 (7)
        • JPA 2 (5)
        • Spring Data Jpa (7)
      • TIL (18)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    scope
    response_mode
    apache poi
    form_post
    SIPE
    spring-cloud-starter-bootstrap
    docker
    aws secrets manager
    Merge
    spring-cloud-starter-aws-secrets-manager-config
    후기
    TempTable
    Jenkins
    view algorithm
    스프링캠프 2026
    백준 17289 파이썬
    백준 3015 파이썬
    사담콘 5기
    백준 17299 파이썬
    Sxssf
    docker: not found
    K-DEVCON
    애플
    KSUG
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.3
wch_t
스프링캠프 2026을 다녀와서
상단으로

티스토리툴바