자동화 slecs

JSP 프리컴파일 전체 GET 엔드포인트 자동 처리 구현

목차

JSP 프리컴파일이 왜 필요한지부터 얘기하면, 기본적으로 JSP는 최초 요청 시점에 서블릿으로 변환되고 컴파일된다. 배포 직후 첫 번째 방문자가 그 비용을 고스란히 부담하는 구조다. 페이지가 많아질수록 이 "첫 번째 불행한 유저"의 응답 시간은 눈에 띄게 길어진다. 프리컴파일은 이 컴파일 비용을 배포 시점으로 당겨놓는 거다. 유저한테 전가되던 지연을 운영팀이 흡수하는 트레이드오프인데, 서비스 입장에서는 거의 항상 맞는 선택이다.

문제는 프리컴파일 대상을 어떻게 정의하느냐다. 흔히 하는 방식이 URL 목록을 하드코딩해서 배포 스크립트나 빌드 태스크에 넣어두는 거다. 이 방식은 짧은 기간엔 잘 돌아가다가, 팀 규모가 커지거나 기능이 늘어나면 반드시 무너진다. 누군가 새 JSP를 추가하면서 목록 업데이트를 빠뜨리면 그 페이지만 프리컴파일이 빠진다. 배포 로그에는 성공이라고 찍혀있는데 첫 접속자만 느린 상황이 생기는 거다.

이번 작업이 해결하려 한 게 바로 그 수동 관리 문제였다. GET 엔드포인트를 자동으로 수집해서 처리하도록 구현했고, 앞으로 신규 페이지가 추가되더라도 프리컴파일 목록을 따로 손댈 필요가 없어진다.

구현 구조와 선택 이유

레이어는 Java 컨트롤러와 유틸 수준에서만 손댔다. 변경 파일은 한 개. 기존 패턴을 최대한 따르면서 신규 기능을 붙이는 방식을 택했다.

흐름을 정리하면 이렇다:

컨트롤러 (신규 진입점 등록)
  └─ 유틸/핸들러 (GET 엔드포인트 목록 자동 수집 → 순회)
        └─ 내부 HTTP 요청 (각 URL 워밍업 호출)
              └─ JSP 컴파일 트리거 (WAS 레벨)

기존 코드에 영향을 주지 않으려고 신규 엔드포인트 경로를 완전히 분리해서 추가했다. 기존 프리컴파일 경로가 이미 있었다면 그건 건드리지 않고, 자동 처리 로직을 별도 경로로 옆에 붙이는 방식. 이 선택은 롤백 리스크를 줄이기 위해서였다. 기존 동작이 그대로 살아있으니 새 기능에 문제가 생겨도 영향 범위가 작다.

Spring MVC 기준으로 보면 RequestMappingHandlerMapping에서 등록된 매핑 정보를 순회해서 GET 메서드인 것만 추려내는 게 일반적인 패턴이다. 예시로 보면 이런 형태다:

@Autowired
private RequestMappingHandlerMapping handlerMapping;

public List<String> collectGetEndpoints() {
    return handlerMapping.getHandlerMethods().entrySet().stream()
        .filter(e -> e.getKey().getMethodsCondition()
                      .getMethods().contains(RequestMethod.GET))
        .map(e -> e.getKey().getPatternValues())
        .flatMap(Collection::stream)
        .filter(url -> !url.contains("{")) // 경로 변수 있는 건 일단 제외
        .collect(Collectors.toList());
}

경로 변수가 포함된 URL(/item/{id} 같은)은 샘플 값 없이 호출하면 404가 나거나 의미 없는 컴파일이 되기 때문에 필터링 기준을 명확히 잡아야 한다. 이번엔 단순 GET 엔드포인트만 대상이었기 때문에 그 경계를 비교적 깔끔하게 그을 수 있었다.

JSP 데이터 바인딩에서 자주 빠지는 구멍

자동 처리 구현 외에 이번 작업에서 신경 쓴 부분이 프론트(JSP)에서 서버(컨트롤러)까지 데이터 흐름이 끊기지 않게 연결하는 것이었다. 파라미터 이름 불일치로 데이터가 조용히 날아가는 케이스가 생각보다 자주 나온다.

대표적인 패턴 몇 가지:

  • 컨트롤러가 @RequestParam("userId")로 받는데 폼이 name="user_id"로 보내는 경우
  • Model에 넣은 attribute 키와 EL 표현식에서 참조하는 이름이 다른 경우 (${userInfo} vs model.addAttribute("user_info", ...))
  • DB에서 VARCHAR로 온 값을 숫자 비교 연산에 쓸 때 타입 변환이 조용히 실패하는 경우

이런 버그는 컴파일 타임에 안 잡히고, 대부분 특정 케이스에서만 재현되기 때문에 브라우저에서 직접 눈으로 확인하는 게 결국 제일 확실하다. SQL에서 가져온 데이터가 화면에 제대로 바인딩되는지, 결과가 기대값과 맞는지 직접 조회해서 대조했다.

엣지 케이스도 간단히 체크했다:

  • 쿼리 결과가 빈 리스트일 때 뷰 렌더링이 깨지지 않는지
  • 권한 없는 접근 시 흐름이 적절히 막히는지
  • 프리컴파일 순회 도중 특정 URL이 4xx/5xx를 내뱉을 때 전체 순회가 죽지 않는지

마지막 항목이 자동 수집 방식 특유의 리스크다. 수동 목록이었다면 관리자가 "살아있는" URL만 골라서 넣었겠지만, 자동 수집이면 인증이 필요한 엔드포인트나 내부 관리 페이지까지 다 걸려들 수 있다. 개별 URL 호출 실패를 무시하고 계속 순회하는 방향이 대부분의 경우 맞다. 한 JSP 컴파일 실패가 전체 배포를 블록하면 득보다 실이 크다.

돌아보면

변경 파일이 한 개짜리 커밋이다. 그렇다고 가볍게 본 건 아니었다. 프리컴파일 로직은 애플리케이션 기동 흐름에 물려있어서 실수하면 배포 속도 자체가 느려지거나 기동이 블록될 수 있다. 기존 코드를 최대한 안 건드리는 방향을 택한 것도 그래서였다.

검증은 API 툴로 신규 엔드포인트를 직접 호출해보고, DB 조회로 실행 결과가 기대값과 맞는지 대조하는 식으로 했다. 회귀는 없었고, 기존 프리컴파일 경로도 이상 없이 살아있었다.

앞으로 비슷한 "배포 시 자동 워밍업" 작업을 할 때 챙길 포인트를 정리해두면:

  • 자동 수집 범위에 내부 관리 엔드포인트가 포함되지 않도록 필터링 조건을 명시적으로 둘 것 (경로 prefix로 제외하거나 별도 애노테이션으로 마킹)
  • 프리컴파일 실패 로그를 반드시 남길 것 - 조용히 스킵되면 문제가 숨는다
  • 순회 타임아웃을 URL당 설정해서 단일 엔드포인트 응답 지연이 전체를 붙잡지 않게 할 것
  • 자동 수집 결과를 한 번 로그로 찍어두는 것도 좋다 - 어떤 URL이 대상에 포함됐는지 배포 로그로 추적할 수 있어야 나중에 디버깅이 편하다

파일 한 개짜리 커밋이 늘 간단한 건 아니다. 건드리는 코드 양보다 그 코드가 어디에 끼어있는지가 복잡도를 결정한다.

댓글 0

첫 댓글 달아줘.