본문 바로가기
기술정리/Django

Django 효율적인 대량 데이터 마이그레이션 방법

by bingual 2024. 4. 5.
반응형

우편번호. txt

# 우편번호.txt

우편번호|시도|시도영문|시군구|시군구영문|읍면|읍면영문|도로명코드|도로명|도로명영문|지하여부|건물번호본번|건물번호부번|건물관리번호|다량배달처명|시군구용건물명|법정동코드|법정동명|리명|행정동명|산여부|지번본번|읍면동일련번호|지번부번|구우편번호|우편번호일련번호
06303|서울특별시|Seoul|강남구|Gangnam-gu|||116804166052|개포로17길|Gaepo-ro 17-gil|0|9|3|1168010300112450005000001||G-heim|1168010300|개포동||개포4동|0|1245|01|5||
06309|서울특별시|Seoul|강남구|Gangnam-gu|||116804166065|개포로36길|Gaepo-ro 36-gil|0|6|0|1168010300112100000000001||H&S개포|1168010300|개포동||개포4동|0|1210|01|0||
06315|서울특별시|Seoul|강남구|Gangnam-gu|||116804166204|논현로8길|Nonhyeon-ro 8-gil|0|67|0|1168010300111830010000001||Artespace|1168010300|개포동||개포4동|0|1183|01|10||
06312|서울특별시|Seoul|강남구|Gangnam-gu|||116804166128|논현로12길|Nonhyeon-ro 12-gil|0|9|6|1168010300111960009000001||Classe|1168010300|개포동||개포4동|0|1196|01|9||
06314|서울특별시|Seoul|강남구|Gangnam-gu|||116804166190|논현로6길|Nonhyeon-ro 6-gil|0|30|6|1168010300111740003000001||DAONVILL|1168010300|개포동||개포4동|0|1174|01|3||

...

 


해당 형식과 같이 주어진 txt 파일이 있고 이 데이터들을 데이터 베이스에 삽입하여 관리하려 한다.

만약 이러한 데이터들이 10만 개... 100만 개... 가 넘어간다면 데이터 베이스에 한꺼번에 삽입하려 할 때 성능 문제가 있을 것이다.

그렇다면 최대한 제너레이트 함수를 이용하여 코드를 개선할 필요가 있다.


제너레이트 함수란? 대용량 처리에 유용한 기법으로 미리 데이터들을 메모리에 할당하는 리스트 같은 이터러블 객체와 달리 필요에 의할 때 메모리를 할당하고 사용할 수 있는 함수다.

 

제너레이터 동작과정 예시

gen = (elem for elem in [1, 2, 3, 4, 5])  # 제너레이터 표현식

print(next(gen))  # 1
print(next(gen))  # 2
print(next(gen))  # 3

print(len(list(gen)))  # 2
print(len(list(gen)))  # 0

 

 

 

models.py

class ZipCode(models.Model):
    code = models.CharField(max_length=6)
    name = models.CharField(max_length=100)

 

 

 

0002_insert_zipcode_data.py

# Generated by Django 4.2.7 on 2024-04-05 22:08

import csv
import itertools
from typing import Dict, Iterator, Tuple

from django.db import migrations


CSV_PATH = "shop/assets/zipcode_db/20231205/서울특별시.txt"


def get_code_and_name_from_csv(zipcode_csv_path: str) -> Iterator[Tuple[str, str]]:
    with open(zipcode_csv_path, "rt", encoding="utf-8-sig") as csvfile:
        csv_reader = csv.DictReader(csvfile, delimiter="|")
        row: Dict
        for row in csv_reader:
            code = row["우편번호"]
            name = "{시도} {시군구} {도로명}".format(**row)
            yield code, name


def get_chunks(iterable: Iterator, chunk_size: int = 100) -> Iterator:
    iterator = iterable if hasattr(iterable, "__next__") else iter(iterable)
    for first in iterator:
        yield itertools.chain([first], itertools.islice(iterator, chunk_size - 1))


def add_zipcode_data(apps, schema_editor):
    ZipCode = apps.get_model("shop", "ZipCode")
    zipcode_list = (
        ZipCode(code=code, name=name)
        for code, name in get_code_and_name_from_csv(CSV_PATH)
    )
    for chunks in get_chunks(zipcode_list, chunk_size=1000):
        ZipCode.objects.bulk_create(chunks)


def remove_zipcode_data(apps, schema_editor):
    ZipCode = apps.get_model("shop", "ZipCode")
    ZipCode.objects.all().delete()


class Migration(migrations.Migration):
    dependencies = [
        ("shop", "0001_initial"),
    ]

    operations = [
        migrations.RunPython(add_zipcode_data, remove_zipcode_data),
    ]

 


get_code_and_name_from_csv

 

csv 파일을 읽어 들이는 제너레이터 함수이다.

 

  • 텍스트 읽기 모드로 csv 파일을 연다. 이때 인코딩은 호환성을 위해 "utf-8-sig"를 통해 바이트 오브 마크를 추가해 준다.
  • csv파일은 | 구분자를 기준으로 컬럼을 나누고 있다. DictReader을 사용하면 각 행의 데이터가 열 이름을 키로 가지는 딕셔너리로 변환되어 편리하게 데이터를 처리할 수 있다.
  • 원하는 방식으로 포맷을 한 뒤 함수를 제너레이터로 정의한다.

 

get_chunks

 

반복가능한 객체를 일정한 크기로 나누어 반환하는 제너레이터 함수이다.

 

  • hasattr() 메서드를 사용하여 첫 번째 매개변수가 이터레이터인지 확인하고 그렇지 않다면 이터러블을 이터레이터로 변환한다.
    이터러블과 이터레이터의 차이점은 __next__ 메서드의 존재 유무이다. 예시로 for문은 내부적으로 이터러블을 이터레이터로 변환하고 연속된 요소를 __next__ 메서드를 통해 가져온다. 즉 이터러블을 이터레이터로 변환 하는 과정은 제너레이터를 사용하기 위함이다.
  • 청크는 첫 번째 요소를 추출해 내기 전엔 크기를 알 수 없기에 첫 번째 요소를 추출하여 해당 부분을 시작점으로 설정하고 나머지 요소들을 이어 연결하여 추출한다.
  • 청크란? 연속적인 데이터의 일부를 뜻한다.

 

add_zipcode_data

 

위에 정의한 함수들을 이용해 실질적으로 데이터를 데이터 베이스에 삽입하는 역할을 하는 함수이다.

 

  • get_code_and_name_from_csv를 이용해 반환된 튜플을 제너레이터 표현식을 이용하여 zipcode_list를 선언한다.
  • Zipcode 모델 클래스로 정의된 zipcode_list로  get_chunks에 인자로 전달하고 지정한 사이즈만큼의 청크를 추출한다.
  • bulk_create를 이용해 추출한 청크를 데이터 베이스에 삽입한다. bluk_create는 내부적으로 첫 번째 매개변수를 list로 변환하는 과정을 수행하기 때문에 별도로 리스트로 변환할 필요는 없다.

 

remove_zipcode_data

 

역방향으로 마이그레이트를 수행할 때 데이터 베이스의 모든 요소를 지우는 역할을 하는 함수이다.

 

 

 

마이그레이트 수행

def add_zipcode_data(apps, schema_editor):
    ZipCode = apps.get_model("shop", "ZipCode")
    zipcode_list = (
        ZipCode(code=code, name=name)
        for code, name in get_code_and_name_from_csv(CSV_PATH)
    )
    for chunks in get_chunks(zipcode_list, chunk_size=1000):
        print("chunk size:", len(list(chunks))) # 디버깅용 추가
        ZipCode.objects.bulk_create(chunks)

 

 

마이그레이트시 데이터 베이스에 값을 집어 넣기 위해서는 위에서 선언한 디버깅을 위한 코드를 제거해주어야 한다. 반복문을 이용하여 제너레이터를 호출할 때 그 값들을 디버깅 블록에서 전부 소진하기 때문에 bulk_create에는 항상 빈 값이 들어가게 된다.

 

이렇게 제너레이터를 선언하여 사용하게 되면 각 함수에서 제너레이트 함수를 호출할 때마다 필요한 만큼 메모리에 할당하고 즉시 처리한다.

 

만약 제너레이터를 사용하지 않고 대용량 데이터를 처리해야 한다면 함수가 정의될 때 모든 데이터를 하나의 객체에 담아서 처리하기 때문에 메모리 효율성이 극히 낮아지기에 적합하지 않다.

'기술정리 > Django' 카테고리의 다른 글

Django slug 필드를 활용하여 검색하기  (0) 2024.04.06
Django 안정적인 마이그레이션 방법  (0) 2024.04.06
Django Instagram Project 2  (0) 2022.11.20
Django Instagram Project 1  (0) 2022.11.14
Django 장식자  (0) 2022.10.10