Java SDK - 실행 모델과 두 가지 태스크 작성 API
Apache Airflow Official Documentation (in-repo snapshot) — Apache Software Foundation authoring-and-scheduling/language-sdks/java.rst - Java SDK (도입부), Prerequisites, Quick start, Writing tasks: Annotation-based API, Interface-based API (약 18-256줄)
이 모듈을 다 읽으면
- Java SDK에서 태스크 인스턴스마다 새 JVM 서브프로세스가 뜨는 실행 모델을 설명할 수 있다
- Annotation 기반 API와 Interface 기반 API의 차이와 각각의 제약을 설명할 수 있다
- Interface 기반 API의 태스크 클래스가 지켜야 할 4가지 제약을 나열할 수 있다
Java SDK는 태스크 인스턴스마다 JavaCoordinator가 새 JVM 서브프로세스를 띄워 실행하며, Dag 작성자는 어노테이션(@Builder.Dag/@Builder.Task/@Builder.XCom)으로 컴파일 타임에 보일러플레이트를 생성시키는 방식과, Task 인터페이스를 직접 구현해 리플렉션으로 인스턴스화되는 방식 중 하나를 선택할 수 있다.
실행 모델과 사전 요구사항
Java SDK는 Java, Kotlin을 비롯한 모든 JVM 언어로 태스크 로직을 구현할 수 있게 해준다. Dag와 그 스케줄링은 여전히 파이썬에 남고, 개별 태스크는 `JavaCoordinator`가 태스크 인스턴스마다 띄우는 JVM 서브프로세스에 위임된다. 워커 노드에는 JRE 11 이상이 있어야 하고, 컴파일된 태스크 JAR과 그 JVM 의존성이 워커에서 접근 가능해야 하며, `apache-airflow-task-sdk` 패키지(Airflow 설치 시 함께 포함)가 코디네이터를 제공하므로 추가 파이썬 패키지는 필요 없다.
예를 들어 파이썬 쪽은 `@task.stub(queue="java")`로 `extract`/`transform` stub을 선언하고 일반 `@task()`로 `load`를 두는 식이며, Java 쪽은 `@Builder.Dag`/`@Builder.Task`로 대응하는 메서드를 구현하고 `@Builder.XCom`으로 업스트림의 반환값 XCom을 주입받는다. 여기서 중요한 점은, 업스트림 XCom을 받으려면 파이썬 `transform` stub과 Java `transform` 메서드 양쪽 모두에 인자가 필요하다는 것이다 - 파이썬 쪽 인자는 의존성(순서)을 선언하는 용도이고, Java 쪽 인자는 실제로 값을 꺼내 쓰는 용도다.
Java 진입점은 `BundleBuilder`를 구현하는 클래스다 - `getDags()`가 컴파일 타임에 생성된 `<ClassName>Builder.build()` 결과 목록을 반환하고, `main()`은 `Server.create(args).serve(new Main().build())` 한 줄이면 된다.
핵심 포인트
- Java SDK는 태스크 인스턴스마다 완전히 새로운 JVM 서브프로세스를 띄우는 실행 모델이며, 워커에는 JRE 11 이상과 컴파일된 태스크 JAR들이 준비되어 있어야 한다
- 업스트림 XCom을 받으려면 파이썬 stub 쪽과 Java 메서드 쪽 모두에 인자가 필요하다 - 파이썬 쪽은 의존성 선언용, Java 쪽은 실제 값을 꺼내 쓰기 위함이다
- Java 진입점은 BundleBuilder를 구현하는 클래스이며 getDags()가 컴파일 타임에 생성된 <Class>Builder.build() 목록을 반환하고, main()은 Server.create(args).serve(...) 한 줄이면 된다
Annotation 기반 API
평범한 Java 클래스에 어노테이션을 붙이고 컴파일 타임에 보일러플레이트를 SDK가 생성하도록 하는 방식이다. `@Builder.Dag(id = "...")`는 클래스를 태스크 컨테이너로 표시하며, 이 `id`는 파이썬 Dag의 `dag_id`와 일치해야 한다. `@Builder.Task(id = "...")`는 메서드를 태스크 구현으로 표시하며, 이 `id`는 파이썬 Dag의 `@task.stub` 함수 이름과 일치해야 하고 `id`를 생략하면 메서드 이름이 그대로 쓰인다. `@Builder.XCom(task = "...")`는 명명된 업스트림 태스크의 `return_value` XCom을 메서드 파라미터로 주입하며, 파라미터 타입은 저장된 값과 호환되어야 한다 (XCom 타입 매핑 참고).
어노테이션 프로세서는 태스크 레지스트리 연결과 XCom 주입을 자동으로 처리하는 `<ClassName>Builder` 클래스를 생성한다. 태스크 메서드는 `throws Exception`을 선언할 수 있으며, 잡히지 않은 예외는 그 태스크 인스턴스를 Airflow에서 실패로 표시한다 (stub에 재시도가 설정되어 있으면 트리거된다).
핵심 포인트
- @Builder.Dag/@Builder.Task의 id는 각각 파이썬 dag_id와 @task.stub 함수 이름과 정확히 일치해야 하며, @Builder.Task의 id를 생략하면 메서드 이름이 그대로 쓰인다
- 어노테이션 프로세서가 컴파일 타임에 <ClassName>Builder를 생성해 태스크 레지스트리 연결과 XCom 주입을 자동 처리하므로, 개발자는 보일러플레이트를 직접 작성하지 않는다
Interface 기반 API와 리플렉션 제약
`Task` 인터페이스를 직접 구현하면 태스크 등록 방식과 XCom을 어떻게 읽을지에 대해 완전한 제어권을 가질 수 있다. 각 태스크는 `DagDef`에 `TaskDef`로 등록된다.
러너는 태스크 인스턴스가 실행될 때마다 리플렉션으로 태스크 클래스의 새 인스턴스를 만드는데, 이 때문에 클래스에 네 가지 제약이 붙는다 - 태스크 클래스 자체가 `public`이어야 하고, 추상 클래스나 인터페이스가 아닌 구체 클래스여야 하며, `public` 무인자 생성자를 선언해야 하고, 다른 클래스 안에 중첩되어 있다면 반드시 `static` 중첩 클래스여야 한다.
이 조건 중 하나라도 어기는 클래스는 컴파일 타임이 아니라 런타임에 태스크 로그에서 "Cannot instantiate task class" 에러로만 드러난다. `BundleBuilder`에서 `DagDef("my_dag").addTask("fetch", FetchTask.class).addTask("process", ProcessTask.class)`처럼 태스크를 수동으로 등록하며, 태스크 클래스는 최상위 클래스(`FetchTask`)일 수도, `static` 중첩 클래스(`ProcessTask`)일 수도 있다.
핵심 포인트
- Interface 기반 API는 태스크 인스턴스 실행마다 리플렉션으로 클래스를 새로 인스턴스화하므로, 태스크 클래스는 public이어야 하고, 구체 클래스여야 하며, public 무인자 생성자를 가져야 하고, 중첩 클래스라면 반드시 static 중첩 클래스여야 한다
- 이 네 조건 중 하나라도 어기면 컴파일 타임이 아니라 런타임에 태스크 로그의 Cannot instantiate task class 에러로만 드러난다
- Interface 기반 API에서는 BundleBuilder에서 DagDef에 태스크를 수동으로 addTask(id, Class)로 등록해, 어노테이션 프로세서 없이도 등록 방식과 XCom 읽기 방식을 완전히 직접 제어할 수 있다