우편번호. 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 |