Undertow
spider-silk-undertow는 같은 앱을 임베디드 Undertow에서 실행한다.
이 모듈이 따로 있는 이유는 spider-silk-tomcat과 같다: 특정 서버에 대한 종속은 core의 플래그 뒤가 아니라 아티팩트의 이름에 드러나야 한다.
세 서버의 비교와 선택 기준은 서버 고르기를 참고한다.
설치
dependencies {
implementation('net.benelog.spidersilk:spider-silk-undertow:1.1.0') {
exclude group: 'org.eclipse.jetty.ee10' // core's Jetty, unused here
}
}
이 모듈은 Jakarta EE 10 계열인 undertow-servlet 2.3.x에 의존한다.
undertow-servlet 2.3.x는 Servlet 6.0으로, core의 서블릿 API 및 Jetty의 ee10과 같은 수준이다.
Tomcat 11.0.x는 한 단계 위인 Servlet 6.1이다.
undertow-core에는 2.4.x가 있지만, 이 모듈에 필요한 절반인 undertow-servlet은 아직 2.4.x를 따라가지 않았다.
실행
new App()
.get("/hello/{name}", req -> WebResponse.text("Hello " + req.pathParam("name")))
.server((app, port) -> new UndertowServer(app).port(port))
.start(8080);
UndertowServer의 API는 JettyServer와 TomcatServer를 그대로 따른다:
new UndertowServer(app)
.port(8443)
.host("127.0.0.1")
.contextPath("/app")
.executor(Executors.newVirtualThreadPerTaskExecutor())
.multipart(new MultipartConfigElement(tmp, 10_485_760L, 10_485_760L, 1_048_576))
.stopTimeout(Duration.ofSeconds(20))
.shutdownHook(false)
.customizeDeployment(deployment -> deployment.setDefaultEncoding("UTF-8"))
.customizeBuilder(builder -> builder.setIoThreads(4).setWorkerThreads(64))
.start();
커스터마이저가 셋이 아니라 둘인 이유는 Undertow에 접근할 가치가 있는 객체가 둘뿐이기 때문이다: 서블릿 배포를 기술하는 DeploymentInfo와 서버를 기술하는 Undertow.Builder이다.
Undertow만의 것
셋 중 Undertow가 가장 쉬운 부분은 정상 종료(graceful shutdown) 하나뿐이다.
GracefulShutdownHandler는 자신이 통과시킨 요청을 세므로, stopTimeout(…)은 스레드 풀이 비워지기를 기다리지 않고 그 수가 0이 되기를 기다린다.
드레인이 요청을 세기 때문에, Tomcat에서와 달리 executor(…)가 드레인을 방해하지 않는다: 가상 스레드 실행기로 교체해도 드레인은 온전히 동작한다.
드레인이 끝나면 stop()은 Undertow의 워커에 100밀리초를 주고, 그 뒤에도 워커에서 실행 중인 것을 인터럽트한다.
Undertow는 그대로 두면 워커를 제한 없이 기다리므로, I/O가 아닌 일에 막힌 핸들러가 실행되는 동안 내내 종료를 붙들었을 것이다.
읽기는 클라이언트의 다음 바이트를 최대 30초 기다리고, 쓰기는 클라이언트가 바이트를 가져가기를 최대 30초 기다린다.
30초는 Jetty 커넥터의 유휴 타임아웃이다.
Undertow는 두 타임아웃 모두 스스로 설정하지 않으므로, 본문 도중에 멈춘 클라이언트가 워커 스레드를 영영 붙들었을 것이다.
customizeBuilder(builder → builder.setSocketOption(Options.READ_TIMEOUT, 60_000))는 읽기 타임아웃을 바꾸고, Options.WRITE_TIMEOUT은 쓰기 타임아웃을 바꾼다.
요청 본문은 모든 서버에서와 같이 core의 bodyLimits(…)와 멀티파트 설정이 제한한다.
이 모듈은 앱이 실행되기 전에 2MB를 넘는 모든 본문을 거부하던 Undertow 자체의 제한을 걷어 낸다.
폼 본문은 예외로, Tomcat의 maxPostSize인 2MB로 제한된다.
Undertow는 자체 제한 없이 폼을 파라미터로 파싱하므로, 그보다 큰 폼에는 앱이 실행되기 전에 413으로 응답한다.
Undertow의 워커 스레드는 스스로 JVM을 붙들어 두지 않으므로, start()는 stop()이 풀어 주는 비데몬 스레드를 하나 대기시킨다.
Tomcat 모듈에도 같은 비데몬 스레드가 필요하며, join()은 이 스레드를 기다린다.
sessions(boolean)은 없다: Undertow 배포는 항상 세션 매니저를 얻는다.
조용히 아무것도 하지 않는 메서드는 메서드가 없는 것보다 나쁘다.
세션 쿠키는 HttpOnly이고, 세션을 만드는 요청이 보안 요청이면 Jetty와 Tomcat에서와 같이 Secure이다.
세션은 Jetty에서와 같이 쿠키로만 찾는다.
URL의 ;jsessionid=는 무시되므로, 그것을 담은 링크가 링크를 따라간 사람의 세션을 고정할 수 없다.
정적 파일은 클래스패스 루트든 디렉터리든 여전히 core 자체의 StaticFiles가 제공하므로, Undertow의 리소스 매니저는 의도적으로 비워 둔다.
응답이 커밋된 뒤에 실패한 응답 본문은 연결을 닫는다.
Undertow는 그대로 두면 본문이 온전한 것처럼 응답을 끝내므로, 이 모듈은 응답이 시작된 뒤라면 연결을 닫는 서블릿 예외 핸들러를 설치한다.
그러면 클라이언트는 Jetty와 Tomcat에서와 같이 중단된 전송을 받는다.
setExceptionHandler(…)를 호출하는 customizeDeployment(…)는 그 핸들러를 대체한다.
로깅은 JBoss Logging을 거치며, JBoss Logging은 클래스패스에 slf4j가 있으면 slf4j를 쓴다. 따라서 이미 slf4j를 쓰는 애플리케이션은 브리지 없이 Undertow의 로그를 받는다.