Spark schemaEvolution์˜ ์‹ค์ œ ์‚ฌ์šฉ

๋…์ž ์—ฌ๋Ÿฌ๋ถ„, ์ข‹์€ ํ•˜๋ฃจ ๋˜์„ธ์š”!

์ด ๊ธฐ์‚ฌ์—์„œ๋Š” Neoflex ๋น… ๋ฐ์ดํ„ฐ ์†”๋ฃจ์…˜ ๋น„์ฆˆ๋‹ˆ์Šค ์˜์—ญ์˜ ์ˆ˜์„ ์ปจ์„คํ„ดํŠธ๊ฐ€ Apache Spark๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ€๋ณ€ ๊ตฌ์กฐ ์‡ผ์ผ€์ด์Šค๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ์˜ต์…˜์— ๋Œ€ํ•ด ์ž์„ธํžˆ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.

๋ฐ์ดํ„ฐ ๋ถ„์„ ํ”„๋กœ์ ํŠธ์˜ ์ผํ™˜์œผ๋กœ ๋Š์Šจํ•˜๊ฒŒ ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋งค์žฅ์„ ๊ตฌ์ถ•ํ•˜๋Š” ์ž‘์—…์ด ์ž์ฃผ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

์ผ๋ฐ˜์ ์œผ๋กœ JSON ๋˜๋Š” XML๋กœ ์ €์žฅ๋œ ๋‹ค์–‘ํ•œ ์‹œ์Šคํ…œ์˜ ๋กœ๊ทธ ๋˜๋Š” ์‘๋‹ต์ž…๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๊ฐ€ Hadoop์— ์—…๋กœ๋“œ๋œ ๋‹ค์Œ ๋ฐ์ดํ„ฐ์—์„œ ๋งค์žฅ์„ ๊ตฌ์ถ•ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด Impala๋ฅผ ํ†ตํ•ด ์ƒ์„ฑ๋œ ์‡ผ์ผ€์ด์Šค์— ๋Œ€ํ•œ ์•ก์„ธ์Šค๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ๊ฒฝ์šฐ ๋Œ€์ƒ ์ƒ์  ์ฒซํ™”๋ฉด์˜ ์Šคํ‚ค๋งˆ๋ฅผ ๋ฏธ๋ฆฌ ์•Œ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋”์šฑ์ด ์ฒด๊ณ„๋Š” ๋ฐ์ดํ„ฐ์— ์˜์กดํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฏธ๋ฆฌ ์ž‘์„ฑํ•  ์ˆ˜ ์—†์œผ๋ฉฐ ์šฐ๋ฆฌ๋Š” ์ด๋Ÿฌํ•œ ๋งค์šฐ ๋Š์Šจํ•˜๊ฒŒ ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃจ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ์˜ค๋Š˜ ๋‹ค์Œ ์‘๋‹ต์ด ๊ธฐ๋ก๋ฉ๋‹ˆ๋‹ค.

{source: "app1", error_code: ""}

๋‚ด์ผ ๊ฐ™์€ ์‹œ์Šคํ…œ์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‹ต๋ณ€์ด ๋‚˜์˜ต๋‹ˆ๋‹ค.

{source: "app1", error_code: "error", description: "Network error"}

๊ฒฐ๊ณผ์ ์œผ๋กœ ์‡ผ์ผ€์ด์Šค์— ์„ค๋ช… ํ•„๋“œ๊ฐ€ ํ•˜๋‚˜ ๋” ์ถ”๊ฐ€๋˜์–ด์•ผ ํ•˜๋ฉฐ ๊ทธ๊ฒƒ์ด ์˜ฌ์ง€ ์—ฌ๋ถ€๋Š” ์•„๋ฌด๋„ ๋ชจ๋ฆ…๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌํ•œ ๋ฐ์ดํ„ฐ์— ์ƒ์  ์ฒซํ™”๋ฉด์„ ์ƒ์„ฑํ•˜๋Š” ์ž‘์—…์€ ๋งค์šฐ ํ‘œ์ค€์ ์ด๋ฉฐ Spark์—๋Š” ์ด๋ฅผ ์œ„ํ•œ ์—ฌ๋Ÿฌ ๋„๊ตฌ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์†Œ์Šค ๋ฐ์ดํ„ฐ๋ฅผ ๊ตฌ๋ฌธ ๋ถ„์„ํ•˜๊ธฐ ์œ„ํ•ด JSON๊ณผ XML์„ ๋ชจ๋‘ ์ง€์›ํ•˜๋ฉฐ ์ด์ „์— ์•Œ๋ ค์ง€์ง€ ์•Š์€ ์Šคํ‚ค๋งˆ์— ๋Œ€ํ•ด์„œ๋Š” schemaEvolution์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

์–ธ๋œป ๋ณด๊ธฐ์— ํ•ด๊ฒฐ์ฑ…์€ ๊ฐ„๋‹จํ•ด ๋ณด์ž…๋‹ˆ๋‹ค. JSON์ด ํฌํ•จ๋œ ํด๋”๋ฅผ ๊ฐ€์ ธ์™€์„œ ๋ฐ์ดํ„ฐ ํ”„๋ ˆ์ž„์œผ๋กœ ์ฝ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Spark๋Š” ์Šคํ‚ค๋งˆ๋ฅผ ๋งŒ๋“ค๊ณ  ์ค‘์ฒฉ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ตฌ์กฐ๋กœ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค. ๋˜ํ•œ Hive ๋ฉ”ํƒ€์Šคํ† ์–ด์— ๋งค์žฅ์„ ๋“ฑ๋กํ•˜์—ฌ Impala์—์„œ๋„ ์ง€์›๋˜๋Š” ๋งˆ๋ฃจ์— ๋ชจ๋“  ๊ฒƒ์„ ์ €์žฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋ชจ๋“  ๊ฒƒ์ด ๋‹จ์ˆœํ•œ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ๋ฌธ์„œ์˜ ์งง์€ ์˜ˆ์ œ์—์„œ๋Š” ์‹ค์ œ๋กœ ๋งŽ์€ ๋ฌธ์ œ๋ฅผ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š”์ง€ ๋ช…ํ™•ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋ฌธ์„œ๋Š” ์ƒ์  ์ฒซ ํ™”๋ฉด์„ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ JSON ๋˜๋Š” XML์„ ๋ฐ์ดํ„ฐ ํ”„๋ ˆ์ž„์œผ๋กœ ์ฝ๋Š” ์ ‘๊ทผ ๋ฐฉ์‹์„ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.

์ฆ‰, ๋‹จ์ˆœํžˆ JSON์„ ์ฝ๊ณ  ๊ตฌ๋ฌธ ๋ถ„์„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

df = spark.read.json(path...)

์ด๋Š” Spark์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋งŒ๋“ค๊ธฐ์— ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.

์‹ค์ œ๋กœ ์Šคํฌ๋ฆฝํŠธ๋Š” ํด๋”์—์„œ JSON ํŒŒ์ผ์„ ์ฝ๊ณ  ๋ฐ์ดํ„ฐ ํ”„๋ ˆ์ž„์„ ๋งŒ๋“œ๋Š” ๊ฒƒ๋ณด๋‹ค ํ›จ์”ฌ ๋” ๋ณต์žกํ•ฉ๋‹ˆ๋‹ค. ์ƒํ™ฉ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์ด๋ฏธ ํŠน์ • ๋งค์žฅ์ด ์žˆ๊ณ  ๋งค์ผ ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด์˜ค๊ณ  ๋งค์žฅ์— ์ถ”๊ฐ€ํ•ด์•ผํ•˜๋ฉฐ ๊ณ„ํš์ด ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Œ์„ ์žŠ์ง€ ๋งˆ์‹ญ์‹œ์˜ค.

์‡ผ์ผ€์ด์Šค๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ์ผ๋ฐ˜์ ์ธ ๊ณ„ํš์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

1 ๋‹จ๊ณ„. ๋ฐ์ดํ„ฐ๋Š” ํ›„์† ์ผ์ผ ๋‹ค์‹œ ๋กœ๋“œ์™€ ํ•จ๊ป˜ Hadoop์— ๋กœ๋“œ๋˜๊ณ  ์ƒˆ ํŒŒํ‹ฐ์…˜์— ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค. ๋‚ ์งœ๋ณ„๋กœ ๋ถ„ํ• ๋œ ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š” ํด๋”๊ฐ€ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.

2 ๋‹จ๊ณ„. ์ดˆ๊ธฐ ๋กœ๋“œ ์ค‘์— Spark์—์„œ ์ด ํด๋”๋ฅผ ์ฝ๊ณ  ๊ตฌ๋ฌธ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ ํ”„๋ ˆ์ž„์€ ๊ตฌ๋ฌธ ๋ถ„์„ ๊ฐ€๋Šฅํ•œ ํ˜•์‹(์˜ˆ: Parquet ํ˜•์‹)์œผ๋กœ ์ €์žฅ๋˜์–ด Impala๋กœ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ง€๊ธˆ๊นŒ์ง€ ๋ˆ„์ ๋œ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋กœ ๋Œ€์ƒ ์‡ผ์ผ€์ด์Šค๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

3 ๋‹จ๊ณ„. ๋งค์ผ ์Šคํ† ์–ดํ”„๋ก ํŠธ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋‹ค์šด๋กœ๋“œ๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.
์ฆ๋ถ„ ๋กœ๋“œ, ์‡ผ์ผ€์ด์Šค๋ฅผ ๋ถ„ํ• ํ•ด์•ผ ํ•  ํ•„์š”์„ฑ, ์‡ผ์ผ€์ด์Šค์˜ ์ผ๋ฐ˜ ๊ตฌ์„ฑ ์œ ์ง€ ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ตฌ์ถ•์˜ ์ฒซ ๋ฒˆ์งธ ๋‹จ๊ณ„๊ฐ€ ๊ตฌํ˜„๋˜์—ˆ๊ณ  JSON ํŒŒ์ผ์ด ํด๋”์— ์—…๋กœ๋“œ๋˜์—ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

๊ทธ๋“ค๋กœ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ ํ”„๋ ˆ์ž„์„ ์ƒ์„ฑํ•œ ๋‹ค์Œ ์‡ผ์ผ€์ด์Šค๋กœ ์ €์žฅํ•˜๋Š” ๊ฒƒ์€ ๋ฌธ์ œ๊ฐ€ ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ Spark ์„ค๋ช…์„œ์—์„œ ์‰ฝ๊ฒŒ ์ฐพ์„ ์ˆ˜ ์žˆ๋Š” ์ฒซ ๋ฒˆ์งธ ๋‹จ๊ณ„์ž…๋‹ˆ๋‹ค.

df = spark.read.option("mergeSchema", True).json(".../*") 
df.printSchema()

root 
|-- a: long (nullable = true) 
|-- b: string (nullable = true) 
|-- c: struct (nullable = true) |    
|-- d: long (nullable = true)

๋ชจ๋“  ๊ฒƒ์ด ๊ดœ์ฐฎ์€ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

JSON์„ ์ฝ๊ณ  ๊ตฌ๋ฌธ ๋ถ„์„ํ•œ ๋‹ค์Œ ๋ฐ์ดํ„ฐ ํ”„๋ ˆ์ž„์„ ์ชฝ๋ชจ์ด ์„ธ๊ณต์œผ๋กœ ์ €์žฅํ•˜๊ณ  ํŽธ๋ฆฌํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ Hive์— ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.

df.write.format(โ€œparquetโ€).option('path','<External Table Path>').saveAsTable('<Table Name>')

์šฐ๋ฆฌ๋Š” ์ฐฝ๋ฌธ์„ ์–ป์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ๋‹ค์Œ๋‚  ์†Œ์Šค์˜ ์ƒˆ ๋ฐ์ดํ„ฐ๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. JSON์ด ํฌํ•จ๋œ ํด๋”์™€ ์ด ํด๋”์—์„œ ์ƒ์„ฑ๋œ ์‡ผ์ผ€์ด์Šค๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์›๋ณธ์—์„œ ๋‹ค์Œ ๋ฐ์ดํ„ฐ ๋ฐฐ์น˜๋ฅผ ๋กœ๋“œํ•œ ํ›„ ๋ฐ์ดํ„ฐ ๋งˆํŠธ์—์„œ ํ•˜๋ฃจ ๋ถ„๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๋…ผ๋ฆฌ์  ์†”๋ฃจ์…˜์€ ์ ํฌ๋ฅผ ๋‚ ์งœ๋ณ„๋กœ ๋ถ„ํ• ํ•˜์—ฌ ๋‹ค์Œ ๋‚ ๋งˆ๋‹ค ์ƒˆ ๋ถ„ํ• ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด์— ๋Œ€ํ•œ ๋ฉ”์ปค๋‹ˆ์ฆ˜๋„ ์ž˜ ์•Œ๋ ค์ ธ ์žˆ์œผ๋ฉฐ Spark๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํŒŒํ‹ฐ์…˜์„ ๋ณ„๋„๋กœ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋จผ์ € ์ดˆ๊ธฐ ๋กœ๋“œ๋ฅผ ์ˆ˜ํ–‰ํ•˜์—ฌ ์œ„์—์„œ ์„ค๋ช…ํ•œ ๋Œ€๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ  ๋ถ„ํ• ๋งŒ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ž‘์—…์„ ๋งค์žฅ ์ดˆ๊ธฐํ™”๋ผ๊ณ  ํ•˜๋ฉฐ ํ•œ ๋ฒˆ๋งŒ ์ˆ˜ํ–‰๋ฉ๋‹ˆ๋‹ค.

df.write.partitionBy("date_load").mode("overwrite").parquet(dbpath + "/" + db + "/" + destTable)

๋‹ค์Œ๋‚  ์ƒˆ ํŒŒํ‹ฐ์…˜๋งŒ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.

df.coalesce(1).write.mode("overwrite").parquet(dbpath + "/" + db + "/" + destTable +"/date_load=" + date_load + "/")

๋‚จ์€ ๊ฒƒ์€ Hive์— ๋‹ค์‹œ ๋“ฑ๋กํ•˜์—ฌ ์Šคํ‚ค๋งˆ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
๊ทธ๋Ÿฌ๋‚˜ ์—ฌ๊ธฐ์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

์ฒซ ๋ฒˆ์งธ ๋ฌธ์ œ. ์กฐ๋งŒ๊ฐ„ ๊ฒฐ๊ณผ ์ชฝ๋ชจ์ด ์„ธ๊ณต ๋งˆ๋ฃจ๋ฅผ ์ฝ์„ ์ˆ˜ ์—†๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” Parquet์™€ JSON์ด ๋นˆ ํ•„๋“œ๋ฅผ ๋‹ค๋ฅด๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

์ผ๋ฐ˜์ ์ธ ์ƒํ™ฉ์„ ์ƒ๊ฐํ•ด ๋ด…์‹œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์–ด์ œ JSON์ด ๋„์ฐฉํ–ˆ์Šต๋‹ˆ๋‹ค.

ะ”ะตะฝัŒ 1: {"a": {"b": 1}},

์˜ค๋Š˜๋‚  ๋™์ผํ•œ JSON์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

ะ”ะตะฝัŒ 2: {"a": null}

๊ฐ๊ฐ ํ•œ ์ค„์ด ์žˆ๋Š” ๋‘ ๊ฐœ์˜ ๋‹ค๋ฅธ ํŒŒํ‹ฐ์…˜์ด ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.
์ „์ฒด ์†Œ์Šค ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์„ ๋•Œ Spark๋Š” ์œ ํ˜•์„ ๊ฒฐ์ •ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ "a"๊ฐ€ "๊ตฌ์กฐ" ์œ ํ˜•์˜ ํ•„๋“œ์ด๊ณ  INT ์œ ํ˜•์˜ ์ค‘์ฒฉ ํ•„๋“œ "b"๊ฐ€ ์žˆ์Œ์„ ์ดํ•ดํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๊ฐ ํŒŒํ‹ฐ์…˜์ด ๊ฐœ๋ณ„์ ์œผ๋กœ ์ €์žฅ๋œ ๊ฒฝ์šฐ ํ˜ธํ™˜๋˜์ง€ ์•Š๋Š” ํŒŒํ‹ฐ์…˜ ๊ตฌ์„ฑํ‘œ๊ฐ€ ์žˆ๋Š” ๋งˆ๋ฃจ๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

df1 (a: <struct<"b": INT>>)
df2 (a: STRING NULLABLE)

์ด ์ƒํ™ฉ์€ ์ž˜ ์•Œ๋ ค์ ธ ์žˆ์œผ๋ฏ€๋กœ ์†Œ์Šค ๋ฐ์ดํ„ฐ๋ฅผ ๊ตฌ๋ฌธ ๋ถ„์„ํ•  ๋•Œ ๋นˆ ํ•„๋“œ๋ฅผ ์ œ๊ฑฐํ•˜๋Š” ์˜ต์…˜์ด ํŠน๋ณ„ํžˆ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

df = spark.read.json("...", dropFieldIfAllNull=True)

์ด ๊ฒฝ์šฐ ๋งˆ๋ฃจ๋Š” ํ•จ๊ป˜ ์ฝ์„ ์ˆ˜ ์žˆ๋Š” ํŒŒํ‹ฐ์…˜์œผ๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.
์‹ค์ œ๋กœ ์ด๊ฒƒ์„ ํ•ด๋ณธ ์‚ฌ๋žŒ๋“ค์€ ์—ฌ๊ธฐ์„œ ์”์“ธํ•˜๊ฒŒ ์›ƒ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์™œ? ์˜ˆ, ๋‘ ๊ฐ€์ง€ ์ƒํ™ฉ์ด ๋” ์žˆ์„ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๋˜๋Š” ์„ธ. ๋˜๋Š” 1. ๊ฑฐ์˜ ํ™•์‹คํ•˜๊ฒŒ ๋ฐœ์ƒํ•˜๋Š” ์ฒซ ๋ฒˆ์งธ๋Š” ์ˆซ์ž ์œ ํ˜•์ด ๋‹ค๋ฅธ JSON ํŒŒ์ผ์—์„œ ๋‹ค๋ฅด๊ฒŒ ๋ณด์ธ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์˜ˆ: {intField: 1.1} ๋ฐ {intField: XNUMX}. ์ด๋Ÿฌํ•œ ํ•„๋“œ๊ฐ€ ํ•˜๋‚˜์˜ ํŒŒํ‹ฐ์…˜์—์„œ ๋ฐœ๊ฒฌ๋˜๋ฉด ์Šคํ‚ค๋งˆ ๋ณ‘ํ•ฉ์ด ๋ชจ๋“  ๊ฒƒ์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ฝ์–ด ๊ฐ€์žฅ ์ •ํ™•ํ•œ ์œ ํ˜•์œผ๋กœ ์ด์–ด์ง‘๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๋‹ค๋ฅธ ๊ฒฝ์šฐ ํ•˜๋‚˜๋Š” intField: int์ด๊ณ  ๋‹ค๋ฅธ ํ•˜๋‚˜๋Š” intField: double์ž…๋‹ˆ๋‹ค.

์ด ์ƒํ™ฉ์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๋‹ค์Œ ํ”Œ๋ž˜๊ทธ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

df = spark.read.json("...", dropFieldIfAllNull=True, primitivesAsString=True)

์ด์ œ ๋‹จ์ผ ๋ฐ์ดํ„ฐ ํ”„๋ ˆ์ž„์œผ๋กœ ์ฝ์„ ์ˆ˜ ์žˆ๋Š” ํŒŒํ‹ฐ์…˜๊ณผ ์ „์ฒด ์‡ผ์ผ€์ด์Šค์˜ ์œ ํšจํ•œ ๋งˆ๋ฃจ๊ฐ€ ์žˆ๋Š” ํด๋”๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ? ์•„๋‹ˆ์š”.

Hive์— ํ…Œ์ด๋ธ”์„ ๋“ฑ๋กํ–ˆ์Œ์„ ๊ธฐ์–ตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Hive๋Š” ํ•„๋“œ ์ด๋ฆ„์—์„œ ๋Œ€์†Œ๋ฌธ์ž๋ฅผ ๊ตฌ๋ถ„ํ•˜์ง€ ์•Š์ง€๋งŒ Parquet์€ ๋Œ€์†Œ๋ฌธ์ž๋ฅผ ๊ตฌ๋ถ„ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์Šคํ‚ค๋งˆ๊ฐ€ ์žˆ๋Š” ํŒŒํ‹ฐ์…˜: field1: int ๋ฐ Field1: int๋Š” Hive์—์„œ ๋™์ผํ•˜์ง€๋งŒ Spark์—์„œ๋Š” ๋™์ผํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ•„๋“œ ์ด๋ฆ„์„ ์†Œ๋ฌธ์ž๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ฒƒ์„ ์žŠ์ง€ ๋งˆ์‹ญ์‹œ์˜ค.

๊ทธ ํ›„์—๋Š” ๋ชจ๋“  ๊ฒƒ์ด ๊ดœ์ฐฎ์€ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ๋ชจ๋“  ๊ฒƒ์ด ๊ทธ๋ ‡๊ฒŒ ๊ฐ„๋‹จํ•˜์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค. ์ž˜ ์•Œ๋ ค์ง„ ๋‘ ๋ฒˆ์งธ ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ๊ฐ์˜ ์ƒˆ ํŒŒํ‹ฐ์…˜์€ ๋ณ„๋„๋กœ ์ €์žฅ๋˜๋ฏ€๋กœ ํŒŒํ‹ฐ์…˜ ํด๋”์—๋Š” Spark ์„œ๋น„์Šค ํŒŒ์ผ(์˜ˆ: _SUCCESS ์ž‘์—… ์„ฑ๊ณต ํ”Œ๋ž˜๊ทธ)์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ์ชฝ๋ชจ์ด ์„ธ๊ณต์„ ์‹œ๋„ํ•  ๋•Œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๋ ค๋ฉด Spark๊ฐ€ ํด๋”์— ์„œ๋น„์Šค ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ•˜์ง€ ๋ชปํ•˜๋„๋ก ๊ตฌ์„ฑ์„ ๊ตฌ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

hadoopConf = sc._jsc.hadoopConfiguration()
hadoopConf.set("parquet.enable.summary-metadata", "false")
hadoopConf.set("mapreduce.fileoutputcommitter.marksuccessfuljobs", "false")

์ด์ œ ๋งค์ผ ์ƒˆ๋กœ์šด ์ชฝ๋ชจ์ด ์„ธ๊ณต ํŒŒํ‹ฐ์…˜์ด ๋Œ€์ƒ ์‡ผ์ผ€์ด์Šค ํด๋”์— ์ถ”๊ฐ€๋˜๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์ด ํด๋”์—๋Š” ๊ทธ๋‚ ์˜ ํŒŒ์‹ฑ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ์œ ํ˜•์ด ์ถฉ๋Œํ•˜๋Š” ํŒŒํ‹ฐ์…˜์ด ์—†๋Š”์ง€ ๋ฏธ๋ฆฌ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ์„ธ ๋ฒˆ์งธ ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ ์ผ๋ฐ˜ ์Šคํ‚ค๋งˆ๋Š” ์•Œ๋ ค์ง€์ง€ ์•Š์•˜์œผ๋ฉฐ, Hive์˜ ํ…Œ์ด๋ธ”์—๋Š” ์ž˜๋ชป๋œ ์Šคํ‚ค๋งˆ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ๊ฐ์˜ ์ƒˆ ํŒŒํ‹ฐ์…˜์ด ์Šคํ‚ค๋งˆ์— ์™œ๊ณก์„ ๋„์ž…ํ–ˆ์„ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

ํ…Œ์ด๋ธ”์„ ๋‹ค์‹œ ๋“ฑ๋กํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๊ฐ„๋‹จํ•˜๊ฒŒ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ƒ์  ์ฒซํ™”๋ฉด์˜ ๋งˆ๋ฃจ๋ฅผ ๋‹ค์‹œ ์ฝ๊ณ , ์Šคํ‚ค๋งˆ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ DDL์„ ์ƒ์„ฑํ•˜์—ฌ Hive์˜ ํด๋”๋ฅผ ์™ธ๋ถ€ ํ…Œ์ด๋ธ”๋กœ ๋‹ค์‹œ ๋“ฑ๋กํ•˜๊ณ  ๋Œ€์ƒ ์ƒ์  ์ฒซํ™”๋ฉด์˜ ์Šคํ‚ค๋งˆ๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.

๋„ค ๋ฒˆ์งธ ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ํ…Œ์ด๋ธ”์„ ์ฒ˜์Œ ๋“ฑ๋กํ•  ๋•Œ Spark์— ์˜์กดํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ ์šฐ๋ฆฌ๋Š” ์ง์ ‘ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋ฉฐ ์ชฝ๋ชจ์ด ์„ธ๊ณต ๋งˆ๋ฃจ ํ•„๋“œ๋Š” Hive์— ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ž๋กœ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์Œ์„ ๊ธฐ์–ตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด Spark๋Š” "corrupt_record" ํ•„๋“œ์—์„œ ๊ตฌ๋ฌธ ๋ถ„์„ํ•  ์ˆ˜ ์—†๋Š” ์ค„์„ ๋ฒ„๋ฆฝ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ํ•„๋“œ๋Š” ์ด์Šค์ผ€์ดํ”„ํ•˜์ง€ ์•Š๊ณ ๋Š” Hive์— ๋“ฑ๋กํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

์ด๊ฒƒ์„ ์•Œ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ณ„ํš์„ ์–ป์Šต๋‹ˆ๋‹ค.

f_def = ""
for f in pf.dtypes:
  if f[0] != "date_load":
    f_def = f_def + "," + f[0].replace("_corrupt_record", "`_corrupt_record`") + " " + f[1].replace(":", "`:").replace("<", "<`").replace(",", ",`").replace("array<`", "array<") 
table_define = "CREATE EXTERNAL TABLE jsonevolvtable (" + f_def[1:] + " ) "
table_define = table_define + "PARTITIONED BY (date_load string) STORED AS PARQUET LOCATION '/user/admin/testJson/testSchemaEvolution/pq/'"
hc.sql("drop table if exists jsonevolvtable")
hc.sql(table_define)

์•”ํ˜ธ ("_corrupt_record", "`_corrupt_record`") + " " + f[1].replace(":", "`:").replace("<", "<`").replace(",", ",`").replace("๋ฐฐ์—ด<`", "๋ฐฐ์—ด<") ๋‹ค์Œ ๋Œ€์‹  ์•ˆ์ „ํ•œ DDL์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

create table tname (_field1 string, 1field string)

"_field1, 1field"์™€ ๊ฐ™์€ ํ•„๋“œ ์ด๋ฆ„์„ ์‚ฌ์šฉํ•˜๋ฉด ํ•„๋“œ ์ด๋ฆ„์ด ์ด์Šค์ผ€์ดํ”„๋˜๋Š” ์•ˆ์ „ํ•œ DDL์ด ๋งŒ๋“ค์–ด์ง‘๋‹ˆ๋‹ค: create table `tname` (`_field1` ๋ฌธ์ž์—ด, `1field` ๋ฌธ์ž์—ด).

์งˆ๋ฌธ์ด ์ƒ๊น๋‹ˆ๋‹ค. ์™„์ „ํ•œ ์Šคํ‚ค๋งˆ(pf ์ฝ”๋“œ)๋กœ ๋ฐ์ดํ„ฐ ํ”„๋ ˆ์ž„์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐฉ๋ฒ•์€ ๋ฌด์—‡์ž…๋‹ˆ๊นŒ? ์ด pf๋ฅผ ์–ป๋Š” ๋ฐฉ๋ฒ•? ๋‹ค์„ฏ ๋ฒˆ์งธ ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค. ๋Œ€์ƒ ์‡ผ์ผ€์ด์Šค์˜ ์ชฝ๋ชจ์ด ์„ธ๊ณต ํŒŒ์ผ์ด ์žˆ๋Š” ํด๋”์—์„œ ๋ชจ๋“  ํŒŒํ‹ฐ์…˜ ๊ตฌ์„ฑํ‘œ๋ฅผ ๋‹ค์‹œ ์ฝ์œผ์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? ์ด ๋ฐฉ๋ฒ•์€ ๊ฐ€์žฅ ์•ˆ์ „ํ•˜์ง€๋งŒ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.

์Šคํ‚ค๋งˆ๋Š” ์ด๋ฏธ Hive์— ์žˆ์Šต๋‹ˆ๋‹ค. ์ „์ฒด ํ…Œ์ด๋ธ”์˜ ์Šคํ‚ค๋งˆ์™€ ์ƒˆ ํŒŒํ‹ฐ์…˜์„ ๊ฒฐํ•ฉํ•˜์—ฌ ์ƒˆ ์Šคํ‚ค๋งˆ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ Hive์—์„œ ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ๋ฅผ ๊ฐ€์ ธ์™€ ์ƒˆ ํŒŒํ‹ฐ์…˜์˜ ์Šคํ‚ค๋งˆ์™€ ๊ฒฐํ•ฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Hive์—์„œ ํ…Œ์ŠคํŠธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๊ณ  ์ž„์‹œ ํด๋”์— ์ €์žฅํ•œ ๋‹ค์Œ Spark๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•œ ๋ฒˆ์— ๋‘ ํŒŒํ‹ฐ์…˜์„ ์ฝ์œผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

์‹ค์ œ๋กœ Hive์˜ ์›๋ž˜ ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ์™€ ์ƒˆ ํŒŒํ‹ฐ์…˜ ๋“ฑ ํ•„์š”ํ•œ ๋ชจ๋“  ๊ฒƒ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์ƒ์  ์ฒซํ™”๋ฉด ์Šคํ‚ค๋งˆ์™€ ์ƒ์„ฑ๋œ ํŒŒํ‹ฐ์…˜์˜ ์ƒˆ ํ•„๋“œ๋ฅผ ๊ฒฐํ•ฉํ•˜๋Š” ์ƒˆ ์Šคํ‚ค๋งˆ๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

from pyspark.sql import HiveContext
from pyspark.sql.functions import lit
hc = HiveContext(spark)
df = spark.read.json("...", dropFieldIfAllNull=True)
df.write.mode("overwrite").parquet(".../date_load=12-12-2019")
pe = hc.sql("select * from jsonevolvtable limit 1")
pe.write.mode("overwrite").parquet(".../fakePartiton/")
pf = spark.read.option("mergeSchema", True).parquet(".../date_load=12-12-2019/*", ".../fakePartiton/*")

๋‹ค์Œ์œผ๋กœ ์ด์ „ ์Šค๋‹ˆํŽซ์—์„œ์™€ ๊ฐ™์ด ํ…Œ์ด๋ธ” ๋“ฑ๋ก DDL์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
์ „์ฒด ์ฒด์ธ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘๋™ํ•˜๋ฉด, ์ฆ‰ ์ดˆ๊ธฐํ™” ๋กœ๋“œ๊ฐ€ ์žˆ๊ณ  ํ…Œ์ด๋ธ”์ด Hive์—์„œ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ƒ์„ฑ๋˜๋ฉด ์—…๋ฐ์ดํŠธ๋œ ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ๋ฅผ ์–ป์Šต๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๋งˆ์ง€๋ง‰ ๋ฌธ์ œ๋Š” Hive ํ…Œ์ด๋ธ”์— ํŒŒํ‹ฐ์…˜์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์—†๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ํŒŒํ‹ฐ์…˜์ด ๊นจ์งˆ ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. Hive๊ฐ€ ํŒŒํ‹ฐ์…˜ ๊ตฌ์กฐ๋ฅผ ์ˆ˜์ •ํ•˜๋„๋ก ๊ฐ•์ œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

from pyspark.sql import HiveContext
hc = HiveContext(spark) 
hc.sql("MSCK REPAIR TABLE " + db + "." + destTable)

JSON์„ ์ฝ๊ณ  ์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ƒ์  ์ฒซ ํ™”๋ฉด์„ ๋งŒ๋“œ๋Š” ๊ฐ„๋‹จํ•œ ์ž‘์—…์œผ๋กœ ์—ฌ๋Ÿฌ ์•”๋ฌต์ ์ธ ์–ด๋ ค์›€์„ ๊ทน๋ณตํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด์— ๋Œ€ํ•œ ์†”๋ฃจ์…˜์€ ๋ณ„๋„๋กœ ์ฐพ์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์†”๋ฃจ์…˜์€ ๊ฐ„๋‹จํ•˜์ง€๋งŒ ์ฐพ๋Š” ๋ฐ ๋งŽ์€ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฝ๋‹ˆ๋‹ค.

์‡ผ์ผ€์ด์Šค ๊ตฌ์„ฑ์„ ๊ตฌํ˜„ํ•˜๋ ค๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ์‡ผ์ผ€์ด์Šค์— ํŒŒํ‹ฐ์…˜ ์ถ”๊ฐ€, ์„œ๋น„์Šค ํŒŒ์ผ ์ œ๊ฑฐ
  • Spark๊ฐ€ ์ž…๋ ฅํ•œ ์†Œ์Šค ๋ฐ์ดํ„ฐ์˜ ๋นˆ ํ•„๋“œ ์ฒ˜๋ฆฌ
  • ๋‹จ์ˆœ ์œ ํ˜•์„ ๋ฌธ์ž์—ด๋กœ ์บ์ŠคํŠธ
  • ํ•„๋“œ ์ด๋ฆ„์„ ์†Œ๋ฌธ์ž๋กœ ๋ณ€ํ™˜
  • Hive์—์„œ ๋ณ„๋„์˜ ๋ฐ์ดํ„ฐ ์—…๋กœ๋“œ ๋ฐ ํ…Œ์ด๋ธ” ๋“ฑ๋ก(DDL ์ƒ์„ฑ)
  • Hive์™€ ํ˜ธํ™˜๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋Š” ํ•„๋“œ ์ด๋ฆ„์„ ์ด์Šค์ผ€์ดํ”„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ์žŠ์ง€ ๋งˆ์‹ญ์‹œ์˜ค.
  • Hive์—์„œ ํ…Œ์ด๋ธ” ๋“ฑ๋ก์„ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฐฉ๋ฒ• ์•Œ์•„๋ณด๊ธฐ

์š”์•ฝํ•˜๋ฉด ์ƒ์  ์ฐฝ์„ ์ง“๊ธฐ๋กœ ํ•œ ๊ฒฐ์ •์—๋Š” ๋งŽ์€ ํ•จ์ •์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๊ตฌํ˜„์ด ์–ด๋ ค์šด ๊ฒฝ์šฐ ์„ฑ๊ณต์ ์ธ ์ „๋ฌธ ์ง€์‹์„ ๊ฐ–์ถ˜ ๊ฒฝํ—˜์ด ํ’๋ถ€ํ•œ ํŒŒํŠธ๋„ˆ์—๊ฒŒ ๋ฌธ์˜ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

์ด ๊ธฐ์‚ฌ๋ฅผ ์ฝ์–ด ์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ์œ ์šฉํ•œ ์ •๋ณด๊ฐ€ ๋˜์…จ๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค.

์ถœ์ฒ˜ : habr.com

์ฝ”๋ฉ˜ํŠธ๋ฅผ ์ถ”๊ฐ€