Spider Silk
호출 스택은 얇게, 시그니처는 탄탄하게.
Spider Silk는 Jakarta Servlet API 위에 만들어진 웹 프레임워크다.
추구하는 가치
빠른 부팅
애플리케이션의 부팅 시간은 최소화한다. 오토스케일링되는 컨테이너, 서버리스 환경, 테스트 실행, 개발 서버의 재시작 등 개발과 운영의 주요 시점마다 부팅 시간은 반복해서 치르는 비용이기 때문이다.
설계와 기술 선택
이 절은 앞의 세 가치를 실현하기 위해 프레임워크가 택한 설계와 기술을 설명한다.
API의 단순성, 직관성 추구
Spider Silk는 코드 추적성을 위해 핵심 API를 직관적이고 단순하게 유지한다.
핵심 인터페이스인 핸들러는 HTTP 요청을 WebRequest 타입의 메서드 파라미터로 전달받아 응답을 WebResponse로 반환한다.
@FunctionalInterface
public interface Handler {
WebResponse handle(WebRequest request) throws Exception;
}
핸들러는 람다 표현식이나 메서드 참조로 URL과 HTTP 메서드와 매핑된다.
var deckRepository = new DeckRepository(dataSource);
var deckService = new DeckService(deckRepository);
var app = new App();
app.get("/decks/{deckId}", req -> {
long deckId = req.pathParamLong("deckId");
Deck deck = deckService.getDeck(deckId);
return WebResponse.template("deck", Model.of("deck", deck));
});
app.get("/api/decks", req -> {
JsonArray resBody = Json.array();
for (DeckSummary deck : deckService.deckSummaries()) {
resBody.add(Json.object()
.put("id", deck.id())
.put("name", deck.name()));
}
return WebResponse.json(resBody);
});
app.post("/api/decks", req -> {
String name = req.bodyJson(json -> json.asObject().getString("name"));
Deck deck = deckService.createDeck(name);
JsonObject resBody = Json.object()
.put("id", deck.id())
.put("name", deck.name());
return WebResponse.json(resBody).status(HttpStatus.CREATED);
});
app.start(8080);
리플렉션 없음
Spider Silk 프레임워크 내부 코드는 리플렉션을 쓰지 않는다. 애플리케이션 부팅 시점의 애너테이션 스캔, 프록시 생성이 없으므로 부팅 시간이 빨라진다. 프레임워크의 콜 스택을 따라가기가 쉽기 때문에 코드 추적성도 높아진다. 애플리케이션을 GraalVM 네이티브 이미지로 컴파일하기도 쉽다.
최소한의 콜 스택 추가
이 프레임워크는 주된 호출 경로인 핸들러에서 HttpServlet.service 위에 스택 프레임을 두 개만 더한다.
필터, 예외 핸들러, 스트리밍 본문처럼 프레임워크가 애플리케이션 코드를 부르는 다른 곳도 네 프레임을 넘지 않는다.
스택 트레이스의 프레임이 적을수록 문제가 생긴 코드를 찾아가기가 쉬워서 코드 추적성이 높다.
다음은 핸들러 안에서 찍은 스택 트레이스다.
java.lang.Throwable: where a handler stands
at com.example.decks.DeckRoutes.one(DeckRoutes.java:15) // the handler
at net.benelog.spidersilk.AppServlet.dispatch(AppServlet.java:299) // Spider Silk
at net.benelog.spidersilk.AppServlet.service(AppServlet.java:149) // Spider Silk
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java) // the Servlet API
at org.eclipse.jetty.ee10.servlet.ServletHolder.handle(ServletHolder.java) // Jetty, from here down
풋프린트를 최소화하는 기본 기술 스택
Spider Silk는 빠른 부팅과 코드 추적성을 기준으로 기본 기술 스택을 골랐다.
spider-silk-core는 웹 서버로 Jetty를, 템플릿 엔진으로 jte를 기본으로 포함한다.
의존성 하나만 추가하면 서버와 템플릿 엔진을 갖춘 애플리케이션을 바로 시작할 수 있다.
Jetty
기본 웹 서버는 임베디드 Jetty다. Jetty는 세 서버 가운데 서버 라이프사이클 관리를 자체 기능으로 가장 완성도 높게 제공한다. 그래서 Jetty를 쓰면 애플리케이션과 서버 사이에 놓이는 이 프레임워크의 코드가 Tomcat이나 Undertow를 쓸 때보다 적다. Jetty가 자체로 제공하는 기능은 다음과 같다.
-
정상 종료는 Jetty 자체의
stopTimeout이, 셧다운 훅은setStopAtShutdown이, JVM 유지는 Jetty의 non-daemon 스레드가 맡는다. -
디스크 위의 작업 디렉터리 없이 실행된다.
-
세션을
sessions(false)로 끌 수 있다.
jte
기본 템플릿 엔진은 jte다.
jte는 템플릿을 Java 소스로 컴파일한다.
그래서 ${deck.name()} 같은 표현식이 컴파일된 메서드 호출로 실행된다.
jte는 모델 값을 리플렉션으로 읽지 않으므로 리플렉션 없음의 원칙과 맞는다.
미리 컴파일한 템플릿은 런타임에 컴파일러가 필요 없으므로 GraalVM 네이티브 이미지에서도 잘 동작한다.
개발 중에 DirectoryCodeResolver로 소스 디렉터리를 읽게 하면 템플릿을 고친 뒤 다시 빌드하지 않아도 바로 반영된다(개발과 프로덕션).
확장성 있는 모듈 구조
Spider Silk는 기본 기술 스택 외에도 많이 쓰이는 기술을 사용할 수 있도록 선택 모듈을 제공한다.
모든 모듈은 spider-silk-core에 의존하고, core는 그 위에 지어진 JSON 라이브러리인 silk-json 말고는 어느 모듈에도 의존하지 않는다.
| 분류 | 모듈 | 내용 | 의존성 |
|---|---|---|---|
기본 |
|
프레임워크 자체. 기본 웹 서버는 Jetty, 기본 템플릿 엔진은 jte다. |
|
JSON |
core가 그 위에 지어진 JSON 라이브러리: 라이터, 리더, 트리, 그리고 |
실행 시점에는 JDK만 |
|
핸들러가 받는 |
core, |
||
웹 서버 |
같은 애플리케이션을 임베디드 Tomcat에서 실행한다. Tomcat을 이미 운영하는 조직이나 Spring Boot에서 옮겨 오는 애플리케이션에 맞는다. |
core, |
|
같은 애플리케이션을 임베디드 Undertow에서 실행한다. Undertow의 정상 종료는 요청을 세므로, 가상 스레드 executor에서도 드레인이 동작한다. |
core, |
||
템플릿 엔진 |
FreeMarker로 렌더링한다. |
core, |
|
Handlebars.java로 렌더링한다. |
core, |
||
Thymeleaf로 렌더링한다. |
core, |
||
WebSocket |
애플리케이션의 HTTP 라우트 곁에 WebSocket 엔드포인트를 매핑한다.
구조상 Jetty 전용이므로, |
core, |
|
테스트 |
테스트 범위용 |
core와 JDK만 (서블릿 API는 core처럼 컴파일 시점에만 필요) |
|
예제 |
|
플래시카드 학습 애플리케이션 |
core, spring-jdbc, H2 |
라우팅, 필터, 상태 페이지, JSON, 템플릿, 정적 파일, SSE, 멀티파트 업로드, 세션은 세 서버에서 같게 동작한다.
TomcatServer와 UndertowServer의 API는 JettyServer를 그대로 따르므로, 서버를 바꾸려면 App.server(…)에 넘기는 팩토리 한 줄만 고치면 된다.
템플릿 엔진은 App.templates(…)로 바꾼다.
엔진을 바꿔도 핸들러의 WebResponse.template(name, model) 호출은 그대로다.
모듈 사이의 의존 관계는 다음 그림과 같다.
개발 도구 생태계 연동
Spider Silk는 개발 도구 생태계와 조화를 이루도록 테스트, API 문서, 관측, 빌드, AI 코딩 에이전트를 위한 도구를 별도 모듈과 스킬로 제공한다.
-
spider-silk-test의WebTest하니스는 빈 포트에서 애플리케이션을 시작하고, 쿠키를 유지하는 클라이언트를 건넨다.
-
spider-silk-openapi는app.routes()가 돌려주는 라우트 목록을 OpenAPI 3.1 문서로 바꾼다. 라우트가 평범한 데이터이므로, 이 모듈은 리플렉션 없이 동작한다.
-
spider-silk-opentelemetry-agent는 서버 스팬마다 응답한 라우트로GET /api/decks/{deckId}처럼 이름을 붙이는 OpenTelemetry Java 에이전트의 확장이다. 이 모듈이 없으면 에이전트는/*에 매핑된 서블릿 하나만 보고 모든 스팬을GET /*로 이름 붙인다.
-
Gradle 플러그인
net.benelog.spidersilk는 jte 템플릿 사전 컴파일, Jib 컨테이너 이미지, GraalVM 네이티브 컴파일의 컨벤션을 담는다. -
spider-silk-maven-parent는 같은 패키징 컨벤션을 Maven의 부모 POM으로 제공한다.
-
에이전트 스킬은
skills/spider-silk/에 Agent Skills 형식으로 들어 있으며, 에이전트가 프레임워크를 추측하지 않고 의도대로 쓰도록 안내한다. Agent Skills는 개방 표준이고 이 스킬은 특정 도구에만 있는 기능을 쓰지 않으므로, 같은 디렉터리를 Claude Code, Codex, Cursor, GitHub Copilot 등 여러 AI 에이전트가 그대로 읽는다. 리포지토리가 플러그인 마켓플레이스를 겸하므로, Claude Code에서는 두 명령으로 설치한다:
/plugin marketplace add benelog/spider-silk
/plugin install spider-silk@spider-silk
리포지토리는 Codex 플러그인 마켓플레이스도 겸하므로, Codex에서도 두 명령으로 설치한다:
codex plugin marketplace add benelog/spider-silk
codex plugin add spider-silk@spider-silk
성능
부록의 벤치마크에서 Jetty 위의 Spider Silk는 단순 텍스트를 Spring Boot보다 약 1.42배, 레코드 100개의 JSON 목록을 약 1.7배 빠르게 처리하며, JVM 프로세스는 3분의 1의 시간에 부팅한다. 이 벤치마크는 두 프레임워크를 한 노트북에서 같은 JVM과 같은 부하로 실행했으며, 비교 대상은 Spring Web MVC, Jackson, Thymeleaf를 기본 설정으로 쓰는 Spring Boot 4.1이다.
| 측정 항목 | Spider Silk (Jetty) | Spring Boot |
|---|---|---|
단순 텍스트, 초당 요청 수 |
256,545 |
180,984 |
작은 JSON 객체, 초당 요청 수 |
256,318 |
180,642 |
레코드 100개의 JSON 목록, 초당 요청 수 |
163,817 |
95,200 |
요청 본문으로 받은 레코드 100개의 JSON 목록, 초당 요청 수 |
114,480 |
55,860 |
HTML 페이지, 초당 요청 수 (jte 대 Thymeleaf) |
197,418 |
63,927 |
JVM에서의 부팅 시간 |
295 ms |
902 ms |
GraalVM 네이티브 이미지의 부팅 시간 |
24 ms |
37 ms |
직렬화기가 대부분의 일을 하는 JSON 목록에서는 Spider Silk가 Jackson을 쓰는 Spring Boot보다 약 1.7배 많은 요청을 처리하고, 같은 목록을 요청 본문에서 읽을 때는 약 2배를 처리한다. 양쪽 모두 Thymeleaf를 쓰고 같은 컨테이너에서 실행하면 두 프레임워크의 페이지 렌더링 성능은 2% 이내로 차이가 나므로, HTML 페이지에서의 격차는 jte의 몫이다. GraalVM 네이티브 이미지로는 Spider Silk가 Spring Boot보다 JVM 처리량을 더 많이 유지하며, 이는 GraalVM CE 이미지와 프로파일 기반 최적화(PGO)로 빌드한 Oracle GraalVM 이미지 모두에서 그렇다. 아래 각 칸은 네이티브 이미지의 처리량을 같은 애플리케이션의 JVM 처리량에 대한 비율로 나타낸 값이다.
| 케이스 | Spider Silk, GraalVM CE | Spring Boot, GraalVM CE | Spider Silk, PGO | Spring Boot, PGO |
|---|---|---|---|---|
단순 텍스트 |
75% |
65% |
93% |
84% |
작은 JSON 객체 |
80% |
69% |
94% |
86% |
레코드 100개의 JSON 목록 |
60% |
14% |
92% |
27% |
요청 본문으로 받은 레코드 100개의 JSON 목록 |
73% |
14% |
90% |
27% |
HTML 페이지 (jte 대 Thymeleaf) |
71% |
57% |
92% |
89% |
양쪽 모두 Thymeleaf를 쓰면 Spider Silk의 이미지가 덜 유지한다: GraalVM CE에서 40%, PGO에서 79%로, Spring Boot의 57%와 89%보다 낮다. 측정 방법, 머신, 네이티브 이미지, 이 수치의 한계는 부록에서 다룬다.