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에서 확인할 수 있습니다.