일기 slecs

서버 기동 시 JSP·URL 매핑 사전 초기화 기능 추가

목차

서버가 뜨고 나서 첫 요청이 들어올 때 JSP 컴파일이 아직 안 돼 있으면 응답 시간이 튄다. 극단적인 경우 타임아웃이 나기도 하고, 모니터링 대시보드에 첫 번째 요청만 빨간불이 들어오는 걸 보면서 "사용자가 맞은 거잖아"라는 생각이 든다. 이번 작업은 거기서 출발했다. 서버 기동 시점에 JSP와 URL 매핑을 미리 초기화해서, 첫 실사용자 요청이 웜업 비용을 떠안지 않도록 하는 것.

slecs 레포 기준 변경 파일은 4개, 레이어는 빌드 설정·컨트롤러·유틸·SQL 매퍼 쪽이다. 파일 수만 보면 가볍지만, 기동 시점에 끼어드는 코드라서 실수하면 서버 자체가 안 뜨는 시나리오가 생긴다. 그래서 평소보다 더 조심스럽게 들어갔다.

왜 사전 초기화가 필요한가

JSP는 서블릿으로 컴파일되는 시점이 기본적으로 첫 요청 때다. 톰캣 기준으로 precompilation 옵션을 빌드 단계에서 걸거나, 기동 후 내부적으로 더미 요청을 날려서 강제 초기화하는 방식을 주로 쓴다. URL 핸들러 매핑도 Spring MVC가 컨텍스트를 올리면서 자동으로 등록하지만, 특정 조건(예: 지연 빈 초기화, 동적 등록)에서는 첫 호출 때까지 매핑 테이블이 완성되지 않을 수 있다.

문제가 표면에 드러나는 전형적인 패턴은 이렇다.

상황 증상 원인
재배포 직후 첫 요청 응답 지연 급등, 간헐적 타임아웃 JSP 컴파일 미완료
트래픽 급증 시 스케일아웃 직후 신규 인스턴스만 에러율 상승 웜업 없이 바로 로드밸런서 투입
유지보수 후 재기동 특정 URL만 404, 이후 정상 URL 핸들러 등록 순서 문제

이번 구현은 이 중 첫 번째와 세 번째를 주요 타깃으로 잡았다.

구현 방식과 패턴 선택

기존 코드베이스 패턴을 최대한 따르는 게 원칙이었다. 새로 도입한 초기화 진입점은 ApplicationListener<ContextRefreshedEvent> 계열의 컨트롤러/유틸 쪽에 붙였고, 기동 시 콜백이 실행되면 필요한 URL 목록을 순회하면서 내부 요청을 처리하는 흐름으로 짰다.

@Component
public class JspWarmupInitializer implements ApplicationListener<ContextRefreshedEvent> {

    private final SomeWarmupService warmupService;
    private volatile boolean initialized = false;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        if (initialized) return; // 컨텍스트 중복 이벤트 방어
        initialized = true;
        warmupService.preloadMappings();
    }
}

ContextRefreshedEvent는 컨텍스트가 갱신될 때마다 발생하는데, 부모-자식 컨텍스트 구조(Spring MVC는 루트 + 서블릿 컨텍스트가 분리됨)에서 두 번 불리는 경우가 있다. initialized 플래그로 막아두지 않으면 웜업 로직이 두 번 실행되고, 운 나쁘면 SQL도 두 번 탄다. 기존 코드에서 이미 이 처리를 하고 있었고, 그 패턴을 그대로 가져왔다.

SQL 쪽은 새 쿼리 ID를 기존 네이밍 스타일에 맞춰 추가했다. 매퍼 파일에서 ID 충돌이 나면 기동 자체가 실패하기 때문에, 추가 전에 기존 ID 목록을 한 번 훑었다. 컨트롤러 레벨의 권한 체크는 기존 패턴을 그대로 따라서 새 엔드포인트에도 동일하게 적용했다. 여기서 손을 빼면 나중에 "왜 이 URL만 권한 없이 뚫려요?"라는 질문이 들어온다.

신규 기능이 기존 흐름에 영향을 주지 않도록 엔드포인트와 쿼리 ID를 완전히 분리한 것도 중요한 선택이었다. 공유 쿼리를 건드리거나 기존 엔드포인트의 파라미터를 변경했다면, 회귀 테스트 범위가 훨씬 넓어졌을 거다. 새로 만들면 검증 범위가 명확하게 잘린다.

검증과 사이드 이펙트 체크

검증은 두 단계로 진행했다.

  • 변경 전, 기존 URL과 화면이 모두 정상 동작하는 것을 브라우저와 API 툴로 확인.
  • 새 기능을 붙인 뒤, 서버를 재기동하고 기동 로그를 보면서 초기화 순서와 실행 여부를 확인. 그다음 기존 기능을 다시 한번 돌려서 회귀가 없는지 체크.

DB 쪽은 쿼리 실행 후 실제 반환값을 조회해서 기대값과 맞는지 확인했다. 웜업 대상이 되는 URL 목록이 비어 있을 때(빈 결과 케이스)도 별도로 돌려봤고, 권한 없는 접근 경로로 초기화 로직이 우회되는지도 체크했다.

기동 시간이 눈에 띄게 늘어나는지도 확인했다. 웜업 로직이 무거우면 서버 올라오는 속도가 느려지고, CI/CD 파이프라인이나 헬스체크 타임아웃에 걸릴 수 있다. 이번 케이스는 초기화 대상이 많지 않아서 기동 시간 증가가 유의미하지 않았지만, 대상이 늘어날수록 비동기 처리나 배치 사이즈 조절을 고민해야 한다. 서버가 뜨는 데 30초 넘게 걸리기 시작하면 운영 팀이 먼저 문제를 제기한다.

전체 작업에서 가장 신경 쓴 부분은 결국 사이드 이펙트였다. 기동 시점에 끼어드는 코드는 범위가 작아도 영향 반경이 서버 전체다. 거기서 예외 하나 터지면 인스턴스가 안 뜨고, 배포가 롤백되고, 새벽에 알람이 온다. 그게 싫어서 기존 패턴을 벗어나지 않고, 새 코드는 철저히 격리해서 붙였다.

댓글 0

첫 댓글 달아줘.