런타임을 최신 버전으로 올리면 흔히 따라오는 질문이 있다.
그래서 얼마나 빨라졌나요?
PhotoGram을 Java 17에서 Java 25로 옮긴 뒤 나도 같은 질문을 확인하고 싶었다. 다만 처음부터 결론을 정해놓고 숫자를 찾는 방식은 피하고 싶었다. 마이그레이션의 목적은 최신 Java를 사용한다는 사실 자체가 아니라, 기존 기능과 데이터 계약을 유지하면서 런타임을 바꿔도 되는지 확인하는 것이었기 때문이다.
그래서 성능 검증도 같은 원칙으로 진행했다. 실제 마이그레이션 경로는 Java 17에서 Java 25로 직접 이동하는 것이었고, 중간에 Java 21 상태를 일부러 만들지 않았다. 벤치마크를 예쁘게 만들기 위해 존재하지 않았던 제품 상태를 만들어내면 측정은 늘어나지만 실제 의사결정과의 연결은 약해진다.
실제 마이그레이션
Java 17 -> Java 25
하지 않은 것
Java 17 -> Java 21 -> Java 25
먼저 성능보다 정확성을 확인했다
비교 전에 가장 중요했던 것은 두 런타임이 같은 애플리케이션 계약을 지키는지였다.
검증에서는 다음 항목을 먼저 통과시켰다.
- 기존 349개 테스트 기준 유지
- Flyway 스키마 검증 통과
- public / authenticated HTTP workload 각각 오류 0
- 중복 follow 시나리오에서 관계 정합성 유지
- Java 25 환경에서 repository validation 통과
성능 수치가 좋아도 기능이 달라지면 비교 자체가 의미가 없다. 그래서 correctness gate를 먼저 통과시키고, 그 다음에 startup, RSS, HTTP latency와 throughput, regression suite 시간을 비교했다.
측정 결과는 꽤 좋아 보였다
대표 지표만 보면 Java 25 쪽이 여러 구간에서 긍정적인 방향을 보였다.
| 지표 | Java 17 | Java 25 | 변화 |
|---|---|---|---|
| Liveness/readiness startup | 7,502 ms | 6,315 ms | -15.8% |
| Peak process RSS | 974,010 KiB | 622,052 KiB | -36.1% |
| Public HTTP throughput | 48.87 req/s | 56.18 req/s | +15.0% |
| Public request p50 | 7.269 ms | 6.300 ms | -13.3% |
| Public request p95 | 13.165 ms | 9.252 ms | -29.7% |
| Authenticated throughput | 42.89 req/s | 44.75 req/s | +4.3% |
| Authenticated request p95 | 14.523 ms | 18.190 ms | +25.2% |
| Full regression suite | 651,374 ms | 537,059 ms | -17.5% |
startup과 RSS는 크게 줄었고 public workload도 개선됐다. 이 표만 보면 “Java 25가 더 빠르다”는 문장을 쓰기 쉽다.
하지만 나는 최종 characterization을 NO CLEAR DIFFERENCE로 남겼다.
숫자가 좋아 보이는데 왜 결론은 보수적이었나
첫 번째 이유는 환경이다. 테스트는 동일한 Docker Desktop host에서 실행했지만, 완전히 격리된 성능 실험 장비는 아니었다. host load와 container scheduling의 영향을 완전히 제거했다고 말할 수 없다.
두 번째는 sample 수의 차이다. startup과 HTTP workload는 반복 측정을 했지만, 전체 regression suite는 한 번 실행하는 데 약 9~11분이 걸려 버전별 단일 샘플만 남았다. 여기서 나온 -17.5%를 일반화된 성능 개선으로 취급하면 과도한 해석이다.
세 번째는 지표가 한 방향으로만 움직이지 않았다는 점이다. authenticated workload의 중앙값은 조금 좋아졌지만 p95는 오히려 나빠졌다. 어떤 경로에서는 개선이 보이고 다른 경로에서는 분산이 커진다면, 한 문장으로 전체 애플리케이션의 성능을 표현하기 어렵다.
즉 측정된 사실과 해석을 분리해야 했다.
측정된 사실
- startup median 감소
- RSS 감소
- public workload 개선
- authenticated p95 증가
- correctness 유지
해석
- 방향성은 긍정적
- 하지만 제품 전체가 명확히 빨라졌다고 단정할 근거는 부족
마이그레이션에서 성능은 promotion gate가 아니었다
이 실험에서 가장 중요한 결과는 사실 “Java 25가 몇 % 더 빠르다”가 아니었다.
Java 25에서도 기존 테스트와 스키마, HTTP 동작, 동시성 관련 correctness가 유지됐고, 측정된 성능이 적어도 명백한 퇴행을 보여주지 않았다는 점이 더 중요했다.
런타임 마이그레이션의 성공 조건을 이렇게 잡을 수 있었다.
Correctness preserved
+ schema preserved
+ operational behavior preserved
+ no convincing performance regression
= migration candidate remains acceptable
성능 개선은 보너스일 수 있지만, 그 보너스를 증명하기 위해 마이그레이션 의사결정을 왜곡할 필요는 없다.
Java 21을 일부러 만들지 않은 이유
처음에는 17, 21, 25를 세 점으로 비교하면 더 보기 좋은 그래프가 나올 수 있다는 생각도 있었다. 하지만 PhotoGram의 실제 변화는 17에서 25로의 직접 이동이었다.
벤치마크 때문에 별도의 Java 21 migration branch나 source repair를 만들면 연구가 제품 이력을 따라가는 것이 아니라, 제품이 연구 모양을 따라가게 된다.
그래서 원칙을 하나 정했다.
BENCHMARK REQUIREMENT
!= IMPLEMENTATION REQUIREMENT
실제 의사결정에 존재하지 않았던 상태는 성능 비교만을 위해 새로 만들지 않는다.
이 실험에서 얻은 교훈
런타임 업그레이드에서 가장 경계해야 하는 것은 “새 버전이니까 당연히 좋아졌을 것”이라는 기대다. 반대로 작은 노이즈 때문에 모든 개선 가능성을 무시할 필요도 없다.
내가 유용하다고 느낀 방식은 단순했다.
- correctness를 먼저 고정한다.
- 실제 migration state만 비교한다.
- 반복 가능한 지표와 그렇지 않은 지표를 구분한다.
- measured fact와 interpretation을 문서에서 분리한다.
- 증거보다 강한 결론을 쓰지 않는다.
이번 결과에서 Java 25는 충분히 좋은 candidate였다. 여러 지표도 긍정적인 방향을 보였다. 하지만 그것과 “PhotoGram이 Java 25에서 명확히 더 빠르다”는 주장은 같은 문장이 아니다.
그래서 결론은 일부러 심심하게 남겼다.
NO CLEAR DIFFERENCE.
성능 측정에서 심심한 결론이 오히려 가장 정확한 결론일 때가 있다.