서블릿 컨테이너 수동 조립
Tomcat 커넥터·컨텍스트·서블릿 매핑·생명주기를 직접 구성하고 Spring DispatcherServlet 연결 요소의 책임 범위를 확인하며 포트 충돌·스레드·종료·임시 디렉터리 실패를 진단합니다.
내장 서버도 내부에는 커넥터와 서블릿 컨테이너가 있습니다.
Tomcat을 직접 띄워 보면 HTTP 소켓, 컨텍스트 경로, 서블릿 등록, 시작·중지를 누가 책임지는지 드러납니다.
Boot의 ServletWebServerApplicationContext와 서버 팩토리 커스터마이저는 이 반복을 일관되게 조립하는 추상화입니다.
커넥터·컨테이너 역할
서버는 주소·포트·프로토콜·연결 대기량을 관리하고 서블릿 컨테이너는 URL 매핑, 필터 체인, 세션, 서블릿 생명주기를 관리합니다. 둘을 “Tomcat” 하나로 뭉개면 바인딩 실패와 매핑 404를 구분하기 어렵습니다.
package board.manual;
import java.io.IOException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public final class HelloServlet extends HttpServlet {
@Override
protected void doGet(
HttpServletRequest request,
HttpServletResponse response
) throws IOException {
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().write("board-ok");
}
}서블릿 객체는 컨테이너가 여러 스레드에서 호출할 수 있으므로 요청별 값을 필드에 저장하지 않습니다. 응답 커밋 전에 상태와 콘텐츠 타입을 정합니다.
Tomcat 수동 조립
다음 메인은 임시 기반 디렉터리와 루트 컨텍스트를 만들고 서블릿을 /healthz에 등록합니다.
실행 뒤 GET 요청은 board-ok를 반환합니다.
package board.manual;
import java.nio.file.Files;
import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;
public final class ManualTomcat {
public static void main(String[] args) throws Exception {
var base = Files.createTempDirectory("board-tomcat");
var tomcat = new Tomcat();
tomcat.setBaseDir(base.toString());
tomcat.setPort(8080);
tomcat.getConnector();
Context context = tomcat.addContext("", base.toString());
Tomcat.addServlet(context, "health", new HelloServlet());
context.addServletMappingDecoded("/healthz", "health");
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
tomcat.stop();
tomcat.destroy();
} catch (Exception failure) {
failure.printStackTrace(System.err);
}
}));
tomcat.start();
System.out.println("server=http://127.0.0.1:8080/healthz");
tomcat.getServer().await();
}
}server=http://127.0.0.1:8080/healthz
GET /healthz -> 200
body=board-okgetConnector() 호출로 기본값 커넥터가 생성되게 합니다.
포트 0을 사용하면 OS가 고른 실제 로컬 포트를 커넥터에서 읽어 테스트 격리를 만들 수 있습니다.
Spring MVC 연결 요소
애노테이션 구성 컨텍스트에 컨트롤러, 핸들러 매핑, 어댑터, 컨버터를 등록하고 DispatcherServlet 생성자에 웹 애플리케이션 컨텍스트를 전달합니다. 서블릿 컨텍스트 생명주기와 Spring 컨텍스트 종료를 같은 서버 종료에 묶어야 합니다.
Boot는 MVC 스타터와 자동 구성으로 이 빈 그래프를 제공하고 서블릿 등록 빈을 구성합니다.
사용자가 DispatcherServlet 빈을 교체하거나 경로를 바꾸면 매핑과 오류 디스패치도 함께 검증합니다.
루트 컨텍스트와 자식 서블릿 컨텍스트를 둘 수 있지만 작은 내장 애플리케이션은 하나의 웹 컨텍스트로 단순화할 수 있습니다. 여러 디스패처를 사용할 때 빈 가시성과 경로 소유권을 명시합니다.
시작 실패 진단
BindException이면 포트를 누가 점유했는지 확인하고 임의로 다른 운영 환경 포트로 우회하지 않습니다.
기반 디렉터리 생성 실패는 파일 시스템 권한과 디스크 상태를 봅니다.
서버는 시작했지만 404라면 커넥터보다 컨텍스트 경로와 서블릿 매핑을 확인합니다.
Spring 컨텍스트 새로 고침 실패가 난 뒤 Tomcat이 열린 채 남지 않게 부분 초기화 정리를 수행합니다. 시작 단계별 리소스를 역순으로 닫습니다. 종료 훅 하나에만 기대면 테스트 컨텍스트 반복 실행이 느려집니다.
스레드 풀 최댓값을 크게 한다고 처리량이 무조건 늘지는 않습니다. 하위 시스템의 DB 풀과 CPU, 요청 타임아웃을 함께 조정합니다. 단계적 종료는 새 요청 수락을 멈추고 처리 중인 요청이 제한 시간 안에 끝나기를 기다린 뒤 강제로 종료합니다.
Boot 사용자 정의 지점
포트와 압축은 속성으로, 커넥터별 옵션은 웹 서버 팩토리 커스터마이저로 바꿀 수 있습니다. Tomcat 팩토리 빈을 통째로 대체하면 Boot가 제공하는 생명주기와 SSL 번들, 단계적 종료 지원을 놓칠 수 있습니다.
통합 테스트는 임의 포트로 실제 소켓을 열고 HTTP 상태·헤더·본문을 확인합니다. 컨트롤러 모의만으로 커넥터 인코딩과 필터 디스패치, 오류 페이지를 검증했다고 말하지 않습니다.
관찰 결과는 커넥터의 로컬 포트가 0보다 크고, /healthz 응답은 200과 UTF-8 board-ok, 등록하지 않은 /missing 응답은 404여야 합니다.
중지한 뒤 같은 포트로 새 연결을 시도하면 거절되어야 하며, 서버 스레드가 테스트 프로세스를 붙잡아 타임아웃을 일으킨다면 생명주기 관리가 완전하지 않은 것입니다.
selectedPort > 0 = true
GET /healthz = 200 board-ok
GET /missing = 404
connection after close = refused
remaining non-daemon server threads = 0연습 문제
수동 서버를 포트 0에서 시작하고 실제 선택된 포트로 GET 요청한 뒤 반드시 중지·소멸하는 테스트 도구를 작성하세요. 매핑 누락과 포트 바인딩 실패를 서로 다른 신호로 보고해야 합니다.
해설 보기
서버 리소스를 소유한 래퍼를 AutoCloseable로 만들면 테스트의 try-with-resources 구문에서 종료를 보장할 수 있습니다.
package board.manual;
import org.apache.catalina.startup.Tomcat;
public final class RunningTomcat implements AutoCloseable {
private final Tomcat tomcat;
public RunningTomcat(Tomcat tomcat) {
this.tomcat = tomcat;
}
public int localPort() {
return tomcat.getConnector().getLocalPort();
}
@Override
public void close() throws Exception {
tomcat.stop();
tomcat.destroy();
}
}시작 실패 전에는 로컬 포트가 유효하지 않으므로 생성자가 아니라 팩토리 메서드가 성공 뒤 래퍼를 반환하게 개선할 수 있습니다.