jvm

6 개의 포스트

datadog4분 읽기큐레이션 요약

접두사 트라이를 JVM 상수로 인코딩해 APM Java 시작 시간을 개선한 방법

애플리케이션 시작 시간은 사용자 경험, 개발 생산성, 클라우드 비용에 모두 영향을 주며, Java APM의 클래스 매칭 비용도 주요 요인이다. Datadog은 JVM 시작 단계의 제약을 고려해 클래스명 접두사 매칭을 최적화했고, 최근 4년간 관련 오버헤드를 30% 줄였다. 특히 여러 접두사 매칭 트라이(trie)를 하나의 JVM 문자열 상수로 미리 인코딩해, 시작 시 별도의 파일 읽기나 자료구조 구축 없이 빠르게 조회하도록 만든 것이 핵심이다. ## Java APM과 클래스 계측 - Java Instrumentation API는 클래스가 로드되기 전에 변환할 수 있어, 네이티브 코드가 필요한 JVMTI보다 Java 기반 APM 구현에 적합하다. - APM은 메서드 시작·종료 시점 기록, 호출 간 컨텍스트 전파 등을 위해 클래스에 코드를 삽입한다. - 개별 메서드 계측 비용은 작지만, 모든 클래스와 메서드를 계측하면 전체 성능 비용이 급격히 증가한다. - 따라서 관측 가치가 높은 클래스만 선별해야 하며, 이 선별 과정이 **클래스 매칭**이다. ## 클래스명 접두사로 탐색 범위 줄이기 - 일반적인 Java 애플리케이션은 수만 개의 클래스를 정의하고, 대규모 엔터프라이즈 애플리케이션은 10만 개 이상을 로드할 수 있다. - 클래스 파일을 분석하거나 상속 구조를 확인하는 방식은 비용이 크다. - 클래스 파일 파싱이 필요하다. - 관련 타입의 추가 클래스 파일을 읽어야 할 수 있다. - 이에 따라 Datadog APM은 먼저 클래스명과 패키지명 접두사를 curated ignore list와 비교해 계측 대상이 아닌 클래스를 빠르게 제외한다. - 이 초기 접두사 매칭은 모든 클래스에 적용되므로, 작은 알고리즘 개선도 전체 시작 시간에 측정 가능한 영향을 준다. ## JVM 시작 단계의 특별한 제약 - 에이전트는 애플리케이션의 `main`보다 먼저 실행되는 `premain` 단계에서 클래스 변환기를 등록한다. - 이 시점에는 다음과 같은 제약이 있다. - 로드된 클래스가 거의 없다. - JIT 컴파일러가 아직 준비되지 않았다. - Java 8에서는 `premain` 이후에야 JIT가 시작되므로 코드가 인터프리트되고 최적화되지 않는다. - 일부 JDK API 호출이 애플리케이션 동작에 영향을 줄 수 있다. - 예를 들어 `java.util.logging`을 사용하면 `LogManager`가 초기화된다. - 이후 애플리케이션의 `main`에서 사용자 지정 `java.util.logging.manager`를 설정하려 해도 이미 초기화가 끝났기 때문에 동작이 깨질 수 있다. - 따라서 `premain`에서는 외부 리소스, 무거운 라이브러리, 부작용이 있는 JDK API 사용을 피해야 한다. ## 기존 코드 기반 매칭의 한계 - 2020년 당시 Datadog APM Java는 복잡한 중첩 구조의 코드를 직접 생성해 접두사를 매칭했다. - 이 방식은 유연했지만 다음 문제가 있었다. - 매칭 규칙을 유지보수하기 어려웠다. - Java 8의 초기화 단계에서는 JIT 최적화를 기대할 수 없었다. - 시작 시 빠르게 실행되도록 여러 추가 최적화가 필요했다. - 분석 결과, 기존 매칭 규칙에는 트라이 자료구조가 가장 적합한 대안으로 판단됐다. - 하지만 일반적인 트라이는 리소스 파일을 찾고, 읽고, 파싱한 뒤 노드를 구성해야 하므로 `premain` 환경에 적합하지 않았다. ## 트라이를 JVM 문자열 상수로 인코딩 - Datadog은 `ClassNameTrie`라는 접두사 트라이를 만들고, 이를 하나의 JVM 문자열 상수로 표현했다. - 문자열 상수는 클래스 로딩 과정에서 JVM이 직접 로드하므로 다음 장점이 있다. - `ldc` 단일 바이트코드 명령으로 접근할 수 있다. - 파일 검색이나 I/O가 필요 없다. - 클래스 내부에 포함되므로 애플리케이션 패키징·재패키징 후에도 유지된다. - 데이터가 작고 연속적으로 저장되어 캐시 지역성이 좋다. - 시작 단계에 트라이 객체를 별도로 생성할 필요가 없다. ## 문자열 내부의 트라이 구조 - Java의 `char`는 2바이트이며 65,536개의 값을 표현할 수 있다. - 이 값을 일반적인 문자뿐 아니라 트라이의 제어 정보와 매칭 결과 저장에도 사용한다. - 각 트라이 노드는 다음 구조를 가진다. - 첫 번째 문자: 해당 노드의 브랜치 개수 - 이어지는 문자들: 각 브랜치의 문자 - 브랜치 문자 뒤의 값 문자들: 각 브랜치의 결과 정보 - 브랜치 문자는 정렬되어 저장되므로 이진 검색으로 빠르게 다음 경로를 찾을 수 있다. - 값은 상위 비트에 따라 세 가지 의미를 가진다. - **Leaf**: 확정된 결과이며 검색을 즉시 종료한다. - **Bud**: 잠정적인 결과를 제공하지만 더 긴 접두사를 확인하기 위해 검색을 계속한다. - **Inline segment 길이**: 해당 브랜치에 이어지는 문자열 구간의 길이를 나타낸다. - Bud와 leaf에는 glob 비트를 설정할 수 있다. - 기본적으로 결과는 입력 키가 해당 노드에서 정확히 끝날 때만 적용된다. - glob 비트가 있으면 뒤에 문자가 더 남아 있어도 결과를 적용할 수 있다. - 이 비트 구성 때문에 `ClassNameTrie`에 저장할 수 있는 최대 값은 8,191이다. ## 실용적인 결론 JVM의 가장 이른 시작 단계에서 실행되는 코드는 JIT 최적화나 일반적인 런타임 자료구조 생성에 의존하기 어렵다. 이런 환경에서는 작은 매칭 데이터라도 문자열 상수처럼 JVM이 직접 로드할 수 있는 형태로 미리 인코딩하면 초기화 비용과 I/O를 줄이고, 대규모 클래스 탐색의 누적 비용을 낮출 수 있다.

원문 읽기(새 탭에서 열림)
datadog4분 읽기큐레이션 요약

JDK 25에서 JFR을 활용한 편향 없는 Java CPU 프로파일링

Java Flight Recorder(JFR)는 저부하·상시 운영에 적합한 강력한 진단 도구지만, CPU 사용량을 정확히 반영해야 하는 프로파일링에서는 한계가 있다. 특히 `ExecutionSample`은 실제 CPU 시간에 비례해 샘플링하지 않아 CPU 집약적이거나 리액티브한 애플리케이션의 핫스팟을 과소·과대평가할 수 있다. 따라서 현대 프로파일러는 JFR의 안정성과 `AsyncGetCallTrace`·JVMTI 기반 샘플링의 정확성을 결합해 사용하며, 장기적으로는 JVM이 공식 지원하는 CPU 프로파일링 이벤트가 필요하다는 것이 글의 결론이다. ## 연속 프로파일링의 기본 원리 - 프로파일러는 일정 시간 동안 스택 트레이스를 반복 수집하고 집계해 애플리케이션의 동작 패턴을 파악한다. - 샘플링 시점은 프로파일러마다 다르다. - CPU 시간 기반: 실제로 스레드가 CPU를 사용하는 동안만 시간이 증가한다. - 벽시계 시간 기반: 실행, 대기, I/O, 락 대기 등 모든 경과 시간을 포함한다. - CPU 시간 프로파일링은 연산 핫스팟을 찾는 데 적합하다. - 예: 무한 루프나 계산 집중 메서드 - 벽시계 시간 프로파일링은 지연 원인을 찾는 데 유용하다. - 예: 느린 데이터베이스 쿼리, 락 대기, I/O - 메모리 할당, 락 경합, 스레드 파킹, GC 등 특정 이벤트를 기준으로 스택을 수집하는 방식도 있다. - 어떤 이벤트를 사용하든 핵심은 “이벤트가 발생했을 때 어떤 코드가 실행 중이었는가”를 스택으로 기록하고 누적하는 것이다. ## JFR `ExecutionSample`의 장점과 한계 - JFR은 JVM에 내장되어 있으며 GC, 클래스 로딩, 스레드 스케줄링, 메모리 할당 등 다양한 런타임 이벤트를 구조화해 제공한다. - 낮은 오버헤드로 상시 실행할 수 있어 운영 환경에 적합하다. - CPU 프로파일링에는 `ExecutionSample` 이벤트가 사용된다. - 그러나 `ExecutionSample`은 운영체제 수준에서 실제 CPU 시간을 직접 기준으로 샘플링하지 않는다. - JVM이 관찰한 실행 가능 스레드 중 일부를 순환하며 샘플링한다. - CPU를 많이 사용하는 스레드가 더 자주 나타날 수는 있지만, 실제 CPU 사용량에 정확히 비례하지는 않는다. - CPU가 포화된 환경이나 리액티브 애플리케이션에서는 스레드 스케줄링 특성 때문에 특정 CPU 핫스팟이 충분히 나타나지 않을 수 있다. - 결과적으로 프로파일이 틀렸다기보다는 데이터가 불완전해져 원인 분석 시간이 길어질 수 있다. ## `AsyncGetCallTrace`를 이용한 CPU 샘플링 - JVMTI 에이전트와 운영체제 신호인 `SIGPROF`를 사용하면 CPU 시간에 기반해 샘플링할 수 있다. - 신호가 발생한 스레드 안에서 HotSpot의 `AsyncGetCallTrace`를 호출해 비동기적으로 Java 스택을 추적한다. - 이 방식은 세이프포인트 편향을 피하고 JFR에 정의된 이벤트가 아닌 임의의 CPU 이벤트에서도 스택을 수집할 수 있다. - `async-profiler` 같은 도구가 이 접근법을 널리 확산시켰다. - 실제 CPU 사용량에 가까운 결과를 제공하므로 CPU-bound 작업 분석에 특히 효과적이다. ## 내부 JVM API 의존성 문제 - `AsyncGetCallTrace`는 공식 공개 API가 아니라 HotSpot 내부 메커니즘이다. - OpenJ9, Zing 등 다른 JVM에서도 지원되지만 안정적인 표준 인터페이스로 설계된 것은 아니다. - 높은 부하나 특수한 상황에서는 오류를 일으킬 가능성이 있어 프로파일러가 JVM 장애를 방지하기 위한 방어 로직을 추가해야 한다. - Datadog 프로파일러는 다음 방식을 조합한다. - JFR: 안정적인 런타임 텔레메트리 수집 - `AsyncGetCallTrace`: 정확한 Java CPU 스택 샘플링 - `vmstructs` 워킹: JVM 내부 메타데이터를 직접 읽어 스택과 런타임 상태 복원 - `vmstructs`는 표준 API가 노출하지 않는 정보를 얻을 수 있지만, 역시 JVM 내부 구조에 의존한다. - 따라서 프로파일러 제작자는 다음과 같은 절충을 해야 한다. - JFR만 사용하면 안전하지만 CPU 샘플링 정확도가 떨어질 수 있다. - 내부 API를 사용하면 정확하지만 JVM 안정성과 유지보수 부담이 커진다. - 실제 상용 프로파일러들은 대체로 두 방식을 함께 사용한다. ## JVM 프로파일링 기반을 개선하려는 움직임 - Datadog, SAP, Amazon, OpenJDK 커뮤니티는 이 문제가 특정 회사만의 문제가 아니라는 데 공감했다. - JFR은 이미 운영 환경용 프로파일링 기반으로 적합하지만, 정확한 CPU 샘플링을 위한 공식 이벤트가 부족했다. - 기존 생태계가 비공개·비공식 HotSpot API에 의존하는 것은 장기적으로 바람직하지 않다. - 글에서는 이러한 한계를 해결하기 위해 JVM에 안전성과 정확성을 모두 갖춘 “일급 CPU 프로파일링 이벤트”를 추가하려는 협력 과정을 소개한다. - 제공된 원문은 OpenJDK 논의와 새 이벤트의 구체적인 설계 설명이 시작되는 부분에서 중단되어 있다. 운영 환경에서는 JFR만으로 CPU 병목을 단정하기보다, CPU 시간 기반 샘플링을 지원하는 프로파일러를 함께 사용하는 것이 좋다. 다만 JVM 내부 API 의존성은 안정성 위험을 동반하므로, 장기적으로는 공식 JFR CPU 프로파일링 이벤트가 제공되는 JVM과 프로파일러를 선택하는 것이 바람직하다.

원문 읽기(새 탭에서 열림)
netflix5분 읽기큐레이션 요약

Nebula ArchRules로 ArchUnit 확장하기

Netflix는 수만 개의 Java 저장소에서 공통 아키텍처 규칙과 기술 부채를 일관되게 점검하기 위해 ArchUnit을 여러 저장소에 배포·적용하는 Nebula ArchRules를 구축했다. ArchUnit은 AST가 아닌 JVM 바이트코드를 분석하므로 Java뿐 아니라 Kotlin·Scala에도 적용할 수 있고, 타입 안전한 Java API로 규칙을 작성·테스트할 수 있다. Nebula 플러그인은 이를 공유 가능한 Gradle 규칙 라이브러리로 확장해 조직 전체의 라이브러리 사용 규칙과 API 생명주기를 자동 검증한다. ## Netflix의 문제: 라이브러리 생명주기와 기술 부채 - Netflix는 수만 개의 Java polyrepo를 운영하므로 공통 빌드 로직과 품질 규칙을 여러 저장소에 배포해야 한다. - 하위 호환성을 깨는 라이브러리 변경 사고를 계기로, deprecated API를 언제 안전하게 제거할 수 있는지 파악할 필요가 생겼다. - API에는 다음과 같은 생명주기 애너테이션을 사용한다. - `@Deprecated`: 더 이상 사용하지 않도록 지정된 API - `@Public`: 하위 프로젝트가 사용하도록 공개된 API - `@Experimental`: 아직 안정성이 보장되지 않는 신규 API - 그 외 API: 기본적으로 내부 구현으로 간주 - 문제는 라이브러리 작성자가 어떤 downstream 프로젝트가 내부 API나 deprecated API를 잘못 사용하고 있는지 알기 어렵다는 점이다. - Spring Boot 주요 버전 업그레이드 같은 fleet-wide migration에서도 deprecated API 사용 현황을 파악하는 것이 중요하다. ## ArchUnit을 선택한 이유 - ArchUnit은 JUnit 테스트 안에서 아키텍처 규칙을 검증하는 오픈소스 라이브러리다. - ASM 기반으로 JVM 바이트코드를 분석하므로 특정 소스 언어의 문법에 덜 의존한다. - Java, Kotlin, Scala처럼 서로 다른 JVM 언어로 작성된 코드에도 같은 규칙을 적용할 수 있다. - 소스 코드의 syntactic sugar에 가려진 실제 실행 바이트코드까지 검사할 수 있다. - 규칙 작성 방식은 두 가지다. - 대부분의 규칙은 fluent builder API로 간결하게 작성한다. - 복잡한 분석에는 더 낮은 수준의 API와 클래스 관계 정보에 접근할 수 있다. - 분석 대상 전체 classpath를 읽고 클래스 간 의존성, 호출 관계, 소유자 등을 그래프로 유지한다. - 단순한 파일 단위 검사를 넘어 호출 사이트와 클래스 관계를 기준으로 규칙을 작성할 수 있다. ## AST 기반 도구와의 차이 - PMD 같은 AST 기반 도구는 소스 문법 구조를 직접 검사한다. - 언어별 문법 차이 때문에 Kotlin이나 Scala 지원 시 규칙을 별도로 작성해야 할 수 있다. - 예상하지 못한 문법적 표현으로 규칙을 우회할 가능성도 있다. - ArchUnit 규칙은 컴파일 결과인 바이트코드를 대상으로 하므로 코드가 어떤 JVM 언어로 작성됐는지보다 실제 동작 구조가 중요해진다. - PMD의 XPath 문자열 규칙과 달리 ArchUnit은 타입 안전한 Java 코드와 fluent API를 사용한다. - IDE 자동완성의 도움을 받을 수 있다. - 별도의 분석 프로세스를 구성하지 않고 규칙 객체와 클래스 목록을 직접 전달해 단위 테스트할 수 있다. ## 규칙 작성의 편의성 - ArchUnit에서는 다음과 같은 형태로 규칙을 표현할 수 있다. - 특정 클래스가 특정 타입에 의존하지 않아야 함 - 특정 생성자를 호출할 때 필수 인자를 전달해야 함 - 특정 패키지 간 의존성을 금지함 - 예를 들어 `DateTime` 객체를 시간대 정보 없이 생성하지 못하게 하는 규칙을 fluent API로 작성할 수 있다. - 규칙은 일반적인 Java 코드이므로 다음 작업이 쉽다. - 리팩터링 - IDE 기반 탐색 - 컴파일 시 오류 검출 - 단위 테스트 - 클래스 관계 그래프를 활용하면 단순한 이름 매칭보다 풍부한 맥락을 가진 규칙을 만들 수 있다. ## 공유 가능한 ArchRules 라이브러리 - 기본 ArchUnit은 하나의 저장소에서 JUnit 테스트로 사용하는 구조다. - Nebula ArchRules는 규칙을 별도 라이브러리로 패키징해 여러 Gradle 저장소에서 재사용할 수 있도록 한다. - `ArchRules Library Plugin`은 Gradle 프로젝트에 `archRules`라는 별도 source set을 추가한다. - 이 source set에는 `ArchRulesService`를 구현하는 클래스를 작성한다. - `ArchRulesService`는 `Map<String, ArchRule>`을 반환하는 단일 추상 메서드를 가진다. - map의 key는 규칙 이름이고 value는 실제 ArchUnit 규칙이다. - 규칙 코드는 본 애플리케이션 코드와 분리되어 별도의 JAR로 패키징된다. - 산출물에는 `arch-rules` classifier가 붙는다. - Gradle Module Metadata를 통해 `arch-rules` usage variant로 배포된다. - 따라서 downstream 프로젝트가 규칙을 사용하려면 Gradle Module Metadata 기반 의존성 해결이 필요하다. ## 독립형 규칙 라이브러리 - Standalone rule library는 본체 애플리케이션 코드 없이 `archRules`만 포함한다. - 다음과 같은 외부 코드 검사에 적합하다. - Java 표준 API 사용 규칙 - Guava 같은 오픈소스 라이브러리 사용 규칙 - 조직 공통 규칙 - `@Deprecated` API 사용 금지 - Netflix는 누구나 사용할 수 있는 OSS standalone 규칙 라이브러리 모음을 유지하며, 직접 규칙을 작성할 때 참고할 수 있는 예제로도 활용한다. ## 번들형 규칙 라이브러리 - Bundled rule library는 일반 라이브러리 코드와 해당 라이브러리 사용 규칙을 함께 배포한다. - `main` source set에는 라이브러리의 실제 기능을 담고, `archRules` source set에는 해당 라이브러리를 사용할 때 지켜야 할 규칙을 담는다. - 예를 들어 특정 라이브러리가 제공하는 공개 API의 올바른 사용법이나, 내부 API·deprecated API에 대한 접근 제한을 규칙으로 포함할 수 있다. - 라이브러리와 사용 규칙을 함께 배포하면 라이브러리 작성자가 권장 사용 방식을 downstream 빌드 과정에서 직접 검증할 수 있다. ## 실용적인 결론 ArchUnit은 단순한 단일 저장소용 아키텍처 테스트를 넘어, 바이트코드와 클래스 관계를 활용하는 범용 JVM 정적 분석 도구로 활용할 수 있다. 조직 차원에서는 Nebula ArchRules처럼 규칙을 별도 Gradle 라이브러리로 배포해 deprecated API 사용, 내부 API 접근, 라이브러리별 권장 패턴을 모든 저장소의 빌드 과정에서 자동 검증하는 방식이 효과적이다.

원문 읽기(새 탭에서 열림)
toss원문

수천 개의 API/BATCH 서버를 하나의 설정 체계로 관리하기 (새 탭에서 열림)

토스페이먼츠는 수천 개의 API 서버와 배치 설정을 관리하기 위해 설정을 단순한 텍스트가 아닌 '진화하는 코드'로 정의하여 운영합니다. 복사-붙여넣기식의 중복 설정을 제거하기 위해 오버레이 아키텍처와 템플릿 패턴을 도입했으며, 이를 통해 오타나 설정 오류로 인한 대규모 정산 장애 리스크를 원천 차단합니다. 결과적으로 인프라 설정을 테스트 가능한 영역으로 끌어올려 대규모 하이브리드 클라우드 환경에서도 높은 안정성과 유연성을 확보했습니다. ### 실시간 API 서버: 오버레이와 템플릿의 결합 * **오버레이 아키텍처:** 설정을 `global`, `cluster`, `phase`, `application` 순서의 계층형 구조로 설계하여 하위 계층이 상위 계층의 기본값을 덮어쓰도록 구성했습니다. 이를 통해 공통 설정은 한 번만 정의하고 각 환경에 필요한 차이점만 관리할 수 있습니다. * **템플릿 패턴 도입:** YAML의 단순 오버레이만으로는 해결하기 어려운 긴 문자열(예: JVM 옵션) 내의 특정 값만 수정하기 위해 `{{MAX_HEAP}}`과 같은 변수 치환 방식을 사용합니다. * **동적 설정 주입:** 설정 파일 내부에 파이썬 스크립트를 삽입하여 랜덤 포트 생성이나 외부 API 호출을 통한 동적 값 할당이 가능하며, 클러스터 이름에 따른 조건부 로직을 적용해 복잡한 환경 변수 요구사항을 해결합니다. ### 배치 서버: DSL과 GitOps를 통한 단순화 * **Jenkins 기반의 단순화:** 대규모 정산 데이터를 다루는 배치 환경일수록 단순함이 강력하다는 원칙 아래, Jenkins를 활용하면서도 수동 조작의 단점을 보완하는 방향을 택했습니다. * **Groovy DSL 활용:** Jenkins의 웹 UI를 통한 수동 설정을 배제하고, Groovy 기반의 자체 DSL(Domain Specific Language)을 구축하여 수천 개의 배치 Job을 코드 형태로 관리합니다. * **GitOps 체계:** 모든 배치 설정을 코드 저장소에서 관리하고 CI/CD 파이프라인과 통합함으로써, 개발자가 직접 Jenkins에 접속하지 않고도 표준화된 환경에서 배치 작업을 배포할 수 있도록 개선했습니다. ### 인프라의 코드화와 검증 자동화 * **테스트 가능한 설정:** 설정값에 대한 오타나 논리적 오류를 방지하기 위해 설정 코드에 대한 유닛 테스트를 수행합니다. 이를 통해 수천 개의 설정 중 단 하나의 오타가 치명적인 금융 장애로 이어지는 것을 사전에 방지합니다. * **유연한 확장성:** 고정된 설정 체계에 안주하지 않고, 인프라의 변화와 개발자의 요구사항에 맞춰 설정 인프라 자체가 계속해서 진화할 수 있는 구조를 지향합니다. 단순히 설정 파일을 잘 작성하는 것에 그치지 않고, 인프라 설정을 애플리케이션 코드와 동일한 수준의 설계와 테스트를 거쳐 관리하는 것이 대규모 시스템의 안정성을 보장하는 핵심입니다. 초기에 다소 복잡해 보일 수 있는 오버레이나 DSL 도입은 장기적으로 중복을 제거하고 휴먼 에러를 막는 가장 확실한 투자입니다.

naver원문

네이버 TV (새 탭에서 열림)

JVM 기반 웹 애플리케이션은 실행 초기 JIT(Just-In-Time) 컴파일러의 최적화 과정에서 발생하는 응답 지연 문제를 해결하기 위해 '웜업' 과정이 필수적입니다. 기존의 API 호출식 웜업은 데이터 오염이나 외부 시스템 부하와 같은 부작용을 초래할 수 있으나, 본 발표에서는 이를 극복하기 위해 핵심 라이브러리만을 직접 예열하는 '라이브러리 웜업' 방식을 제안합니다. 이 기술을 통해 부작용 없이 애플리케이션 배포 직후의 성능을 안정적으로 확보할 수 있습니다. **JVM 웜업의 필요성과 기존 방식의 한계** * JVM은 실행 초기에 인터프리터 방식으로 동작하다가, 반복되는 코드를 JIT 컴파일러가 네이티브 코드로 최적화하는 과정을 거치며 성능이 올라갑니다. * 이 최적화가 완료되기 전까지는 응답 시간이 길어지거나 CPU 사용량이 급증하는 현상이 발생하므로, 실제 트래픽이 들어오기 전 코드를 미리 실행하는 웜업이 필요합니다. * 기존의 API 호출 방식은 가짜 요청을 보내는 과정에서 DB 데이터 정합성을 해칠 수 있고, 외부 API 호출에 따른 불필요한 연동 부하를 발생시키는 단점이 있습니다. **라이브러리 웜업의 핵심 아이디어와 구현** * 비즈니스 로직 전체를 수행하는 대신, 애플리케이션에서 성능 비중이 크고 공통적으로 사용되는 '라이브러리 코드'만을 타겟팅하여 예열합니다. * 예를 들어 JSON 파싱, 암호화, 복잡한 수치 계산 모듈 등 JIT 컴파일 임계치(Threshold)를 넘겨야 하는 핵심 메서드들을 반복 호출하도록 설계합니다. * 애플리케이션 시작 단계(Post-Construct 등)에서 비즈니스 로직과는 독립된 웜업 코드를 실행함으로써 데이터 오염의 위험을 원천적으로 차단합니다. **성능 검증 및 실무적 이점** * 라이브러리 웜업 적용 후, 배포 초기에 발생하는 응답 속도의 '튀는 현상(Spike)'이 현저히 감소하고 전체적인 레이턴시가 안정화됨을 확인했습니다. * API 호출 방식보다 구현이 단순하고 외부 의존성이 적어 관리가 용이하며, 배포 파이프라인의 안정성을 높이는 데 기여합니다. * 다만, 모든 비즈니스 경로를 커버하지는 못하므로 성능 영향도가 높은 핵심 모듈을 선별하여 집중적으로 웜업하는 전략이 유효합니다. 빠른 스케일 아웃이 필요한 마이크로서비스 환경이나 지연 시간에 민감한 실시간 서비스라면, API 기반 웜업의 대안으로 이와 같은 라이브러리 단위의 정밀한 웜업 도입을 적극 권장합니다.

airbnb원문

에어비앤비 (새 탭에서 열림)

에어비앤비는 4.5년에 걸쳐 수천만 라인의 Java, Kotlin, Scala 코드로 구성된 대규모 JVM 모노레포를 Gradle에서 Bazel로 성공적으로 이전했습니다. 이번 마이그레이션을 통해 빌드 속도는 3~5배, IDE 동기화 및 배포 속도는 2~3배 향상되었으며, 개발자 만족도(CSAT)가 38%에서 68%로 크게 올랐습니다. Bazel의 밀폐성(Hermeticity)과 원격 실행 기능을 활용하여 대규모 코드베이스에서도 안정적이고 확장 가능한 빌드 시스템을 구축한 것이 핵심 성과입니다. **Gradle에서 Bazel로 전환한 이유** * **빌드 속도의 혁신:** Bazel의 원격 빌드 실행(RBE)을 통해 수천 개의 작업을 병렬로 처리하며, 'Build without the Bytes' 기능을 도입하여 필요한 아티팩트만 다운로드함으로써 대역폭 소모를 줄였습니다. * **빌드 안정성 및 밀폐성:** Gradle과 달리 샌드박스 환경을 제공하여 빌드 작업이 지정된 입력 외의 파일 시스템(예: /tmp 디렉토리)에 접근하는 것을 차단하고, 환경 차이로 인한 빌드 실패를 방지했습니다. * **통일된 인프라 구축:** 에어비앤비 내의 웹, iOS, Python, Go 등 다양한 언어의 레포지토리를 Bazel로 단일화하여 원격 캐싱, 로깅, 변경된 타겟 계산 로직을 공유할 수 있게 되었습니다. **단계적 마이그레이션과 개념 증명(PoC)** * **Viaduct 플랫폼 선정:** 에어비앤비에서 가장 크고 복잡한 서비스 중 하나인 GraphQL 모놀리스 'Viaduct'를 첫 타겟으로 선정하여, 가장 까다로운 케이스에서 성능 이점을 증명했습니다. * **공존 전략:** 초기에는 개발자가 Gradle과 Bazel 중 선택해서 사용할 수 있도록 두 시스템을 병렬로 운영하여 서비스 중단 위험을 최소화했습니다. * **개발자 설득:** 단순한 성능 향상을 넘어, 초기 단계에서 발생한 버그와 통합 문제를 해결하여 개발자들이 자발적으로 Bazel을 선택하도록 유도했습니다. **자동 빌드 파일 생성 및 유지보수** * **커스텀 생성기 개발:** Bazel 빌드 파일(BUILD)을 수동으로 관리하는 불편을 줄이기 위해 소스 코드의 패키지와 임포트 구문을 분석하여 의존성 그래프를 그리는 자동 생성기를 구축했습니다. * **Gazelle의 영감:** Go 언어의 Gazelle 도구에서 아이디어를 얻었으나, JVM 언어의 특성과 성능 요구사항을 맞추기 위해 캐싱 기능을 포함한 자체 도구로 발전시켰습니다. * **CI 통합:** 모든 커밋 전에 자동 생성기를 실행하여 Gradle과 Bazel의 빌드 그래프가 항상 일치하도록 유지했습니다. **IDE 사용자 경험 개선** * **IntelliJ 동기화 최적화:** 대규모 모노레포에서 Gradle 동기화가 최대 40분까지 소요되던 문제를 Bazel의 병렬 분석과 'Query Sync(실험적 기능)' 도입을 통해 3~10분 수준으로 단축했습니다. * **IntelliJ Aspect 활용:** Bazel의 Aspect 기능을 사용하여 프로젝트 구조 정보를 추출함으로써 IDE가 소스 코드와 의존성을 더 효율적으로 이해하도록 돕습니다. **성공적인 전환을 위한 교훈** 대규모 마이그레이션에서 가장 중요한 것은 **성능에 대한 집착**과 **개발자 경험(DevEx)에 대한 투자**입니다. 빌드 속도가 빨라지면 개발자들은 자연스럽게 새로운 도구를 수용하게 되며, 특히 IntelliJ와 같은 IDE와의 매끄러운 통합이 프로젝트의 성패를 좌우합니다. 또한 빌드 파일 생성과 같은 반복적인 작업을 자동화하여 개발자가 시스템 환경 설정이 아닌 코드 작성에만 집중할 수 있는 환경을 조성하는 것이 필수적입니다.