← 학습 카테고리

Learn

Airflow

151개 모듈 · 현재 69번째

Airflow 모듈 69/151 airflow-learn-69

Java SDK - XCom 타입 매핑, 빌드/패키징(Gradle·Maven), 코디네이터 설정과 제약

Apache Airflow Official Documentation (in-repo snapshot) — Apache Software Foundation authoring-and-scheduling/language-sdks/java.rst - XCom type mapping, Building and packaging (Gradle/Maven), JavaCoordinator configuration, Pinning the Java executable, Limitations (약 413-750줄)

이 모듈을 다 읽으면

  • Java의 boxed 타입과 primitive 타입이 없는 업스트림 XCom을 어떻게 다르게 처리하는지 설명할 수 있다
  • Gradle과 Maven 각각으로 Fat JAR를 만드는 방식의 핵심 차이를 설명할 수 있다
  • jars_root/java_executable/main_class 등 JavaCoordinator kwargs의 역할을 설명할 수 있다

XCom 값은 JSON으로 저장되어 Java의 Long/Double/String/Boolean/List/Map 등으로 매핑되되 boxed 타입은 없는 값을 null로 안전하게 받는 반면 primitive 타입은 MissingXComException으로 실패하며, Gradle은 플러그인 하나로 Fat JAR을 만들고 Maven은 shade/dependency 플러그인을 조합해 같은 결과를 만든다.

XCom 타입 매핑과 boxed vs primitive

XCom 값은 Airflow 메타데이터 DB에 JSON으로 저장된다. `getXCom`으로 읽어올 때 Python `int`는 대부분 `Long`으로 오지만 값이 너무 크면 `BigInteger`로 오고, `float`는 `Double`로, `str`은 `String`으로, `bool`은 `Boolean`으로, `None`은 `null`로, `list`는 `List<Object>`로, `dict`는 `Map<String, Object>`로 매핑된다.

`@Builder.XCom` 파라미터가 한 번도 push되지 않은 값을 읽으려 하면 `null`로 해석된다. boxed 타입(`Integer`, `Long`, `Boolean` 등) 파라미터는 이 `null`을 안전하게 받을 수 있지만, primitive 타입(`int`, `long`, `boolean` 등) 파라미터는 `null`을 표현할 방법이 없어 태스크가 `MissingXComException`으로 실패한다. 업스트림 XCom이 없을 수도 있는 파라미터라면 primitive가 아니라 boxed 타입으로 선언해야 한다.

핵심 포인트

  • 정수는 대부분 Long으로 오지만 값이 너무 크면 BigInteger로 온다는 점, 그리고 boxed 타입(Integer, Long, Boolean 등)은 없는 XCom을 null로 안전하게 받지만 primitive 타입(int, long, boolean)은 null을 표현할 수 없어 MissingXComException으로 태스크가 실패한다는 차이를 알아야 한다
  • 업스트림 XCom이 없을 수도 있는 파라미터라면 primitive가 아니라 boxed 타입으로 선언해야 안전하다

Gradle과 Maven으로 번들 빌드하기

Gradle에서는 `build.gradle`에 `org.apache.airflow.sdk` 플러그인을 적용하고, `annotationProcessor`(어노테이션 기반 API를 쓸 때만 필요)와 `implementation` 의존성을 추가한 뒤 `airflowBundle { mainClass = ... }`를 지정하면 된다. `./gradlew bundle`을 실행하면 `build/bundle/`에 필요한 모든 JAR이 생기고, 이를 그대로 `jars_root`로 복사하면 된다. 플러그인은 기본적으로 Shadow 플러그인으로 Fat JAR을 생성해 배포 JAR을 하나로 줄여 의존성 충돌 위험을 없애며, `fatJar = false`로 설정하면 Thin JAR을 만들 수 있지만 이 경우 의존성 JAR도 모두 `jars_root`에서 찾을 수 있는 곳에 함께 둬야 한다.

Maven에서는 `airflow-sdk-bom` BOM을 임포트해 아티팩트 버전과 `${airflow.supervisor.schema.version}` 프로퍼티를 한 곳에서 관리하고, `airflow-sdk` 의존성을 추가하며, `maven-compiler-plugin`의 `annotationProcessorPaths`로 어노테이션 프로세서를 런타임 클래스패스 밖에 연결한다. Fat JAR(옵션 1)은 `maven-shade-plugin`으로 만들되, `ManifestResourceTransformer`에서 `mainClass`를 지정하고 `Airflow-Supervisor-Schema-Version` 매니페스트 값을 BOM에서 관리되는 프로퍼티로 채워야 한다(하드코딩 금지) - `mvn package`를 실행하면 `target/<artifactId>-<version>.jar`이 생성된다. Thin JAR(옵션 2)은 `maven-jar-plugin`으로 `Main-Class`를 지정하고 `maven-dependency-plugin`의 `copy-dependencies`로 런타임 의존성을 옆에 모으는 방식이며, 이 경우에는 `Airflow-Supervisor-Schema-Version`을 따로 설정할 필요가 없다 - Airflow가 클래스패스의 `airflow-sdk` JAR에서 이 값을 직접 읽기 때문이다.

Maven에는 Gradle 플러그인의 `verifyBundleMainClass` 같은 컴파일 타임 검증이 없어서, 잘못된 `mainClass`를 지정해도 런타임이 되어서야 문제가 드러난다.

핵심 포인트

  • Gradle은 Shadow 플러그인 기반 Fat JAR을 기본값으로 생성하며(fatJar=false로 Thin JAR 전환 가능), airflowBundle { mainClass = ... }만 지정하면 ./gradlew bundle이 배포 가능한 build/bundle/을 만들어준다
  • Maven Fat JAR(옵션 1)은 maven-shade-plugin으로 만들며 Airflow-Supervisor-Schema-Version 매니페스트 값을 BOM 프로퍼티에서 가져와야 하고(하드코딩 금지), Thin JAR(옵션 2)은 이 값을 별도로 설정할 필요가 없다 - Airflow가 클래스패스의 airflow-sdk JAR에서 직접 읽기 때문이다
  • Maven에는 Gradle의 verifyBundleMainClass 같은 컴파일 타임 검증이 없어서, 잘못된 mainClass를 지정해도 런타임이 되어서야 발견된다

JavaCoordinator 설정과 java 실행 파일 고정, 제약

`JavaCoordinator`의 핵심 kwargs는 `jars_root`(필수, `.jar` 파일을 재귀적으로 스캔할 하나 이상의 디렉터리), `java_executable`(기본값 `"java"`, PATH에서 찾음), `jvm_args`(기본값 `[]`, 예: `["-Xmx1g"]`), `main_class`(기본은 자동 감지 - `jars_root`를 스캔해 `Main-Class`가 설정된 JAR을 찾으며, 실행 가능한 JAR이 여러 개면 결과가 비결정적이므로 이 경우 명시적으로 지정해야 함)다.

`[sdk]` 설정은 시작 시점에 읽히므로, `coordinators`나 `queue_to_coordinator` 변경(예: `jvm_args` 추가)은 스케줄러(또는 `airflow standalone`)를 재시작해야 반영된다. 반면 번들 JAR을 다시 빌드하는 것은 재시작 없이 다음 태스크 실행부터 바로 반영되는데, 태스크 인스턴스마다 새 JVM이 뜨기 때문이다.

`java_executable`은 PATH에서 찾아지는 `java`에 의존하지 말고 절대 경로로 고정하는 것이 일반적으로 권장된다 - 이는 관리자가 시스템 전역 `java`를 통제하지 못하는 프로덕션/기업 환경에서 태스크를 알려진 특정 JDK에 고정하기 위함이다(파이썬 버전을 고정하는 것과 같은 이유다). 예를 들어 macOS에서 Homebrew로 설치한 JDK의 `java`는 PATH에 없으므로 `java_executable`을 명시적으로 그 경로로 지정해야 한다.

알려진 제약으로는, 태스크 인스턴스마다 JVM 서브프로세스가 하나씩 뜨므로 인스턴스 간에 인메모리 상태를 공유해야 하는 태스크는 XCom이나 외부 저장소를 대신 써야 하고, 에셋·디퍼럴 등 일부 Airflow 기능에 대한 지원이 아직 제한적이며(사용자 피드백에 따라 추후 구현될 수 있음) 이 점도 감안해야 한다.

핵심 포인트

  • [sdk] 설정(coordinators, queue_to_coordinator)의 변경은 스케줄러 재시작이 있어야 반영되지만, JAR 자체를 다시 빌드해 jars_root에 올리는 것은 재시작 없이 다음 태스크 실행부터 바로 반영된다 (태스크마다 새 JVM을 띄우기 때문)
  • java_executable은 PATH에 의존하지 말고 절대 경로로 고정하는 것이 권장된다 - 관리자가 시스템 전역 java를 통제하지 못하는 환경(Homebrew macOS 등)에서 특정 JDK 버전을 확실히 쓰기 위함이다
  • Java SDK는 태스크 인스턴스마다 완전히 새 JVM을 띄우므로 인스턴스 간 인메모리 상태 공유가 안 되고(XCom이나 외부 저장소 필요), 에셋·디퍼럴 등 일부 Airflow 기능 지원이 아직 제한적이다