개요

우리 시스템은 상당히 오래된 레거시 시스템이었다.
(물론 지금도 레거시 하긴하다)
여러 차례 손을 대야 할 시점은 있었지만, 항상 우선순위에서 밀려났고 결국 여기까지 오게 되었다.

이번 글은 Spring 6, Tomcat 10, Java 21로 업그레이드를 진행하면서 실제로 겪었던 문제들과 의사결정 과정을 정리한 기록이다.
완전히 끝난 작업은 아니지만, 일단 시스템이 정상 기동하고 외형상 서비스가 가능한 상태까지는 도달했기 때문에 그 과정에서의 경험을 남기고자 한다.


환경 정보

주요 설정 변경 사항

구분이전현재
JavaJava 11Java 21
Spring Framework5.2.22.RELEASE6.2.11
Servlet APIjavax.servlet-api 4.0.1jakarta.servlet-api
TomcatTomcat 9Tomcat 10
Apache Shiro1.9.1Jakarta EE 대응 버전
MyBatis3.4.63.5.16
CacheEhcache 2.10.6Caffeine + Redis
Apache POI3.10-FINAL미정 (추후 정비 대상)
Quartz2.3.0유지

업그레이드를 막고 있던 요인

Apache Tiles (제거됨)

Apache Tiles 3.0.5web-app, view 모듈에서 사용하고 있었다.
이 라이브러리가 이번 업그레이드의 최대 걸림돌이었다.

Spring 6Jakarta EE 기반으로 전환되었고, 이에 따라 javax.* 네임스페이스를 더 이상 사용할 수 없게 되었다.
하지만 Tiles는 이미 개발이 중단된 상태였고, Jakarta를 지원할 계획도 없었기에 Tiles를 유지하는 한 Spring 6으로는 절대 올라갈 수 없는 구조였다.


업그레이드를 시작하게 된 계기

업그레이드의 필요성은 이전부터 팀 내에서 계속 인지하고 있기는 했다.
다만 기능 개발, 유지보수, 긴급 장애 대응, 다른 프로젝트 투입 등이 반복되면서 항상 미뤄지고 있었다.

업그레이드 착수 직전까지도 “언젠가는 해야 한다”, “Tiles가 엮여 있어서 쉽지 않다”는 이야기를 반복하고 있던 어느날. 대표가 간만에 텍스트 번들 정리한다고 코드를 들여다 보더니 프레임워크가 너무 레거시한 것 아니냐고 했고, 대표가 말을 꺼냈을 때가 기회라고 생각한 이사가 그럼 지금 당장 올리자 라고 해서 갑작스럽게 바로 시작하게 되었다.

결과적으로 현재도 버그를 계속 잡고 있다.
표면적으로는 시스템이 정상 동작하는 상태지만, 세부 테스트를 하면 추가 문제는 계속 나올 가능성이 크다.
그럼에도 불구하고 일단 큰 산 하나는 넘었다는 판단 하에 이 기록을 남긴다.


왜 올리지 못하고 있었는가

이유는 단순했다.

  • Tiles가 있으면 Spring 6으로 올릴 수 없었다.

  • Spring을 못 올리니 Java도 올릴 수 없었다.

  • Tomcat 역시 SpringJava 버전을 따라갈 수밖에 없었다.

모든 병목은 Tiles에서 시작되고 있었다.


javaxjakarta 전환 배경

Oracle은 2017년 Java EEEclipse Foundation에 기증했지만, Javajavax에 대한 상표권은 넘기지 않았다.

Eclipse는 모든 패키지명을 jakarta.*로 변경할 수밖에 없었고,
Java EE 8에서 Jakarta EE 9로 넘어가며 생태계 전체가 대규모 마이그레이션을 강제당했다.
오픈소스를 기증하면서도 이름에 대한 권리는 쥐고 있었고, 이로 인해 수많은 라이브러리와 프레임워크가 불필요한 비용을 치르게 되었다.

이번 시스템이 Spring 6으로 올라가지 못했던 근본 원인 역시 여기에 있었다.


Apache Tiles 제거와 커스텀 레이아웃 전환

Apache Tiles는 JSP 기반 애플리케이션에서 레이아웃을 템플릿화하는 프레임워크였다.
header, footer, sidebar 등을 조합하는 용도로 사용하고 있었다.

하지만 Jakarta를 지원하지 않기 때문에 유지할 수 없었고, 결국 Tiles를 제거하고, 커스텀 ViewResolver와 JSP include 기반 레이아웃으로 전환했다.

결과적으로 다음 구성이 가능해졌다.

  • Spring 6

  • JDK 21

  • Tomcat 10

레이아웃 조합 기능은 충분히 대체 가능했고,
외부 라이브러리 의존성을 제거함으로써 유지보수성도 오히려 개선되었다.


캐시 구조 변경 배경 (EhcacheCaffeine + Redis)

기존에 Ehcache를 사용하던 이유는 RMI 기반 캐시 공유 때문이었다.
Gradle 멀티모듈 기반 모노레포 구조였기 때문에 foundation, drive, view 모듈 간 캐시 공유가 필요했다.

하지만 Ehcache 2.x 이후 RMI는 사실상 폐기되어서, Redis와 같은 외부 캐시로 위임하는 구조가 되었다.

이 시점에서 Ehcache를 유지할 이유가 사라졌다고 판단했기 때문에 팀 내부적으로 협의 하에 구조를 변경하기로 했다.

  • 로컬 캐시는 Caffeine
  • 컨텍스트 간 동기화는 Redis

Redis 단독 사용도 고려했지만, 트래픽 증가 시 속도 이슈 가능성과 Docker 기반 환경에 대한 러닝 커브를 고려해 우선 로컬 테스트가 쉬운 구조를 선택했다.


업그레이드 과정

Spring 6 / Tomcat 10 / Java 21 강제 상향

결정이 내려진 뒤에는 일단 냅다 버전을 올렸다.
build.gradle에서 관련 버전을 전부 상향했고 환경을 세팅했고, 결과는 예상대로 컴파일 에러 지옥이었다.


javax 제거와 의존성 정리

Jakarta 환경에서는 javax가 하나라도 남아 있으면 충돌이 발생해서 라이브러리 의존성 트리 전체에서 javax를 제거해야 했다.

기존 Groovy 기반 Gradle 스크립트를 Kotlin DSL로 전환하면서 중앙 build.gradle 구조도 서브 프로젝트 단위로 분리되었다.
이 과정에서 의존성 패키지, 버전 관리가 더 까다로워졌다.


Eclipse 컴파일은 되는데 IntelliJ 컴파일은 왜 안 되지

우리 팀은 개발 환경이 Eclipse와 IntelliJ 두 가지 타입으로 나뉜다.
두 가지 환경에서 배포 방식이 차이가 있다.
Gradle 의존성을 수집하면 각 패키지마다, 필요하면 더 세분화된 폴더에 메타데이터로 뿔뿔이 저장된다.
IntelliJ는 이 의존성들을 한번에 모아 빌드도 해주고 타겟 폴더에 배포도 해주는 Artifact라는 기능이 있지만 Eclipse는 아니다.. Artifect가 내부 Tomcat 배포가 수행하던 의존성 취합과 Ant가 수행하던 배포 대상 파일 타겟 위치로 복사의 역할을 대신하는 형태이다.

  • Eclipse: Gradle 의존성 수집 후 내장 Tomcat Deploy를 통해 의존성 한 폴더로 취합. Ant 배포
  • IntelliJ: Gradle 의존성 수집 후 Artifact 기반 배포

분명 의존성을 정리했고 Eclipse에서는 빌드 및 배포가 에러없이 잘 수행되고 서버도 잘 뜨는데
IntelliJ에서는 빌드 시에 javax 네임스페이스가 있다고 에러가 났다. 분명히 이 패키지는 jakarta 아티팩트로 받아오도록 설정했는데도..
상황은 이러했다.

  • Eclipse에서는 정상 빌드, 배포 됨
  • IntelliJ에서는 javax 라이브러리가 계속 포함됨
  • 해당 라이브러리는 javax로 받지 않으려고 jakarta artifact로 의존성을 받아오도록 설정을 해놨음
  • IntelliJ에서 Artifect가 아닌 gradle 빌드 배포를 하니 정상 수행됨~~(?? 왜??)~~

그래서 처음에는 IntelliJ IDE 문제인 줄 알았다.
근데 살펴보다 보니 해당 패키지가 javax인 plain classifier 라이브러리와 jakarta classifier 라이브러리가 함께 들어오고 있었고 IntelliJ 뿐만아니라 Eclipse도 마찬가지였다.

원인

명확한 원인은 다음과 같았다.

빌드 시에 plain classifier 라이브러리와 jakarta classifier 라이브러리가 의존성이 함께 들어온 경우

  • Gradle 빌드에서는 jakarta classifier 라이브러리를 우선 사용해서 빌드 한다.
  • Eclipse는 jakarta classifier 라이브러리를 우선 사용해서 빌드 한다.
  • IntelliJ는 javax classifier 라이브러리를 우선 사용해서 빌드 한다.

확실치는 않지만 IntelliJ가 Gradle 사용 방식이 내부적으로 조금 다르다고 듣긴 했는데 그 차이의 연장선상에 있는 현상인가 싶기도 하다.
(예전에도 트러블 슈팅했던 문제중에, Nexus SSL 문제로 Eclipse는 ignore SSL 속성을 줘야만 하고, IntelliJ에서는 속성 안줘도 문제 없던 현상이 있었다. 당시에 찾아봤을 때는 Gradle 초기화 방식이 IntelliJ가 조금 다르다는 답변을 봤던 기억이 있음)

결국 모든 종속성에서 plain 아티팩트를 개별적으로 exclude 처리해서 plain 아티팩트 라이브러리가 종속성으로 아얘 안들어오는 방향성으로 작업을 했다.
classifier를 지정했음에도 plain 아티팩트가 종속성의 종속성으로 딸려들어오는 경우도 있다고 했기 때문이었고 결과적으로는 이런 방향의 처리가 맞다고 생각한다.
빌드 환경 별 동작 방식의 차이와 무관하게 우리가 쓸 라이브러리만 종속성으로 들어오는 것이 옳은 방향 아니겠는가.
이 처리도 Gradle의 exclude(group, module)이 classifier를 구분하지 않는 문제 때문에 상당히 애를 먹었지만..

결과적으로 IntelliJ 아티팩트 배포에서도 문제가 사라졌다.


Redis 추가 후 Spring 7 의존성 유입 문제

Redis 의존성을 추가하자 Spring 7 관련 의존성이 함께 딸려 들어왔다.
Spring 7에서 제거된 메서드를 사용 중이어서 컴파일 에러가 발생했는데 이미 직전에 의존성 문제때문에 호되게 고생한 직후라서 적당히 의존성들을 exclude 처리하고 대신 의존 패키지의 버전을 지정관리 할 수 있게 분리해서 Spring 6 의존성만 사용하도록 조정했다.


캐시 어노테이션 변경 이슈

Spring 6부터 @Cacheable의 파라미터 표현 방식이 변경되었다.
기존의 Cache key에서 파라미터 이름 기반으로 사용하던 #container.id 방식이 동작하지 않았고, p0, a0와 같은 인덱스 기반 접근 방식으로 수정해야 했다.

  • Method arguments can be accessed by index. For instance the second argument can be accessed via #root.args[1]#p1 or #a1. Arguments can also be accessed by name if that information is available.

Spring Expression Language (SpEL) key 표현이 파라미터 인덱스 먼저, 이름은 “정보가 있을 때만 사용”이라고 Spring API 문서의 Annotation Interface Cacheable에 명시되어있다.

그리고 Spring Github Wiki의 Upgrading to Spring Framework에서도 아래와 같이 명시되어있다.

LocalVariableTableParameterNameDiscoverer6.1 버전에서 해당 기능이 제거되었습니다. 따라서 Spring Framework 및 Spring 포트폴리오 프레임워크 내의 코드는 더 이상 바이트코드를 파싱하여 매개변수 이름을 추론하려고 시도하지 않습니다. 의존성 주입, 속성 바인딩, SpEL 표현식 또는 매개변수 이름에 의존하는 기타 사용 사례에서 문제가 발생하는 경우, 컴파일러 플래그에 의존하는 대신 Java 8 이상에서 일반적으로 사용되는 -parameters매개변수 이름 유지 플래그를 사용 하여 Java 소스 코드를 컴파일해야 합니다 .

파라미터를 넣어 컴파일 하면 이전처럼 쓸 수는 있기는한데, 굳이 기능이 제거된 것을 끌어안고 있을 필요는 없는 것 같긴하다. 컴파일 옵션 추가는 부가적으로 잘못 설정할 가능성을 높이는 것 같으므로 기왕이면 문제가 없는 방향으로 가고 싶기 때문에 수정 작업을 수행했다.


해결하지 못한 아쉬운 점들

JPA를 사용하지 못하는 구조

동적 속성 시스템과 복잡한 검색 쿼리 구조 때문에 컬럼과 타입이 사실상 무한히 확장되는 구조라서 JPA로는 감당하기 어려운 형태이다. 요새 다들 JPA를 쓰는 편이니까 사용해보고 싶었는데..
사실 우리는 PTC 자체 개발한 PTC Persistence Management 라는 ORM(Object-Relational-Mapping)의 컨셉을 계승했기 때문에 자체적으로 Mybatis를 사용해서 PTC Persistence Management 를 유사하게 구현해서 사용 중이다.

참고로 PTC Persistence Management은 JPA의 Persistence Management와는 전혀 다른 용어, 범위, 개념이다.
하여간 PTC 개발자들은 이것저것 용어를 겹치게 만들어놔서 자료 서치도 힘들고 다른 사람들을 용어때문에 더 이해시키기도 힘들다. 떼잉..


Git 전환 실패

Git을 도입하자는 이야기는 여러 번 나왔지만 매번 항상 “소 잡는 칼로 닭 잡는 격”이라는 반응으로 돌아왔다.
이번에도 슬쩍 주장해 보았지만.. 너 어려운거 좋아하는구나? 라는 반응과 함께 그냥 쓰루당해 버렸다ㅠ

SVN이 정말 관리 비용이 낮은지,
Git이 과도한 도구인지에 대해서는 여전히 의문이 남는다.

AI 기반 자동화, PR, 코드 리뷰 문화는 이야기하면서 정작 SVN 훅 한계 때문에 기본적인 자동화도 못 하고 있는 상황이었다.
내가 이번 뿐만이 아니라 N번째 옮기자 주장 중 이지만 명확한 이유를 정리해서 설득하지 않는 한 절대 들어주지 않을 것 같다..ㅠ


DHTMLX 의존성과 매몰 비용

DHTMLX Suite, Diagram, Gantt 컴포넌트를 모두 사용하고 있다.
이미 비용을 지불했고, 사용 패턴이 익숙해지면서 점점 종속성이 커졌다.

프론트엔드 모더나이즈를 이야기한 지는 오래됐지만, 인력과 시간, 프로젝트 구조상 실행으로 옮기지 못하고있다..
당장도 올해 1월, 4월에 신규 프로젝트 잡혀있고, 일정이 정해지지는 않았지만 올해안에 해야할 프로젝트가 2개 더 있다.
이정도면 릴리즈 하는것 만으로도 기적이라고 해야할 것 같다.


결론

시스템은 우여곡절 끝에 일단 올렸다.
완벽하지는 않지만, 더 이상 발목 잡히지는 않는 상태가 된 것 같다.

이제부터는 천천히 갚아야 할 기술 부채의 문제다.