Storing Null Values in Avro Files

1. 소개

이 튜토리얼에서는 Java에서 Apache Avro를 사용하여 파일 null 값 처리 및 쓰기 방법을 두 가지 탐색할 것입니다. 이러한 null 값 접근 방식은 nullable 필드 처리에 대한 모범 사례를 논의할 수 있게 해줍니다.

2. Avro의 Null 값 문제

Apache Avro는 풍부한 데이터 구조와 컴팩트하고 빠른 이진 데이터 형식을 제공하는 데이터 직렬화 프레임워크입니다. 그러나 Avro에서 null 값 사용은 특별한 주의가 필요합니다.

다음은 문제가 발생할 수 있는 일반적인 시나리오입니다:

GenericRecord record = new GenericData.Record(schema);
record.put("email", null);
// 파일에 쓰기 시 NullPointerException이 발생할 수 있습니다.

기본적으로 Avro 필드는 nullable하지 않습니다. null 값을 저장하려고 하면 직렬화 중에 NullPointerException이 발생합니다.

첫 번째 솔루션을 살펴보기 전에, 올바른 의존성으로 프로젝트를 설정합시다:

<dependency>
    <groupId>org.apache.avro</groupId>
    <artifactId>avro</artifactId>
    <version>1.12.0</version>
</dependency>

3. Null 값 처리 솔루션

이 섹션에서는 Avro에서 null 값을 처리하기 위한 두 가지 주요 접근 방식을 탐색합니다: 스키마 정의 및 주석 기반입니다.

3.1. 세 가지 가능한 방식으로 스키마 정의하기

Avro 스키마를 null 값을 수용할 수 있도록 세 가지 방식으로 정의할 수 있습니다. 첫 번째로, JSON 문자열 접근 방식을 살펴보겠습니다:

private static final String SCHEMA_JSON = """
    {
        "type": "record",
        "name": "User",
        "namespace": "com.baeldung.apache.avro.storingnullvaluesinavrofile",
        "fields": [
            {"name": "id", "type": "long"},
            {"name": "name", "type": "string"},
            {"name": "active", "type": "boolean"},
            {"name": "lastUpdatedBy", "type": ["null", "string"], "default": null},
            {"name": "email", "type": "string"}
        ]
    }""";
public static Schema createSchemaFromJson() {
    return new Schema.Parser().parse(SCHEMA_JSON);
}

여기서는 nullable 필드를 union 타입 구문인 [“null”, “string”]을 사용하여 정의했습니다.

다음으로, SchemaBuilder 접근 방식을 사용하여 더 프로그래밍적인 방식으로 스키마를 정의하겠습니다:

public static Schema createSchemaWithOptionalFields1() {
    return SchemaBuilder
      .record("User")
      .namespace("com.baeldung.apache.avro.storingnullvaluesinavrofile")
      .fields()
      .requiredLong("id")
      .requiredString("name")
      .requiredBoolean("active")
      .name("lastUpdatedBy")
      .type() // 구성 시작
      .unionOf()
      .nullType()
      .and()
      .stringType()
      .endUnion()
      .nullDefault() // 구성 종료
      .requiredString("email")
      .endRecord();
}

이 예제에서는 SchemaBuilder를 사용하여 lastUpdatedBy 필드가 null 또는 문자열 값이 될 수 있는 스키마를 생성합니다.

마지막으로, 위의 스키마와 유사하지만 다른 접근 방식을 사용하여 또 다른 스키마를 생성합시다:

public static Schema createSchemaWithOptionalFields2() {
    return SchemaBuilder
      .record("User")
      .namespace("com.baeldung.apache.avro.storingnullvaluesinavrofile")
      .fields()
      .requiredLong("id")
      .requiredString("name")
      .requiredBoolean("active")
      .requiredString("lastUpdatedBy")
      .optionalString("email")  // 선택적 필드 사용
      .endRecord();
}

type().unionOf().nullType().andStringType().endUnion().nullDefault() 체인 대신 optionalString()을 사용했습니다.

마지막 두 가지 스키마 정의 방법을 간단히 비교해 보겠습니다.

긴 버전은 null 값을 구성할 때 더 많은 제어 옵션을 제공합니다. 짧은 버전은 SchemaBuilder에서 제공하는 구문 설탕입니다. 본질적으로 두 방법은 동일한 동작을 합니다.

3.2. @Nullable 주석 사용

다음 접근 방식은 Avro의 내장 @Nullable 주석을 사용합니다:

public class AvroUser {
    private long id;
    private String name;
    @Nullable
    private Boolean active;  
    private String lastUpdatedBy;  
    private String email; 

    // 나머지 코드
}

이 주석은 Avro의 리플렉션 기반 코드 생성 기능에게 해당 필드가 null 값을 수용할 수 있음을 알립니다.

4. 파일 쓰기 구현

이제 null 값을 포함하는 Record를 직렬화하는 방법을 살펴보겠습니다:

public static void writeToAvroFile(Schema schema, GenericRecord record, String filePath) throws IOException {
    DatumWriter<GenericRecord> datumWriter = new GenericDatumWriter<>(schema);
    try (DataFileWriter<GenericRecord> dataFileWriter = new DataFileWriter<>(datumWriter)) {
        dataFileWriter.create(schema, new File(filePath));
        dataFileWriter.append(record);
    }
}

GenericDatumWriter를 초기화하여 GenericRecord 객체를 처리합니다. 이것은 GenericRecord와 함께 작동하는 구현입니다. 그런 다음, 데이터를 직렬화하는 방법을 알리기 위해 스키마를 생성자 인수로 전달합니다.

그 후, 실제 데이터를 Avro record에 쓰는 클래스인 DataFileWriter를 초기화합니다. 그것은 파일의 메타데이터와 압축도 처리합니다.

그 다음, create() 메서드를 사용하여 지정된 스키마로 Avro 파일을 만듭니다. 여기서 추가 데이터(헤더) 및 메타데이터를 추가합니다.

마지막으로, 실제 record를 파일에 씁니다. 만약 레코드에 @Nullable로 표시된 필드나 union 타입의 필드에 null 값이 포함되어 있으면, 올바르게 직렬화될 것입니다.

5. 솔루션 테스트

이제 우리의 구현이 올바르게 작동하는지 확인합시다:

@Test
void whenSerializingUserWithNullPropFromStringSchema_thenSuccess(@TempDir Path tempDir) {
    user.setLastUpdatedBy(null);
    schema = AvroUser.createSchemaWithOptionalFields1();

    String filePath = tempDir.resolve("test.avro").toString();
    GenericRecord record = AvroUser.createRecord(AvroUser.createSchemaFromJson(), user);

    assertDoesNotThrow(() -> AvroUser.writeToAvroFile(schema, record, filePath));

    File avroFile = new File(filePath);
    assertTrue(avroFile.exists());
    assertTrue(avroFile.length() > 0);
}

이 테스트에서 우리는 처음에 lastUpdatedBy 필드를 null로 설정했습니다. 그런 다음, 처음에 선언한 문자열 스키마를 기반으로 스키마를 생성했습니다.

테스트 결과에서 볼 수 있듯이, 레코드는 null 값을 포함하여 성공적으로 직렬화됩니다:

@Test
void givenSchemaBuilderWithOptionalFields1_whenCreatingSchema_thenSupportsNull(@TempDir Path tempDir) {
    user.setLastUpdatedBy(null);
    String filePath = tempDir.resolve("test.avro").toString();

    schema = AvroUser.createSchemaWithOptionalFields1();
    GenericRecord record = AvroUser.createRecord(schema, user);

    assertTrue(schema.getField("lastUpdatedBy").schema().isNullable(),
        "Union type field should be nullable");
    assertDoesNotThrow(() -> AvroUser.writeToAvroFile(schema, record, filePath));

    File avroFile = new File(filePath);
    assertTrue(avroFile.exists());
    assertTrue(avroFile.length() > 0);
}

두 번째 테스트에서도 비슷한 상황이며, 이번에는 SchemaBuilder를 사용하여 null 필드에 대한 더 긴 구성을 사용합니다.

마지막으로, SchemaBuilder의 두 번째 버전은 더 짧은 null 필드 구성을 가집니다:

@Test
void givenSchemaBuilderWithOptionalFields2_whenCreatingSchema_thenSupportsNull(@TempDir Path tempDir) {
    user.setEmail(null);
    String filePath = tempDir.resolve("test.avro").toString();

    schema = AvroUser.createSchemaWithOptionalFields2();
    GenericRecord record = AvroUser.createRecord(schema, user);

    assertTrue(schema.getField("email").schema().isNullable(),
        "Union type field should be nullable");
    assertDoesNotThrow(() -> AvroUser.writeToAvroFile(schema, record, filePath));

    File avroFile = new File(filePath);
    assertTrue(avroFile.exists());
    assertTrue(avroFile.length() > 0);
}

6. 결론

이 기사에서는 Apache Avro에서 null 값을 처리하는 두 가지 주요 접근 방식을 탐색했습니다. 먼저, 세 가지 방법으로 스키마를 정의하는 방법을 살펴보았습니다. 그런 다음, 클래스 속성에 직접 @Nullable 주석을 구현했습니다.

두 방법 모두 유효하지만, 스키마 접근법은 더 세밀한 제어를 제공하며 일반적으로 프로덕션 시스템에서 선호됩니다.

항상 그랬듯이, 코드는 GitHub에서 확인할 수 있습니다.

원본 출처

You may also like...

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다