Tomcat
spider-silk-tomcat은 core에 번들된 Jetty 대신 임베디드 Tomcat에서 같은 앱을 실행한다.
이 모듈이 따로 있는 이유는 특정 서버에 대한 종속이 core 안이 아니라 아티팩트의 이름에 드러나야 하기 때문이다.
어느 쪽이든 core에는 서버 고유의 것이 필요 없다: AppServlet은 평범한 서블릿이고, WebServer는 메서드 네 개로 이루어진다.
세 서버의 비교와 선택 기준은 서버 고르기를 참고한다.
설치
dependencies {
implementation('net.benelog.spidersilk:spider-silk-tomcat:1.1.0') {
exclude group: 'org.eclipse.jetty.ee10' // core's Jetty, unused here
}
}
이 모듈은 Servlet 6.1 계열인 tomcat-embed-core 11.0.x에 의존한다.
Servlet 6.1은 core의 서블릿 API, Jetty의 ee10, Undertow 2.3.x보다 한 단계 위이며, 이들은 모두 Servlet 6.0이다.
core는 Servlet 6.1이 추가한 것을 아무것도 호출하지 않으므로, 같은 애플리케이션이 세 서버 모두에서 실행된다.
Jetty 제외는 선택 사항이다. 쓰지 않는 서버를 클래스패스에서 치워 둘 뿐이다.
실행
new App()
.get("/hello/{name}", req -> WebResponse.text("Hello " + req.pathParam("name")))
.server((app, port) -> new TomcatServer(app).port(port))
.start(8080);
TomcatServer의 API는 JettyServer를 메서드 하나하나 그대로 따르므로, 서버를 바꾸려면 팩토리의 한 줄만 고치면 된다:
new TomcatServer(app)
.port(8443)
.host("127.0.0.1")
.contextPath("/app")
.baseDir(Path.of("/var/tmp/tomcat")) // default: a temp dir, deleted on stop
.executor(Executors.newVirtualThreadPerTaskExecutor())
.multipart(new MultipartConfigElement(tmp, 10_485_760L, 10_485_760L, 1_048_576))
.stopTimeout(Duration.ofSeconds(20))
.shutdownHook(false)
.customizeConnector(connector -> connector.setProperty("maxThreads", "400"))
.customizeContext(context -> context.addParameter("mode", "production"))
.customizeTomcat(tomcat -> tomcat.getHost().setAutoDeploy(false))
.start();
core가 제공하는 모든 것이 그대로 동작한다: 라우팅, 필터, 상태 페이지, JSON, 템플릿, 정적 파일, SSE, 멀티파트 업로드, 세션, 그리고 WebTest다.
WebTest는 app.start(0)으로 앱을 시작하므로, 앱이 Tomcat을 지정하면 테스트 스위트는 Tomcat에서 실행되고 그 테스트는 바뀌지 않는다.
Tomcat 고유의 것
정상 종료(graceful shutdown)는 여기서 직접 구현했는데, Tomcat에는 자체 stopTimeout이 없기 때문이다.
엔진에 붙은 밸브가 진행 중인 요청을 세고, stop()이 시작되면 새 요청마다 Connection: close와 함께 503으로 답한다.
그다음 stop()은 타임아웃 안에서 그 수가 0이 되기를 기다리며, 이는 Undertow의 GracefulShutdownHandler가 드레인하는 방식이다.
드레인은 스레드가 아니라 요청을 세므로, 가상 스레드 실행기를 포함해 어떤 실행기에서도 동작한다.
executor(…)로 넘긴 실행기는 애플리케이션의 것이므로, stop()은 재시작이나 그 위의 다른 작업을 위해 그 실행기를 계속 돌게 둔다.
드레인을 위해 커넥터를 일시 정지하지는 않는다.
일시 정지된 커넥터는 keep-alive 연결이 새 요청을 보내면 답하지 않고 연결을 닫으며, 클라이언트는 그런 요청을 멱등일 때만 재시도한다.
종료 훅도 직접 구현했다.
Jetty의 setStopAtShutdown은 스스로 등록을 해제한다.
이 모듈의 훅은 시작 시 등록되고 정지 시 다시 제거되므로, 테스트마다 서버를 시작하는 스위트에도 아무것도 쌓이지 않는다.
Tomcat은 디스크에 catalina.base 디렉터리를 필요로 한다.
기본값은 시작 시 만들어지고 정지 시 삭제되는 임시 디렉터리이므로, 빌드 옆에 아무것도 남지 않는다.
baseDir(…)는 이 기본값을 직접 정한 디렉터리로 바꾸며, 그 디렉터리는 정지 시 그대로 남는다.
sessions(boolean)은 없다: Tomcat의 StandardContext는 시작 시 세션 관리자를 설치하고 빠져나갈 길을 주지 않는다.
조용히 아무 일도 하지 않는 메서드는 메서드가 없는 것보다 나쁘다.
로깅은 JULI를 거치므로, 이를 애플리케이션 자신의 로깅으로 보내려면 jul-to-slf4j와 브리지 핸들러가 필요하다.
로깅에 의존성과 브리지를 설치하는 두 줄이 있다.
직접 시작하지 않은 Tomcat에 배포하기
이 페이지의 내용은 독립 실행 Tomcat에 넣은 WAR에는 해당하지 않는다.
WAR 배포에는 모듈이 전혀 필요 없다.
배포가 설명하는 대로 AppServlet을 직접 매핑하고 임베디드 서버를 제외한다.