Json보닀 λΉ λ₯΄κ³  μ»΄νŒ©νŠΈν•œ Avro 포맷을 λ§Œλ‚˜λ³΄μž πŸ‘‹

7 minute read

μ™œ Avroλ₯Ό μ“°κ²Œ λ˜μ—ˆλ‚˜μš”??

데이터 μ—”μ§€λ‹ˆμ–΄λ‘œ 일을 ν•˜λ©΄μ„œ ν˜„μž¬ 이벀트 νŒŒμ΄ν”„λΌμΈμ„ λ„λ§‘μ•„μ„œ κ΄€λ¦¬ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. ν•˜λ£¨ 3μ–΅ 건 μ •λ„μ˜ 데이터λ₯Ό λ°›μ•„μ„œ Kafka에 λ°€μ–΄λ„£κ³  μžˆλŠ”λ°μš”! 이 κ³Όμ •μ—μ„œ AvroSerializer둜 데이터λ₯Ό μΈμ½”λ”©ν•˜μ—¬ Kafka에 보내고 μžˆμŠ΅λ‹ˆλ‹€.

그런데, μ–΄λŠ μˆœκ°„!! μ™œ Avro 포맷을 μ“°λŠ” 걸까?λΌλŠ” 생각이 λ“€μ—ˆμŠ΅λ‹ˆλ‹€. 1λ…„ λ„˜κ²Œ 이벀트 νŒŒμ΄ν”„λΌμΈμ„ κ΄€λ¦¬ν•˜κ³  μžˆμ—ˆλŠ”λ°, Avroκ°€ λ­”μ§€ μ œλŒ€λ‘œ μ•Œμ§€λ„ λͺ»ν•˜λŠ” μƒνƒœμ—μ„œ Avroλ₯Ό μ“°κ³  μžˆλ‹€λŠ” κ±Έ 문득 κΉ¨λ‹«κ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€. 이번 ν¬μŠ€νŠΈλŠ” β€˜μ•„μ§ λΆ€μ‘±ν•œκ²Œ λ§Žκ΅¬λ‚˜β€™λΌλŠ” 마음, 그리고 β€˜μ—¬μ „νžˆ μž¬λ°ŒλŠ” 것듀이 λ‚¨μ•˜κ΅°!β€™λΌλŠ” μƒκ°μœΌλ‘œ μ •λ¦¬ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

What is Avro

JSON, CSVλ₯Ό 담은 데이터 ν˜•μ‹μž…λ‹ˆλ‹€. νŠΉμ΄ν•œ 점은 μŠ€ν‚€λ§ˆ 정보가 데이터 νŒŒμΌμ— ν•¨κ»˜ λ‹΄κΈ΄λ‹€λŠ” 점 μž…λ‹ˆλ‹€. 즉, xxxx.avroλΌλŠ” 파일 μ•ˆμ— μ•„λž˜μ™€ 같은 μŠ€ν‚€λ§ˆ 정보λ₯Ό 담을 수 μžˆμŠ΅λ‹ˆλ‹€.

{
  "namespace": "NAMESPACE",
  "name": "NAME",
  "doc": "Any Commentary",
  "type": "record",
  "fields": [
    {"name": "name", "type": "string"},
    {"name": "age", "type": "int"},
  ]
}

μ΄λ•Œ, avro의 μŠ€ν‚€λ§ˆ μ •λ³΄λŠ” .avscλΌλŠ” Avro SchemaλΌλŠ” 포맷에 맞좰 μž‘μ„±λ©λ‹ˆλ‹€.

O’Reilly - Operationalizing the Data Lake

Let’s Start Avro

Avro 포맷을 μ‚¬μš©ν•˜λŠ” μ—¬λŸ¬ 방법이 μžˆμ§€λ§Œ, 이번 ν¬μŠ€νŠΈμ—μ„œλŠ” Python으둜 μ§„ν–‰ν•©λ‹ˆλ‹€. 이λ₯Ό μœ„ν•΄ fastavroλΌλŠ” νŒ¨ν‚€μ§€λ₯Ό μ„€μΉ˜ν•©λ‹ˆλ‹€. μ €λŠ” μž‘μ„± μ‹œμ  κΈ°μ€€μœΌλ‘œ μ΅œμ‹  버전인 1.9.7 λ²„μ „μœΌλ‘œ μ§„ν–‰ν–ˆμŠ΅λ‹ˆλ‹€.

pip install fastavro==1.9.7

Avro Write

from fastavro import parse_schema, writer

schema = {
  "namespace": "earth",
  "name": "person",
  "doc": "person information",
  "type": "record",
  "fields": [
    {"name": "name", "type": "string"},
    {"name": "age", "type": "int"},
  ]
}

parsed_schema = parse_schema(schema)

message = {
  "name": "Jobs",
  "age": 26,
}

with open("./hello.avro", "wb") as f:
  writer(f, parsed_schema, [message] * 100)

κ°„λ‹¨ν•©λ‹ˆλ‹€. fastavro의 writer() ν•¨μˆ˜μ— μŠ€ν‚€λ§ˆμ™€ μ €μž₯ν•  데이터λ₯Ό μ „λ‹¬ν•©λ‹ˆλ‹€.

Avro Read

from fastavro import reader

with open("hello.avro", "rb") as f:
  _reader = reader(f)
  print(_reader)
  print(_reader.writer_schema)
  print(_reader.reader_schema)

  for record in _reader:
    print(record)
    break

Avro νŒŒμΌμ—λŠ” μŠ€ν‚€λ§ˆ 정보가 이미 담겨 있기 λ•Œλ¬Έμ—, 읽을 λ•ŒλŠ” μŠ€ν‚€λ§ˆλ₯Ό μ§€μ •ν•  ν•„μš”κ°€ μ—†μŠ΅λ‹ˆλ‹€.

Avro Schema

Avro μŠ€ν‚€λ§ˆλŠ” JSON 포맷으둜 μž‘μ„±λ©λ‹ˆλ‹€.

{
  "namespace": "earth",
  "name": "person",
  "doc": "person information",
  "type": "record",
  "fields": [
    {"name": "name", "type": "string"},
    {"name": "age", "type": "int"},
  ]
}

.avro νŒŒμΌμ„ 좜λ ₯해보면, Avro SchemaλŠ” λ°”μ΄λ„ˆλ¦¬κ°€ μ§λ ¬ν™”λ˜μ§€ μ•Šκ³ , κ·Έ κ°’ κ·ΈλŒ€λ‘œ μ‘΄μž¬ν•©λ‹ˆλ‹€.

Avro Schema에 μ‘΄μž¬ν•˜λŠ” nameκ³Ό namespace ν•„λ“œλŠ” μŠ€ν‚€λ§ˆλ₯Ό μ‹λ³„ν•˜κΈ° μœ„ν•œ μ‹λ³„μž μž…λ‹ˆλ‹€. μ €λŠ” μ‰½κ²Œ 생각해 name이 RDB의 Table, namespaceκ°€ RBD의 database에 λŒ€μ‘λ˜λŠ” κ°œλ…μ΄λΌκ³  μ΄ν•΄ν–ˆμŠ΅λ‹ˆλ‹€.

Reject Schema Mismatch when AVRO Write

message = {
  "name": "Jobs",
  # no `age` field
}

fastavro.write()λ₯Ό ν•  λ•Œ, μ €μž₯ν•  λ©”μ‹œμ§€κ°€ μ„€μ •ν•œ μŠ€ν‚€λ§ˆμ™€ 뢈일치 ν•œλ‹€λ©΄, avro write이 μΌμ–΄λ‚˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, μŠ€ν‚€λ§ˆμ—λŠ” age ν•„λ“œκ°€ μ •μ˜λ˜μ–΄ μžˆλŠ”λ°, λ©”μ‹œμ§€μ—λŠ” μ—†λ‹€λ©΄ ValueError: no value and no default for age Exception을 보게 λ©λ‹ˆλ‹€.

λ°˜λŒ€λ‘œ, λ©”μ‹œμ§€μ— μ •μ˜ν•˜μ§€ μ•Šμ€ 컬럼이 μΆ”κ°€λ‘œ λ“€μ–΄μ˜€λŠ” κ²½μš°λŠ”

message = {
  "name": "Jobs",
  "age": 26,
  "address": "USA" # new field, but not defined
}

Avro writeλŠ” μ„±κ³΅ν•˜μ§€λ§Œ, μ €μž₯된 데이터λ₯Ό 읽어보면, μ •μ˜ν•˜μ§€ μ•Šμ€ ν•„λ“œλŠ” .avro에 μ €μž₯λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

Rejection은 νƒ€μž…μ— λŒ€ν•΄μ„œλ„ μΌμ–΄λ‚˜λŠ”λ°,

message = {
  "name": "Jobs",
  "age": "26" # string, but defined as int
}

ageλ₯Ό intκ°€ μ•„λ‹Œ string으둜 μ „λ‹¬ν•˜λ©΄ TypeError: an integer is required on field ageλΌλŠ” Exception을 보게 λ©λ‹ˆλ‹€.

Nullable Field

Avroμ—μ„œ μ–΄λ–€ ν•„λ“œκ°€ Nullable ν•œμ§€λŠ” μ•„λž˜μ™€ 같은 μŠ€ν‚€λ§ˆλ‘œ ν‘œν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

{
  "namespace": "earth",
  "name": "person",
  "doc": "person information",
  "type": "record",
  "fields": [
    {"name": "name", "type": "string"},
    {"name": "age", "type": ["null", "int"]},
  ]
}

이런 경우, age ν•„λ“œκ°€ nullable 이기 λ•Œλ¬Έμ—, ν•΄λ‹Ή ν•„λ“œμ˜ 값이 없어도 avro writeκ°€ κ°€λŠ₯ν•©λ‹ˆλ‹€.

Union Field

사싀 μœ„μ—μ„œ nullable ν•„λ“œλ₯Ό ν‘œν˜„ν•˜λŠ” 방법은 Avro μŠ€ν‚€λ§ˆμ˜ β€œUnion” νƒ€μž…μ„ ν™œμš©ν•œ κ²ƒμž…λ‹ˆλ‹€. Union νƒ€μž…μ€ ν•˜λ‚˜μ˜ ν•„λ“œμ— μ—¬λŸ¬ 데이터 νƒ€μž…μ„ μ“Έ 수 μžˆλ„λ‘ ν—ˆμš©ν•©λ‹ˆλ‹€.

{
  "namespace": "earth",
  "name": "person",
  "doc": "person information",
  "type": "record",
  "fields": [
    {"name": "name", "type": "string"},
    {"name": "age", "type": ["null", "int", "string"]},
  ]
}

κ·Έλž˜μ„œ μ΄λ ‡κ²Œ ageκ°€ intμ΄λ©΄μ„œ, stringμ΄λ©΄μ„œ, nullable인 κ²½μš°λ„ κ°€λŠ₯ν•©λ‹ˆλ‹€!!

Schema Reference

Avroμ—μ„œλŠ” μ •μ˜ν•œ μŠ€ν‚€λ§ˆλ₯Ό λ‹€λ₯Έ μŠ€ν‚€λ§ˆμ˜ ν•„λ“œ 객체둜 μ‚¬μš©ν•˜λŠ” 것도 κ°€λŠ₯ν•˜λ‹€. μ•„λž˜ μ˜ˆμ œλŠ” earth.animalλΌλŠ” μŠ€ν‚€λ§ˆλ₯Ό μ •μ˜ν•˜κ³ , 이λ₯Ό earth.personλΌλŠ” μŠ€ν‚€λ§ˆμ˜ pet ν•„λ“œμ˜ νƒ€μž…μœΌλ‘œ μ‚¬μš©ν•˜λŠ” μ˜ˆμ œμ΄λ‹€.

schema = [{
  "namespace": "earth",
  "name": "animal",
  "doc": "animal information",
  "type": "record",
  "fields": [
    {"name": "pet_name", "type": "string"},
    {"name": "type", "type": ["null", "string"]},
  ]
}, {
  "namespace": "earth",
  "name": "person",
  "doc": "person information",
  "type": "record",
  "fields": [
    {"name": "name", "type": "string"},
    {"name": "age", "type": ["null", "int", "string"]},
    {"name": "pet", "type": ["null", "earth.animal"]},
  ]
}]

message = [{
  "name": "John",
  "age": "30",
  "pet": {
    "pet_name": "Maru",
    "type": "dog"
  }
}]

Union Schema

μœ„μ˜ Schema Reference μΌ€μ΄μŠ€μ—μ„œλŠ” xxxx.avro 파일이 ν•˜λ‚˜μ— earth.petκ³Ό earth.person, 두 개의 avro μŠ€ν‚€λ§ˆκ°€ 헀더에 μ €μž₯될 것 μž…λ‹ˆλ‹€. μœ„μ˜ μ˜ˆμ‹œμ—μ„œλŠ” earth.person에 λŒ€ν•œ 데이터λ₯Ό μ €μž₯ν•˜λŠ” κ²ƒμ²˜λŸΌ μ μ—ˆμ§€λ§Œ, μ•„λž˜μ™€ 같이 earth.petκ³Ό earth.person 데이터λ₯Ό ν•˜λ‚˜μ˜ avro νŒŒμΌμ— μ €μž₯ν•˜λŠ” 것도 κ°€λŠ₯ν•©λ‹ˆλ‹€.

...
message = [{
  "name": "John",
  "age": "30",
  "pet": {
    "name": "Maru",
    "type": "dog"
  }
}, {
  "pet_name": "Maru-2",
  "type": "human?"
}]

μ•”νŠΌ μ „ν•˜κ³  싢은 말은 AvroλŠ” 닀쀑 μŠ€ν‚€λ§ˆλ„ μ§€μ›ν•©λ‹ˆλ‹€!

맺음말

AvroλŠ” λͺ…λ°±νžˆ Schemaκ°€ μžˆλŠ” μžλ£Œκ΅¬μ‘°μ§€λ§Œ, Union Fieldλ‚˜ Union Schema의 μΌ€μ΄μŠ€λ₯Ό 보면 닀쀑 νƒ€μž…μ΄λ‚˜ 닀쀑 μŠ€ν‚€λ§ˆλ₯Ό μ§€μ›ν•œλ‹€λŠ” μœ μ—°μ„±λ„ κ°–μΆ”κ³  μžˆμŠ΅λ‹ˆλ‹€.

포슀트λ₯Ό μž‘μ„±ν•˜λ©΄μ„œ μ§€κΈˆμ˜ 이벀트 νŒŒμ΄ν”„λΌμΈ μ½”λ“œκ°€ μ–΄λ–€ μ˜λ„λ‘œ μ»€μŠ€ν…€μ„ ν–ˆλŠ”μ§€ μžμ„Ένžˆ μ‚΄νŽ΄λ³΄λŠ” 계기가 된 것 κ°™μŠ΅λ‹ˆλ‹€. μ•žμœΌλ‘œ μ’…μ’… Kafkaλ₯Ό κ³΅λΆ€ν•˜λ©° μ ‘ν•˜λŠ” 지식듀을 μ •λ¦¬ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€ γ…Žγ…Ž

Categories:

Updated: