십수 년 된 톰캣을 최신 버전으로 올릴 때 미리 확인하고 해결하는 방법입니다.
들어가며
톰캣(Tomcat) 6.0 대에서 돌던 웹 애플리케이션을 9.0 대로 올렸습니다. 버전만 갈아 끼우면 되는 작업처럼 보이지만, 그 사이에 기본값이 바뀐 항목들이 있습니다.
문제는 이게 대부분 에러 메시지로 친절하게 안 알려준다는 점입니다. 어떤 건 기동 자체가 안 되고, 어떤 건 기동은 되는데 화면에서 한글이 전부 깨지고, 어떤 건 특정 검색어에서만 400을 뱉습니다. 미리 알고 가면 30분, 모르고 가면 하루입니다.
실제로 걸렸던 다섯 가지를 순서대로 정리합니다.
- 버전 차이가 아니라 기본값 차이가 문제
- 증상이 기동 실패 / 한글 깨짐 / 간헐적 400 으로 제각각
- 다섯 개 다 설정 파일에서 해결됨(소스 수정 없이)
함정 1. 톰캣 6 전용 Listener 를 안 지우면 기동조차 안 된다
톰캣 6의 server.xml 에는 그 시절에만 있던 Listener 가 들어 있습니다. 대표적으로 ServerLifecycleListener 와 JasperListener 입니다. 이 클래스들은 상위 버전에 아예 존재하지 않습니다.
기존 server.xml 을 그대로 복사해 오면 기동 시 ClassNotFoundException 이 나면서 톰캣이 올라오지 않습니다. 설정을 옮길 때 가장 먼저 걸리는 지점이라 오히려 발견은 쉽습니다.

여기서 배운 건, 기존 server.xml 을 복사해서 고치지 말고 새 버전의 기본 server.xml 에 필요한 것만 옮겨 담는 쪽이 안전하다는 겁니다. 반대로 하면 이런 잔재를 하나씩 만나게 됩니다.
함정 2. URIEncoding 기본값이 뒤집혀서 한글이 전멸한다
이게 이번 작업에서 가장 위험했던 항목입니다. 기동은 정상인데 화면의 한글이 전부 깨집니다.
원인은 컨테이너의 URL 디코딩 기본값이 버전 사이에 바뀌었다는 점입니다.
- 톰캣 6 : URIEncoding 기본값 ISO-8859-1
- 톰캣 9 : URIEncoding 기본값 UTF-8
이게 왜 치명적이냐면, 오래된 애플리케이션은 컨테이너가 ISO-8859-1 로 디코딩한다는 전제 위에 짜여 있는 경우가 많기 때문입니다. 파라미터를 받아서 바이트로 되돌린 다음 실제 인코딩으로 다시 해석하는 코드가 소스 곳곳에 들어 있습니다.

컨테이너가 이미 UTF-8 로 디코딩해 버리면 이 재디코딩이 성립하지 않습니다. 소스는 그대로인데 결과만 깨지는 겁니다.
해결은 커넥터에 예전 기본값을 명시적으로 박아 두는 것입니다. 한 줄이면 되고, 덕분에 소스는 한 곳도 고치지 않았습니다. "기본값에 기대던 코드"를 "명시된 값에 기대는 코드"로 바꾼 셈입니다.
물론 정석은 애플리케이션 쪽 재디코딩을 걷어내고 UTF-8 로 통일하는 겁니다. 다만 그건 소스 전수 수정이라 이관과 같이 하면 안 됩니다. 실패했을 때 원인이 이관인지 인코딩 정리인지 못 가립니다. 별도 과제로 분리하는 게 맞습니다.
- 기본값이 뒤집혔으므로 명시적으로 지정해서 기존 동작을 유지
- 인코딩 정리는 이관과 분리(동시에 하면 원인 규명이 불가능해짐)
- 확인 방법은 한글 검색어로 조회해 목록이 정상인지 보는 것
함정 3. AJP 커넥터를 그대로 두면 커넥터가 기동에 실패한다
톰캣 6 시절 server.xml 에는 8009 포트 AJP 커넥터가 기본으로 들어 있습니다. 앞단에 웹 서버를 두고 연결하는 용도인데, 9.0.31 부터 기본값이 바뀌었습니다. 주소가 로컬호스트로 제한되고, 비밀키 요구가 기본으로 켜집니다. 예전 형태 그대로 옮기면 커넥터가 올라오지 않습니다.
그래서 먼저 판단할 건 지금 이걸 실제로 쓰고 있느냐입니다.
- 포트에 연결 세션(ESTABLISHED)이 있으면 -> 앞단 웹 서버가 쓰는 중, 새 기본값에 맞춰 살려야 함
- 대기 상태(LISTEN)만 있고 연결이 없으면 -> 안 쓰는 것, 제거 대상
이번 건은 로그에 AJP 관련 기록이 수백 건 있었는데, 전부 AJP 포트로 들어온 평문 HTTP 요청이었습니다. 즉 앞단 웹 서버 트래픽이 아니라 포트 스캔이었습니다. 그래서 커넥터를 제거했고, 덤으로 고스트캣(Ghostcat, CVE-2020-1938) 공격면도 같이 사라졌습니다.
로그에 뭔가 찍힌다고 "쓰고 있구나"로 넘어가면 안 됩니다. 무슨 내용이 찍히는지까지 봐야 합니다.
함정 4. 대괄호가 들어간 검색어에서만 400 이 난다
기동도 되고 한글도 멀쩡한데, 특정 검색어에서만 400 이 떨어지는 증상이 있습니다.
상위 버전 톰캣은 HTTP 요청 문법(RFC 7230)을 엄격하게 검사합니다. 쿼리 문자열에 인코딩되지 않은 상태로 아래 여덟 글자가 들어오면 잘못된 요청으로 보고 400 을 반환합니다. 예전 버전은 그냥 통과시켰기 때문에, 이 문자를 쓰는 화면이 있었다면 이관 후에 갑자기 깨집니다.
- 역슬래시 \ , 대괄호 [ ] , 중괄호 { } , 파이프 | , 캐럿 ^ , 백틱 `
브라우저는 표준상 공백과 " # < > 정도만 인코딩해 보내고 나머지는 원문 그대로 보냅니다. 그래서 대괄호가 든 첨부 파일명이나 검색어에서 400 이 납니다.
이건 애플리케이션 로그에 아무것도 안 남습니다. 컨테이너가 앞단에서 잘라내기 때문입니다. 브라우저 개발자 도구의 네트워크 탭에서 실패한 요청 URL 을 봐야 원인이 보입니다.
해결은 커넥터에 허용 문자를 지정하는 것인데, 여기에 함정이 하나 더 있습니다.
설정값을 보면 "\[]{}|^"` 처럼 역슬래시가 앞에 붙어 있어 대괄호를 이스케이프한 것처럼 보입니다. 아닙니다. 이 속성은 이스케이프를 처리하지 않고 문자열의 각 글자를 그대로 허용 목록에 넣습니다. 즉 저 역슬래시는 역슬래시 자체를 허용한다는 뜻이고, 이걸 이스케이프로 착각해서 지우면 역슬래시가 든 요청에서 400 이 다시 납니다.
허용 범위는 넓게 잡을 게 아니라 "브라우저가 원문으로 보내는 문자"와 "톰캣이 거부하는 문자"의 교집합만 넣으면 됩니다. " < > 는 브라우저가 알아서 인코딩하므로 뺐고, 경로 쪽 완화 옵션은 두지 않아 경로 검증은 엄격한 상태로 유지했습니다.
정석은 애플리케이션이 URL 인코딩해서 보내도록 고치는 겁니다. 참고로 인코딩된 형태(%5B)는 원래부터 허용됐으므로, 이 설정으로 애플리케이션에 도달하는 입력 범위가 넓어지지는 않습니다.
함정 5. 애플리케이션 안의 servlet-api.jar 가 충돌한다
오래된 프로젝트의 WEB-INF/lib 에는 servlet-api.jar 가 들어 있는 경우가 많습니다. 예전에는 빌드 편의상 같이 넣곤 했는데, 이 라이브러리는 컨테이너가 제공하는 것입니다.
애플리케이션 쪽에 같은 것이 있으면 클래스 로딩이 꼬여 기동에 실패하거나 엉뚱한 동작을 합니다. 이관하면서 라이브러리 목록을 훑을 때 이것부터 확인하면 됩니다.

이관 후 검증은 화면을 눌러 보기 전에 로그와 포트로 먼저 하는 게 빠릅니다. 예외가 없는지, 열려 있으면 안 되는 포트가 닫혔는지, 문제가 됐던 요청이 통과하는지를 한 번에 확인할 수 있습니다.
정리
다섯 개를 관통하는 공통점이 있습니다. 버전이 바뀐 게 아니라 기본값이 바뀌었고, 애플리케이션이 그 기본값에 기대고 있었다는 것입니다. 그래서 소스는 한 줄도 안 건드렸는데 동작이 달라집니다.
그래서 이관 전에 할 일은 새 버전의 변경점을 읽는 것보다, 지금 애플리케이션이 컨테이너의 무엇에 기대고 있는지를 먼저 적어 보는 쪽이 빠릅니다. 인코딩, 커넥터, 클래스패스 세 군데만 봐도 대부분 걸립니다.
하나 덧붙이면, 웹앱 디렉터리 아래에 백업 폴더를 만들어 두는 습관도 위험합니다. 톰캣은 그 디렉터리를 웹 애플리케이션으로 자동 배포하므로, 백업해 둔 설정 파일이나 키 파일이 URL 로 그대로 열립니다. 배포 제외 설정으로 막을 수 있고, 실제로 설정 전에는 파일 내용이 응답으로 나오는 것까지 확인했습니다.
- 버전 차이가 아니라 기본값 차이를 먼저 조사할 것
- 기존 설정 파일을 복사해 고치지 말고, 새 기본 설정에 필요한 것만 옮길 것
- 기존 동작을 유지해야 하면 예전 기본값을 명시적으로 지정
- 리팩터링(인코딩 통일 등)은 이관과 같은 회차에 넣지 말 것
- 완화 옵션은 교집합만 최소로, 이스케이프처럼 보이는 문자도 실제로는 허용 대상
- 검증은 화면보다 로그·포트·문제 요청으로 먼저
- 웹앱 디렉터리 아래 백업 폴더는 그대로 웹으로 열린다
함께 보면 좋은 글:
- 톰캣(Tomcat) allowLinking 취약점 조치하면서 기능은 살리기 (심볼릭 링크 대신 전용 Context 매핑) - 같은 이관 작업에서 나온 설정 편
- 한글이 분명 있는데 grep이 안 될 때 (EUC-KR과 UTF-8 인코딩 함정) - 같은 인코딩 함정 편
- 커넥션 풀(DBCP) 경고가 갑자기 쏟아질 때 (장애가 난 걸까, 이제야 보이는 걸까) - 같은 톰캣 전환에서 나온 커넥션 풀 편
- 메모리가 부족해서 힙을 늘렸더니 아예 안 뜰 때 (32비트 JVM의 4GB 벽) - 같은 톰캣 전환에서 나온 JVM 편
- 톰캣을 새로 깔았는데 권한 점검에 또 걸릴 때 (tar가 남긴 디렉터리 모드와 umask) - 같은 톰캣 권한 조치 편
여기까지 톰캣 6에서 9로 올릴 때 만나는 함정 다섯 가지에 대해서 작성해봤습니다. 여기까지 읽어주셔서 감사합니다!
'일상' 카테고리의 다른 글
| PDF에서 텍스트를 뽑았는데 라벨만 나올 때 (폼 필드 레이어와 poppler 없이 이미지로 렌더링) (0) | 2026.08.02 |
|---|---|
| 접근성 API가 안 통하는 윈도우 앱 자동화하기 (카카오톡 UI 자동화 함정 3가지) (0) | 2026.07.29 |
| 톰캣(Tomcat) allowLinking 취약점 조치하면서 기능은 살리기 (심볼릭 링크 대신 전용 Context 매핑) (0) | 2026.07.29 |
| 키로(Kiro) CLI 설정이 조용히 무시될 때 (예약된 built-in 에이전트 이름, 자동 승인이 매번 풀리는 이유) (0) | 2026.07.29 |
| 한글이 분명 있는데 grep이 안 될 때 (EUC-KR과 UTF-8 인코딩 함정) (0) | 2026.07.26 |