Search
📝

SpringCamp 2026 후기

요새 여러 언어, 프레임워크를 사용하다보니, 스프링 주제로 생각하는 시간을 가져보고 싶어 Spring Camp 2026를 신청했다. 오랜만에 지인들도 만나고, 질문도 하고, 희승님과 (이제는 AWS Evangelist가 된) 용호님과 사진도 찍고, 왠일로 퀴즈도 순위권에 올라 KSUG 티셔츠도 받고 그랬다.
이 중 Spring I/O Recap 내용들이 여러모로 알찼고, 희승님과 이야기 나눴던 게 기억에 많이 남고, 승주님의 세션이 인상 깊었다.
아래는 기억에 남는 몇가지를 남겨본다.

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

Virtual Thread

요청 하나에 스레드 하나.
"요청이 몰리면 톰캣이 막힌다"는 말은, 반은 맞고 반은 틀리다. 일단 커넥션은 수락된다. 톰캣 8.5+가 NIO로 max-connections(기본 8192)까지 받아 폴링하기 때문이다. 정작 병목은 요청을 처리하는 worker 스레드에서 생긴다. 기본 max-threads는 200이다. worker 하나가 DB나 외부 API를 기다리는 동안 묶이면, 유휴 스레드가 쌓여 병목이 된다. 그래서 자원을 효율적으로 쓰려고, 아래 공식으로 스레드 수를 잡기도 한다.
적절한 스레드 수 = 코어 수 × (1 + 대기시간 / 서비스시간)
가령 idle이 평균 99%면, 이론상 코어 8개로 worker 800개를 돌릴 수 있다. 하지만 트랜잭션이 무거우면 대기가 길어져 800개로도 모자라다. 그래서 몇몇 서비스는 WebFlux로 풀기도 했다. 코드를 논블로킹으로 바꿔야 하고 스택 트레이스도 끊기지만, 제한된 서버 자원으로 처리량을 확보하려면 어쩔 수 없었다.
Virtual Thread는 기다릴 때 스레드를 반납한다. 그만큼 자원을 효율적으로 쓸 수 있다.
가상 스레드는 실행될 때만 carrier 스레드(기본 ForkJoinPool)에 mount한다. 블로킹을 만나면 unmount해 OS 스레드를 반납하고, carrier는 다른 가상 스레드를 mount한다. 따라서 이론적으로 스레드 대기시간이 사라지는 셈이다.
JDK 25 이전에는, synchronized 안에서 블로킹하면 carrier를 반납하지 못하고 고정(pinning) 되는 이슈로 인해 처리량이 내려갔다. synchronizedHikariCP 등 여러 라이브러리 코드에 포함되어 있기 때문에, 사실상 Virtual Thread의 실익이 크지 않았다. JEP 491은 모니터 소유권을 carrier가 아니라 Virtual Thread에 연결해 이 문제를 풀었다고 한다.
spring.threads.virtual.enabled=true
Plain Text
복사
위 설정만 하면 Boot 3.2+, JDK 21+에서 톰캣 요청·@Async·@Scheduled·Kafka·RabbitMQ 리스너가 모두 Virtual Thread로 적용된다. (데몬이라 @Scheduled만 도는 앱은 spring.main.keep-alive=true도 둬야 JVM이 안 죽는다.)
Virtual Thread를 켠다면 아래를 고려해야 한다고 한다.
스레드를 풀링하지 않는다. 가상 스레드는 만들고 버리는 비용이 거의 없기 때문에, 굳이 풀에 모아 재사용할 이유가 없다. 오히려 풀에 가두면 풀 크기가 곧 동시성 상한이 되어, 가상 스레드의 장점이 사라진다. 요청마다 새로 하나씩 쓰면 된다(Executors.newVirtualThreadPerTaskExecutor()).
ThreadLocal을 무겁게 쓰지 말자. 예전엔 스레드가 수백 개라, ThreadLocal에 담은 객체도 많아야 수백개 정도여서 부담이 적었다. 그런데 가상 스레드는 수백만 개라, 스레드마다 들고 있으면 메모리가 급격히 늘 수 있다. 무거운 객체는 공유 불변으로 두거나 Scoped Value로 옮겨야 한다.
외부 자원에 상한을 직접 걸어야 한다(백프레셔). 예전엔 worker가 200개로 묶여 있어, 한 번에 DB, 외부 API를 호출하는 수를 자연히 제한할 수 있었다. HikariCP 풀은 기본적으로 10이라, 10개만 처리하고 나머지는 대기하므로 한번에 만 개의 요청이 오면 DB에 부하가 몰린다. 그래서 처리량의 한계가 스레드에서 커넥션 풀, 외부 API 쪽으로 옮겨간다. 풀은 DB가 감당할 만큼으로 설정하고, 외부 API엔 동시 호출 상한을 직접 걸어야 한다. @ConcurrencyLimit(Spring Framework 7)을 적용하자.

그 외에 기억에 남는 주제들..

Spring AI 2.0 (2026-06-12 GA · Boot 4 필요) — Tool Search. LLM에 도구를 전부 실어 보내면 토큰이 낭비되는데, 검색용 도구 하나만 노출하고 질문에 맞는 도구만 그때 찾아 준다고 한다. 토큰을 34~64% 아낄 수 있다(발표의 "63%"는 Anthropic 모델 기준).
Spring Modulith 2.0 — 패키지를 모듈 경계로 삼아, 테스트에서 ApplicationModules.of(...).verify() 한 줄로 경계 위반(다른 모듈의 내부를 직접 참조하는 등)을 잡아낼 수 있다고 한다. AI에 대부분의 코드 작성을 위임하다 보니, 사람이 모두 리뷰하기 힘들다. 단순한 코드 분리만으로 도메인의 경계를 강제하기 어렵고, 물리적인 모듈로 쪼개기엔 기준이 애매할 때 Spring Modulith가 적절한 대안이 될 수 있다. - 카뱅 적용 사례.
Spring Security — 사람 습관은 잘 안 바뀌어서, 90일마다 비밀번호를 바꾸게 한들 비밀번호 방식 자체가 보안에 취약하다. 이에 Passkey(생체 인증)·OTT(이메일 매직링크) 로그인이 6.4부터, 다단계 인증(MFA)이 Spring Security 7에 들어왔다고 한다.

세션 2 — 이희승: Is Java Alive and Kicking?

싱글 테넌시에서 멀티 테넌시로, JVM의 강점이 약점이 되다

"강력한 한 대의 머신에서, 소수의 서비스를, 오래 돌린다."
자바는 이 가정 위에서 설계되었다고 한다. JIT는 오래 돌수록 더 최적화하니 기동이 길어도 이득이었다. GC는 메모리를 넉넉히 쓰며 처리량을 끌어올렸고, 리플렉션·동적 로딩·실시간 프로파일링 등 리치 런타임이 개발을 편하게 했다. 모두 서버 한 대를 오래 쓰는 환경에 맞는 선택이었다.
그런데 요새는 머신 하나에 컨테이너를 띄워 여러 서비스가 나눠 쓰는 멀티 테넌시 환경이 선호된다. 서버 인스턴스도 통째로 내 것이 아니고, 스케일 인/아웃과 마이그레이션이 수시로 일어난다. 그러자 같은 설계가 거꾸로 발목을 잡았다. 잦은 재기동마다 JIT를 다시 돌려야 하고, 메모리 풋프린트는 오버부킹된 호스트에서 노이지 네이버(한 프로세스가 이웃 몫까지 잡아먹는 현상)를 일으킨다.
게다가 Polyglot 환경, 개발 장비 성능의 개선 등으로 런타임이 하던 일의 일부가 OS 레벨과 빌드 타임으로 내려갔다. 가령 모니터링도 perf·eBPF처럼 언어를 안 가리는 OS 도구를 쓴다. 여러 언어로 개발해야 한다면, 공통적으로 활용할 수 있는 도구가 유리하기 때문이다. 게다가 개발 장비 성능도 좋아져 런타임 JIT 보단, 빌드 타임 AOT 쪽으로 최적화하기도 한다. 이에 한동안 GraalVM Native Image가 대안으로 제시되기도 했지만, 빌드에 상당히 많은 CPU와 메모리가 필요하고, 무료 에디션은 고급 최적화와 GC가 막혀 있고 디버깅도 어려워서 관심이 금세 사그라들었다고 한다. Project Leyden도 좋은 접근이기는 하나 아직은 힌트, 캐시 수준이라, 런타임이 무겁다고 한다.
게다가 자바는 컴파일러와 표준 라이브러리만 제공하는 미니멀한 구성이라 빌드, 의존성, IDE가 제각각이고 커뮤니티에 의존적이다. 요새 나오는 언어들은 코드 스타일, 정적분석, 테스팅, 의존성 관리, 빌드 시스템, 개발 환경 통합 등 제공해주는게 많으므로, 이제 셋업하는 스타트업들이 자바를 선택할 유인이 적다고 한다.
그래서 자바 진영에선 두 가지 접근이 가능하다.
자바 자체를 고치거나(Amber·Loom·Panama), 아예 JVM 위 새 언어로 떠나거나(Kotlin·Scala). 하지만 전자는 하위 호환 문제를 해결하기가 어렵다. 가령, Project Valhalla (Value Object)는 2014년에 시작해 2026년에야 첫 프리뷰라고 한다. 그리고 코틀린을 사용한다 하더라도 결국 JVM 환경이니, 무거운 런타임과 상호운용 비용도 발생할 수 있다. (가령, 자바 진영에서 코틀린의 장점을 흡수해 기능이 추가된다면 이 또한 대응해야 한다.)
추가적으로.. 자바 챔피언인 희승님은 요새, 자바를 잘 사용하지 않는다고 하셨다. 기술 지형은 늘 바뀌므로, 한 언어만 붙들 이유는 없다는 것이다. 예전엔 팀이 익숙한 기술을 골랐다면, 이제는 AI 덕에 낯선 기술도 고를 수 있으므로 상황에 맞게 선택한다고 하셨다.

세션 3 — 정승주: 워크플로우 코드 왜 항상 복잡할까? - Temporal이 보여준 다른 방법

발표자는 고객 보상 워크플로우(데이터 수집 → 금액 계산 → 매니저 승인 대기 → 크레딧 지급)를 예로, 비즈니스 로직은 단순한데 이걸 완수시키려면 재시도·타임아웃·결과 영속화(수집 단계), 타이머·외부 신호 대기(7일 승인 단계), 원자성·보상(지급 단계) 같은 부가 로직이 도메인 로직에 더 붙어야 한다는 문제를 제기했대. 워크플로우는 코드만 봐선 비즈니스 로직이 눈에 잘 안 들어온다는 거다.

Durable Execution: 실행 그 자체가 상태가 된다

Temporal의 카피는 "실패가 없는 것처럼 코드를 짠다"이다. 한 번 시작한 워크플로우는 프로세스가 죽어도, 며칠이 걸려도 끝까지 완수된다. 이게 가능한 건 역할이 갈리기 때문이다. Temporal Server는 모든 실행 상태를 보관·조정하되, 코드는 실행하지 않는다. Worker는 반대로 내 코드를 돌리고 결과를 보고하되, 자기 상태는 가지지 않는다. 그래서 Worker가 죽어도 상태는 전부 Temporal Server에 남아, 다른 Worker가 이어받는다.
① Event History. 워크플로우에서 일어난 모든 사건(시작/종료, Activity 호출과 결과, 타이머 시작/만료, 외부 신호)을 지우지 않고 기록한다.
② Replay. 워크플로우 코드는 1일 대기, 배포 등 여러 이유로 처음부터 다시 실행될 수 있다. 이 때, 멈춘 자리의 스택, 메모리를 저장, 복원하는 게 아니라, 코드를 처음부터 돌리되 Event History를 입력으로 준다고 한다. 코드가 결정적(deterministic)이면 매번 같은 분기를 타 마지막 기록 지점까지 똑같이 따라잡고, 그 뒤부터 실제로 이어진다. 그래서 워크플로우는 반드시 결정적이어야 한다. Worker가 결과 보고 직전에 죽으면 Activity가 재시도될 수 있어, Activity는 at-least-once를 전제로 멱등하게 설계해야 한다.
③ Activity Stub. "히스토리에 있으면 기록된 결과를 쓰고, 없으면 실제로 호출한다"는 분기를 구현한 적이 없는데도 동작하는 건, SDK가 Activity 인터페이스와 같은 시그니처의 stub(Workflow.newActivityStub())을 런타임에 만들어 호출을 가로채 Temporal 명령으로 바꾸기 때문이다. 스프링의 프록시처럼 동작하지만, Temporal 공식 용어는 stub이다.
Activity는 본질적으로 비결정적(랜덤, DB, 부수효과)인데, 워크플로우는 결정적이어야 한다. 이에 Temporal은, Activity를 1회차에만 실제로 실행하고 그 결과를 Event History에 영속화한 뒤, 재실행 때는 부르지 않고 기록된 값을 그대로 돌려준다. 비결정적 Activity를 워크플로우가 결정적으로 호출하는 셈이다.
물론 장점만 있는 건 아니다. Temporal Server 클러스터 운영(또는 클라우드) 비용, 워크플로우의 결정성 제약, 그리고 진행 중인 워크플로우가 Replay로 깨질 수 있어 코드 변경 시 버전 관리가 필요하다. 하지만 대부분 한 번만 치르는 비용이다. 구축해 두면 이후 워크플로우는 같은 기반 위에서 돌릴 수 있다.
승주님은 워크플로우가 복잡한 건 문제가 복잡해서가 아니라, 분산 환경이 만든 우발적 복잡도를 우리가 비즈니스 코드 안에서 직접 떠안아 왔기 때문이라고 했다. Temporal은 그 우발적 복잡도를 옆 함수나 다른 레이어가 아니라 런타임으로 옮긴다. 덕분에 소스코드에는 비즈니스만 남는다.

역시는 역시

컨퍼런스에 오면, 좋은 기운들을 받아가는 거 같다. 새로운 키워드를 얻기도 하고, 현장에서 고심했던 이야기를 엿보기도 하고, 영감을 얻기도 한다. 이번 Spring Camp 에 가장 많이 참여한 경력이 4~6년차라는 것도 흥미로웠다. 매번 대학생 혹은 주니어들만 많은 행사였었는데, 꾸준히 관심을 갖고 참여하는 듯 하여 좋았다. (그런데, 우테코 1~3기 크루들은 한명도 못봤.. )