WAS를 뭘로 갈지 정하는 회의에 불려 가면 거의 매번 같은 질문이 먼저 나온다. "그냥 Tomcat 쓰면 안 되나요?" 예전에 JBoss AS 7 운영 모드를 정리한 글을 쓸 때도 이야기는 결국 이 지점으로 돌아왔다. 그래서 이번에는 두 서버를 같은 축 위에 올려놓고 무엇이 실제로 다른지 순서대로 적어 둔다.
결론부터 말하자면, Tomcat은 Jakarta Servlet, Pages(JSP), Expression Language, WebSocket, Authentication, Annotations 사양을 구현하는 서블릿 컨테이너다. WildFly는 여기에 CDI, EJB, JPA, JMS, JTA까지 얹은 Jakarta EE 플랫폼 전체 구현체다. Spring Boot 기반 REST/웹 애플리케이션이라면 Tomcat이 단순하고 관리 포인트가 적다. EJB나 JMS, 분산 트랜잭션처럼 Jakarta EE 표준 API에 의존하거나 JBoss EAP 상용 지원이 필요하면 WildFly를 골라야 한다.
1. 서블릿 컨테이너와 애플리케이션 서버라는 용어부터
서블릿 컨테이너는 HTTP 요청을 받아 서블릿 객체의 생명주기를 관리하고 service()를 호출해 주는 런타임이다. 세션 관리, 요청 매핑, JSP 컴파일, WebSocket 업그레이드까지가 담당 범위다. 그 위에서 트랜잭션을 어떻게 묶을지, 의존성을 어떻게 주입할지에는 관여하지 않는다.
Jakarta EE 애플리케이션 서버는 서블릿 컨테이너를 품고 있으면서, 의존성 주입(CDI), 영속성(JPA), 메시징(JMS), 분산 트랜잭션(JTA), 비즈니스 컴포넌트(EJB), 표준 보안(Jakarta Security)까지 서버가 직접 제공한다. 애플리케이션은 구현체를 WAR에 넣지 않고 서버가 주는 것을 쓴다.
그래서 차이가 배포물 크기에서 먼저 드러난다. Tomcat에 올리는 Spring Boot WAR/JAR에는 Spring, Hibernate, Jackson이 전부 들어간다. WildFly에 올리는 Jakarta EE WAR에는 이런 라이브러리가 없다. 서버 쪽 Weld(CDI), Hibernate(JPA), RESTEasy(JAX-RS), ActiveMQ Artemis(JMS), Narayana(JTA) 서브시스템이 그 역할을 한다.
참고로 WildFly는 JBoss AS의 후속이다. JBoss AS 7까지 쓰던 이름을 WildFly 8부터 바꿨고, 상용 지원판이 Red Hat JBoss EAP다. WildFly가 업스트림이고 EAP가 그 스냅숏에 지원을 붙인 제품이라고 보면 계보가 맞는다.
2. 오해 하나: WildFly 안에 Tomcat이 들어 있지 않다
JBoss AS 시절 웹 컨테이너였던 JBoss Web은 Tomcat 포크였다. 그 기억 때문에 "WildFly는 Tomcat을 내장한 서버"라고 설명하는 자료가 아직 돌아다닌다. WildFly 8부터 웹 컨테이너는 Undertow로 완전히 교체됐다. 논블로킹 I/O 기반으로 새로 작성된 별개 프로젝트이고, HTTP 파싱부터 서블릿 처리까지 Tomcat 코드가 관여하지 않는다.
이 구분이 실무에서 중요해지는 지점은 튜닝과 트러블슈팅이다. Tomcat의 maxThreads, acceptCount, Connector 설정 지식은 WildFly에 그대로 옮겨지지 않는다. WildFly에서는 undertow 서브시스템의 리스너와 io 서브시스템의 worker(io-threads, task-max-threads)를 건드린다.
액세스 로그 설정 위치도, 스레드 덤프에 찍히는 스레드 이름도 다르다. 처음 WildFly를 잡았을 때 server.xml부터 찾다가 시간을 버렸다.
3. 버전과 사양 대응
공식 문서 기준으로 현재 상태를 정리하면 아래와 같다. 버전 번호는 확인 시점의 최신 릴리스이며 저장소 상태에 따라 다를 수 있다.
| 항목 | Apache Tomcat | WildFly |
|---|---|---|
| 최신 안정 버전 | 11.0.25 (10.1.59, 9.0.121 병행 지원) | 41.0.0.Final (2026-07-16) |
| 사양 레벨 | Servlet 6.1, JSP 4.0, EL 6.0, WebSocket 2.2, Authentication 3.1, Annotations 3.0 | Jakarta EE 11 Platform, Web Profile, Core Profile 인증. EE 10 변형판은 41.0.1.Final까지만 제공되고 WildFly 42에서 중단 예정 |
| 필요 Java | 11.0.x는 Java 17+, 10.1.x는 Java 11+, 9.0.x는 Java 8+ | Java SE 17 이상, EE 11 인증은 SE 17과 21 기준, SE 25 권장 |
| 네임스페이스 | 10 이상 jakarta.*, 9 이하 javax.* | EE 10 변형판과 EE 11 배포판 모두 jakarta.* |
| 웹 컨테이너 | Catalina + Coyote | Undertow |
| 설정 방식 | conf/server.xml, context.xml, web.xml 직접 편집 | standalone.xml / domain.xml + jboss-cli, 관리 콘솔 |
| 관리 인터페이스 | 배포판에 포함된 웹앱(manager, host-manager), 역할 부여와 접근 제한 설정 필요 | 관리 콘솔 9990 포트, jboss-cli.sh, 관리 REST API |
| 배포 단위 | WAR | WAR, EAR, EJB JAR |
| 클러스터 관리 | 인스턴스별 개별 설정 | domain 모드로 중앙 관리 가능 |
| 라이선스 | Apache License 2.0 | Apache License 2.0 (30 이전은 LGPL 2.1) |
javax.* 네임스페이스를 쓰는 계열은 9.0.x이고, 그 지원 종료는 2027년 3월 31일보다 이르지 않을 예정이다. 다만 9.0.x가 마지막은 아니다. 공식 문서에는 javax.* 확장 지원을 위한 9.1.x 브랜치가 예정돼 있다고 적혀 있고, 9.1.x의 지원 종료 시점은 아직 정해지지 않았으며 최소 12개월 전에 미리 고지된다. 즉시 마이그레이션이 어려운 레거시라면 이 브랜치를 시간 확보용 카드로 두고 계획을 잡을 수 있다.
Java 버전은 미리 넉넉하게 잡아 두는 쪽이 편하다. WildFly는 41 릴리스 노트에서 향후 1년 내 어느 릴리스에서 Java SE 17 지원을 내릴 가능성을 언급했다. 새로 구축한다면 21이나 25로 시작하자.
4. 설치 후 디렉터리 구조와 기동
압축을 풀었을 때 보이는 구조부터 성격이 다르다. Tomcat은 디렉터리 여덟 개로 끝난다.
$ tar xzf apache-tomcat-11.0.25.tar.gz -C /opt $ ls /opt/apache-tomcat-11.0.25 bin conf lib logs temp webapps webapps-javaee work
실제 ls 결과에는 LICENSE, NOTICE, RELEASE-NOTES, RUNNING.txt 같은 텍스트 파일도 같이 나오는데, 위에서는 구조를 보려고 디렉터리만 추렸다.
버전과 실행 환경 확인은 version.sh로 한다.
$ ./bin/version.sh Using CATALINA_BASE: /opt/apache-tomcat-11.0.25 Using CATALINA_HOME: /opt/apache-tomcat-11.0.25 Using CATALINA_TMPDIR: /opt/apache-tomcat-11.0.25/temp Using JRE_HOME: /usr/lib/jvm/java-21-openjdk Using CLASSPATH: /opt/apache-tomcat-11.0.25/bin/bootstrap.jar:/opt/apache-tomcat-11.0.25/bin/tomcat-juli.jar Server version: Apache Tomcat/11.0.25 Server number: 11.0.25.0 OS Name: Linux JVM Version: 21.0.x+xx JVM Vendor: Red Hat, Inc.
기동은 startup.sh이고, 포그라운드로 붙여 로그를 바로 보려면 catalina.sh run을 쓴다. systemd 유닛으로 감쌀 때는 후자가 다루기 쉽다.
$ ./bin/catalina.sh run ... INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [NNN] milliseconds
관리 웹앱은 따로 내려받을 것이 없다. manager와 host-manager는 바이너리 배포판의 webapps/에 이미 들어 있고, 필요한 작업은 두 가지다. 먼저 conf/tomcat-users.xml에 역할(manager-gui, manager-script, admin-gui 등)을 가진 사용자를 만들어야 화면이나 스크립트 API에 들어갈 수 있다.
다음은 접근 제한이다. 두 웹앱의 META-INF/context.xml에는 로컬호스트만 허용하는 RemoteAddrValve가 기본으로 걸려 있어서, 운영자 PC 대역에서 붙이려면 이 밸브를 손봐야 한다. IP 정규식을 늘려 쓰기보다 org.apache.catalina.valves.RemoteCIDRValve로 바꿔 CIDR 표기로 허용 대역을 적는 편이 관리하기 낫다.
<Context antiResourceLocking="false" privileged="true">
<Valve className="org.apache.catalina.valves.RemoteCIDRValve"
allow="127.0.0.1/32, ::1/128, 10.0.10.0/24" />
</Context>
WildFly는 최상위 구조부터 서브시스템 서버 티가 난다.
$ unzip -q wildfly-41.0.0.Final.zip -d /opt $ ls /opt/wildfly-41.0.0.Final appclient bin docs domain modules standalone welcome-content $ ls /opt/wildfly-41.0.0.Final/standalone configuration data deployments lib log tmp
modules 디렉터리가 WildFly 구조의 핵심이다. JBoss Modules가 여기서 모듈 단위로 클래스로더를 격리해 로드한다. Tomcat처럼 lib에 JAR을 던져 넣는 방식이 아니라서, JDBC 드라이버 하나 추가하는 데도 모듈 등록이라는 절차가 붙는다.
기동은 단독 모드가 standalone.sh, 도메인 모드가 domain.sh다.
$ ./bin/standalone.sh -c standalone-full.xml -b 0.0.0.0 -bmanagement 0.0.0.0 ========================================================================= JBoss Bootstrap Environment JBOSS_HOME: /opt/wildfly-41.0.0.Final JAVA: /usr/lib/jvm/java-21-openjdk/bin/java ========================================================================= ... INFO [org.jboss.as] (MSC service thread 1-2) WFLYSRV0049: WildFly Full 41.0.0.Final starting ... INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: WildFly Full 41.0.0.Final started in [NNN]ms INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0060: Http management interface listening on http://0.0.0.0:9990/management
위 명령의 -bmanagement 0.0.0.0은 관리 인터페이스를 모든 인터페이스에 여는 옵션이다. 관리 포트로는 배포, 설정 변경, 서버 정지까지 되기 때문에 공개망에서 이렇게 띄우면 안 된다. 검증 장비에서만 쓰고, 열기 전에 add-user.sh로 관리 사용자를 먼저 만들어 둬야 한다. 사용자가 없는 상태에서는 로컬 인증만 통과하므로 원격 접속은 어차피 거부되지만, 포트를 노출한 것 자체가 공격 표면이 된다.
여기서 한 번 걸리는 지점이 설정 프로파일이다. -c를 생략하면 standalone.xml로 뜨는데, 이 프로파일에는 메시징(JMS) 서브시스템이 빠져 있다. MDB나 JMS 큐를 쓰는 애플리케이션을 올리면 배포가 실패한다. JMS가 필요하면 standalone-full.xml, 클러스터링까지 필요하면 standalone-full-ha.xml을 지정해야 한다.
5. 배포 방식
Tomcat은 webapps/에 WAR를 복사하면 끝난다. 자동 배포가 켜져 있으면 스캐너가 압축을 풀고 컨텍스트를 올린다.
$ cp sarc.war /opt/apache-tomcat-11.0.25/webapps/ $ tail -f logs/catalina.out INFO [Catalina-utility-1] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive [/opt/apache-tomcat-11.0.25/webapps/sarc.war] INFO [Catalina-utility-1] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [/opt/apache-tomcat-11.0.25/webapps/sarc.war] has finished in [NNN] ms
WildFly의 standalone/deployments/도 겉보기는 같지만, 스캐너가 마커 파일로 상태를 기록한다는 점이 다르다.
$ cp sarc.war /opt/wildfly-41.0.0.Final/standalone/deployments/ $ ls standalone/deployments/ sarc.war sarc.war.deployed
마커는 .dodeploy(배포 요청), .isdeploying(진행 중), .deployed(성공), .failed(실패), .undeployed(내림)로 구성된다. 배포가 깨지면 sarc.war.failed 파일이 생기고, 그 안에 실패 사유가 그대로 들어간다. 로그를 뒤지기 전에 이 파일부터 열어 보는 습관을 들이면 시간이 줄어든다. 재배포는 파일을 다시 복사하는 대신 touch sarc.war.dodeploy로도 된다.
운영 환경이라면 디렉터리 스캐너보다 CLI 배포가 낫다. 배포 성공 여부가 종료 코드로 돌아오므로 배포 스크립트에서 판정하기 쉽다.
$ ./bin/jboss-cli.sh --connect --command="deploy /tmp/sarc.war --force"
$ ./bin/jboss-cli.sh --connect
[standalone@localhost:9990 /] :read-attribute(name=server-state)
{
"outcome" => "success",
"result" => "running"
}
[standalone@localhost:9990 /] deployment-info
NAME RUNTIME-NAME PERSISTENT ENABLED STATUS
sarc.war sarc.war true true OK
배포 단위 자체도 차이가 있다. Tomcat은 WAR만 받는다. EAR을 webapps/에 넣어도 아무 일도 일어나지 않는다. EJB JAR과 WAR을 묶은 EAR을 그대로 올려야 하는 레거시라면 이 시점에서 선택지가 WildFly로 좁혀진다.
6. 데이터소스 붙이기
Tomcat은 conf/context.xml이나 애플리케이션의 META-INF/context.xml에 Resource를 선언하고, 드라이버 JAR을 lib/에 둔다. 커넥션 풀은 기본 내장인 Tomcat JDBC Pool 또는 DBCP2를 쓴다.
<Resource name="jdbc/sarc" auth="Container"
type="javax.sql.DataSource"
factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
driverClassName="org.postgresql.Driver"
url="jdbc:postgresql://localhost:5432/sarc"
username="sarc" password="secret"
maxActive="20" maxIdle="10" maxWait="10000"
validationQuery="SELECT 1" testOnBorrow="true" />
속성명이 풀 구현에 묶여 있다는 점을 놓치기 쉽다. factory가 org.apache.tomcat.jdbc.pool.DataSourceFactory면 maxActive, maxIdle, maxWait를 써야 한다. maxTotal과 maxWaitMillis는 DBCP2(org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory)에서만 유효한 이름이라 Tomcat JDBC Pool에서는 그냥 무시된다.
무시되는 방식이 함정이다. 에러도 경고도 없이 해당 항목만 기본값으로 동작한다. Tomcat JDBC Pool의 기본값은 maxActive 100, maxWait 30000ms다. 그래서 maxTotal="20"처럼 작은 값을 의도해 적었다면 풀이 20으로 제한되는 게 아니라 100까지 열린다. DB 쪽 max_connections를 넘겨 커넥션 거부가 나는 형태로 뒤늦게 드러나기도 한다. 설정을 넣었으면 실제 최대 커넥션 수를 JMX나 DB 세션 수로 한 번 확인해 두자.
여기서 type이 javax.sql.DataSource인 것은 오타가 아니다. JDBC는 Java SE API라서 Jakarta EE 네임스페이스 변경 대상이 아니다. Tomcat 11에서도 그대로 javax.sql이다.
WildFly는 드라이버를 배포물로 올린 뒤 데이터소스를 정의한다. JDBC 4 규격 드라이버는 deployments/에 JAR을 넣으면 드라이버로 자동 인식된다.
$ cp postgresql-42.7.x.jar standalone/deployments/
$ ./bin/jboss-cli.sh --connect
[standalone@localhost:9990 /] data-source add \
--name=SarcDS \
--jndi-name=java:jboss/datasources/SarcDS \
--driver-name=postgresql-42.7.x.jar \
--connection-url=jdbc:postgresql://localhost:5432/sarc \
--user-name=sarc --password=secret \
--min-pool-size=5 --max-pool-size=20 \
--valid-connection-checker-class-name=org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLValidConnectionChecker
[standalone@localhost:9990 /] /subsystem=datasources/data-source=SarcDS:test-connection-in-pool
{
"outcome" => "success",
"result" => [true]
}
--driver-name은 배포한 JAR 파일명과 정확히 일치해야 한다. 여기서 오타가 나면 배포 시점이 아니라 데이터소스 기동 시점에 실패한다. 운영 서버에서는 드라이버를 module add로 정식 모듈 등록하는 방식을 더 많이 쓰지만, 검증 단계에서는 JAR 배포 방식이 빠르다.
CLI로 넣은 설정은 standalone.xml에 그대로 반영되어 영속화된다. 이 점이 Tomcat과 큰 차이다. Tomcat은 XML을 손으로 고치고 재기동하는 것이 기본이고, WildFly는 CLI나 콘솔로 바꾸면 대부분 런타임에 반영되며 파일에도 기록된다.
7. 운영 모드와 다중 인스턴스
Tomcat에서 인스턴스를 여러 개 띄우려면 CATALINA_HOME은 공유하고 CATALINA_BASE를 인스턴스마다 따로 둔다. 설정은 인스턴스별로 각각 관리되고, 중앙에서 한 번에 배포하거나 설정을 밀어 넣는 기능은 없다. 그 역할은 Ansible 같은 외부 도구가 맡는다.
WildFly에는 domain 모드가 있다. 도메인 컨트롤러가 domain.xml에 프로파일과 서버 그룹을 정의하고, 각 호스트의 호스트 컨트롤러가 서버 인스턴스를 띄운다. 서버 그룹에 배포하면 그 그룹에 속한 모든 인스턴스에 한 번에 반영된다. 이 구조는 JBoss AS 7 운영 모드 글에서 다뤘고, 골격은 WildFly 41까지 그대로다.
다만 domain 모드가 클러스터링과 같은 말은 아니다. 세션 복제나 부하 분산은 별개 설정이고, domain 모드는 관리 평면을 하나로 묶는 기능이다. 인스턴스가 두세 개라면 standalone 모드에 배포 자동화를 붙이는 쪽이 관리하기 쉽다.
8. 자주 만나는 오류와 해결
Tomcat 10 이상에 javax.* WAR을 올렸을 때
Tomcat 9까지 잘 돌던 WAR을 그대로 옮기면 서블릿이 매핑되지 않아 404가 나거나, 로그에 아래 형태의 에러가 남는다.
java.lang.NoClassDefFoundError: javax/servlet/http/HttpServlet
at java.base/java.lang.ClassLoader.defineClass1(Native Method)
...
Caused by: java.lang.ClassNotFoundException: javax.servlet.http.HttpServlet
Tomcat 10부터 서블릿 API 패키지가 jakarta.*로 바뀌었기 때문이다. 소스를 고칠 수 없다면 WAR을 webapps/가 아니라 webapps-javaee/에 넣는다. Tomcat이 내장된 Apache Tomcat Migration Tool for Jakarta EE로 바이트코드를 변환해 webapps/로 복사한다. Tomcat 11에는 이 도구 1.0.12가 포함돼 있고, 오프라인 변환용으로 따로 내려받을 수도 있다.
변환이 만능은 아니다. 리플렉션으로 클래스명 문자열을 조립하거나, 설정 파일에 javax.servlet.* 클래스명을 문자열로 박아 둔 코드는 그대로 남는다. 이런 애플리케이션은 결국 소스를 고쳐야 한다.
WildFly 배포가 .failed로 끝날 때
자주 만나는 원인 중 하나는 애플리케이션이 참조하는 JNDI 이름의 데이터소스가 서버에 없는 경우다.
$ cat standalone/deployments/sarc.war.failed
{"WFLYCTL0080: Failed services" => {"jboss.deployment.unit.\"sarc.war\".WeldStartService" => ...}}
WFLYCTL0412: Required services that are not installed:
["jboss.naming.context.java.jboss.datasources.SarcDS"]
persistence.xml의 jta-data-source 값과 서버에 정의한 JNDI 이름이 한 글자라도 다르면 이 에러가 난다. 앞의 data-source add로 만든 이름과 대조하는 것이 첫 확인 항목이다.
물론 .failed가 데이터소스 문제만 가리키는 것은 아니다. 서드파티 라이브러리를 모듈로 등록하지 않아 ClassNotFoundException이 나는 모듈 누락, 같은 타입의 CDI 빈이 중복되거나 주입 대상을 못 찾는 배포 오류, persistence.xml의 엔티티 매핑이나 방언 설정이 틀린 JPA 초기화 실패, 선택한 프로파일에 해당 서브시스템이 없어 서비스가 아예 설치되지 않는 경우도 흔하다.
어느 쪽이든 판단의 출발점은 같다. .failed 파일의 Failed services와 Required services that are not installed 두 항목을 먼저 읽는다.
관리 콘솔에 접속되지 않을 때
WildFly는 관리 인터페이스가 기본으로 127.0.0.1에만 바인딩되고, 관리 사용자도 없는 상태로 배포된다. 원격에서 콘솔을 열려면 사용자를 먼저 만들어야 한다.
$ ./bin/add-user.sh What type of user do you wish to add? a) Management User (mgmt-users.properties) b) Application User (application-users.properties) (a): a Username : admin Password : Added user 'admin' to file '.../standalone/configuration/mgmt-users.properties'
사용자를 만든 뒤에도 4절에서 적은 대로 관리 포트를 공개망에 그대로 열지는 않는다. 내부 대역에만 노출하거나 SSH 터널로 접근한다. Tomcat의 manager 웹앱도 같은 원칙으로 다룬다.
포트가 겹칠 때
한 서버에 WildFly 인스턴스를 여러 개 띄우면 8080과 9990이 충돌한다. 포트 오프셋으로 한 번에 밀어 준다.
$ ./bin/standalone.sh -Djboss.socket.binding.port-offset=100 \
-Djboss.server.base.dir=/opt/wildfly-inst2/standalone
오프셋 100이면 HTTP는 8180, 관리 포트는 10090이 된다. Tomcat에서는 server.xml의 Connector port와 Server port(종료 포트, 기본 8005)를 인스턴스마다 직접 고쳐야 한다. 종료 포트를 안 고쳐서 두 번째 인스턴스가 기동에 실패하는 경우를 꽤 자주 본다.
9. 그래서 무엇을 골라야 하나
Tomcat을 고르는 경우. Spring Boot 또는 Spring Framework 기반이면 고민할 것이 없다. 프레임워크가 DI, 트랜잭션, 영속성, REST를 전부 제공하므로 서버가 같은 기능을 또 제공할 이유가 없다. 내장 톰캣으로 JAR 하나만 굴리는 구성이라면 서버 설치 자체가 사라진다. 이 선택지는 Tomcat과 Spring Boot 내장 톰캣의 차이 글에서 따로 정리했다. 정적 콘텐츠와 JSP 위주 사내 애플리케이션도 Tomcat으로 충분하다.
WildFly를 고르는 경우. EJB, JMS, JTA 분산 트랜잭션(XA)처럼 컨테이너가 관리해야 하는 기능을 쓰는 애플리케이션이다. 특히 두 개 이상의 리소스를 하나의 트랜잭션으로 묶어야 한다면 Narayana 같은 트랜잭션 매니저가 서버에 있는 쪽이 훨씬 낫다. Jakarta EE 표준 API만으로 작성된 레거시를 그대로 이관해야 하거나, EAR 단위 배포가 전제이거나, JBoss EAP 상용 지원이 계약 조건이라면 역시 WildFly다.
메모리와 기동 시간은 WildFly 쪽이 더 든다. 서브시스템을 함께 올리기 때문이다. 다만 절대 수치는 프로파일과 배포 애플리케이션에 따라 크게 달라지므로, 판단이 필요하면 대상 프로파일로 직접 재야 한다.
서버 크기가 걸린다면 WildFly 쪽에도 줄이는 방법이 있다. Galleon 프로비저닝으로 필요한 서브시스템만 담아 서버를 구성할 수 있고, wildfly-maven-plugin으로 애플리케이션을 포함한 bootable JAR도 만들 수 있다. 컨테이너 이미지 크기가 문제라면 이 방향을 먼저 검토해 볼 만하다.
중간 지대를 원한다면 Apache TomEE가 있다. Tomcat에 Jakarta EE Web Profile 구현을 얹은 배포판으로, CDI와 JPA는 필요하지만 EJB 원격 호출이나 도메인 관리까지는 필요 없는 경우에 맞는다.
10. 정리
Tomcat과 WildFly는 같은 카테고리의 경쟁 제품이라기보다 담당 범위가 다른 두 계층에 가깝다. Tomcat은 HTTP와 서블릿까지만 책임지고 나머지는 애플리케이션이 가져오게 하며, WildFly는 Jakarta EE 전체를 서버가 제공하고 그만큼 설정과 관리 도구를 갖췄다.
선택 기준은 한 문장으로 줄일 수 있다. 애플리케이션이 표준 API를 컨테이너에서 받아 쓰도록 작성됐는지, 아니면 프레임워크를 통째로 들고 다니는지. 전자면 WildFly, 후자면 Tomcat이다. 이 질문에 답이 나오면 나머지 비교 항목은 부수적인 문제가 된다.