처음부터 시작하는 Django 데이터 적재(2)

June
None
Published in
13 min readFeb 15, 2023

안녕하세요. 휴먼스케이프 june입니다.

이번시간엔 django crontab을 이용한 주기적인 적재방법, 적재 도중 api서버가 오류를 뱉을때의 핸들링 방법에 대해 알아보도록 하겠습니다.

프로젝트를 진행한 코드는 깃허브에 올라가 있으니 참고하시기 바랍니다.

적재 진행 상태 확인

지난시간에 작성한 코드를 실행해보면, 직접 db에 들어가 보지 않는이상 적재가 얼마나 진행되었는지 알기 어렵습니다.

tqdm이라는 패키지를 사용해 터미널에서 작업 진행률을 확인해봅시다.

poetry add tqdm

tqdm은 주로 반복문에 함수를 씌워 자동으로 progress bar를 만드는데, save_all_studies는 반복문이 2중으로 들어가기 때문에 manual한 방식으로 적용합니다.

모든 임상연구 적재가 완료된 뒤엔 value를 다시 1로 업데이트해 이후 실행 시 처음부터 적재할 수 있도록 합니다.

def save_all_studies():
"""
clinicaltrials.gov 에서 제공하는 API 에서 전체 임상 연구 목록을 저장하는 메소드
"""
studies_num = get_studies_num()
with tqdm(total=studies_num) as progress_bar:
for start in range(1, studies_num, 100):
end = start + 99
studies = get_studies(start, end)
for study in studies:
study = convert_study(study)
study_serializer = StudySerializer(data=study)
try:
study_serializer.is_valid(raise_exception=True)
except ValidationError as e:
if e.detail.get('nct_id', None) is not None and e.detail['nct_id'][0].code == 'unique':
continue
raise e
finally:
progress_bar.update(1)
study_serializer.save()
ConfigurationVariable.objects.filter(name='loaded_studies_num').update(value=1)

실행해보면 아래와 같이 progress_bar가 생긴걸 볼 수 있습니다.

0%|▍                                | 1099/442054 [00:29<2:30:16, 48.91it/s]

Django Command 적용

이제 꽤 그럴듯한 적재 코드를 작성했는데, 실행하기 위해선 python3 manage.py shelldjango shell에 접속해 직접 함수를 실행해야한다는 불편함이 있습니다.

shell을 실행하지 않고 적재 코드를 실행할 수 있는 방법을 고민해보니 2가지 방법이 생각났습니다.

  1. api로 작성: runserver실행 후 해당 api에 접근하면 적재 코드 실행, 적재가 오래걸릴 경우 timeout 이슈가 있음
  2. command로 작성: django command로 만들어 실행, timeout이슈는 없지만 beanstalk과 같은 일부 클라우드 서비스에서 api를 이용한 cronjob만 지원해 이후 일부 클라우드 서비스에서 사용이 힘들 수 있음

저는 runserver 실행이 귀찮음, timeout 이슈, 이후 beanstalk 서비스 사용 계획이 없음을 이유로 command로 작성하기로 했습니다.

위의 문서를 따라하면 쉽게 command를 추가할 수 있습니다.

management/commands 디렉토리를 추가합니다

생성하려는 command name(save_studies.py)로 file을 생성합니다.

아래와 같이 command class를 작성해줍니다.

from django.core.management.base import BaseCommand
from studies.batch_tasks import save_all_studies
class Command(BaseCommand):
help = 'clinicaltrials.gov의 전체 임상연구 적재'

def handle(self, *args, **options):
save_all_studies()

이제 python3 manage.py save_studies를 실행하면 임상연구가 성공적으로 적재되는걸 확인할 수 있습니다.

중간단계 저장

command까지 만들었으니, 신나게 적재를 시도해봅니다.

적재 중 누군가 pc방의 전원을 내려(pc방 전원을 내려보겠습니다)컴퓨터가 꺼졌고, 개발자는 멘탈이 나가게됩니다.

위와 같은 불행을 겪지 않기 위해 적재 중 중간단계 저장 코드를 추가해봅시다.

studies는 rank 1부터 차례로 적재하니 적재한 study의 rank를 db에 저장하는 방식으로 중간 단계 저장을 구현합니다.

studies에 ConfigurationVariable model을 작성합니다.

class ConfigurationVariable(models.Model):
name = models.CharField(max_length=100, unique=True, verbose_name="변수명")
value = models.CharField(max_length=100, verbose_name="값")

save_all_studies 함수를 수정합니다.

def save_all_studies():
"""
clinicaltrials.gov 에서 제공하는 API 에서 전체 임상 연구 목록을 저장하는 메소드
"""
studies_num = get_studies_num()
loaded_studies_num = int(ConfigurationVariable.objects.get_or_create(name='loaded_studies_num', defaults={'value': 1})[0].value)
with tqdm(total=studies_num, initial=loaded_studies_num) as progress_bar:
for start in range(loaded_studies_num, studies_num, 100):
end = start + 99
studies = get_studies(start, end)
for study in studies:
study = convert_study(study)
study_serializer = StudySerializer(data=study)
try:
study_serializer.is_valid(raise_exception=True)
except ValidationError as e:
if e.detail.get('nct_id', None) is not None and e.detail['nct_id'][0].code == 'unique':
continue
raise e
finally:
progress_bar.update(1)
ConfigurationVariable.objects.filter(name='loaded_studies_num').update(value=progress_bar.n)
study_serializer.save()

Exception Handling

데이터 적재를 하다보면 여러가지 이유로 오류가 발생하게 되는데, 이번엔 clinicaltrials.gov에서 일시적으로 오류를 뱉을 때의 처리를 간단하게 진행해보겠습니다.

Http status code를 보고 어떤 오류인지 판별해 핸들링하면 크게 어렵지 않습니다.

200~300 번대의 status code는 넘어가고 예외, 오류를 뜻하는 400~500대 오류를 주로 보도록 하겠습니다.

  • 401, 403에러는 권한 관련 에러로, 너무 많은 request를 날릴 경우 일시적으로 차단될 수 있습니다. 따라서 해당 에러는 잠시(약 10분) 쉰 뒤 이어서 적재하도록 핸들링합니다.(계속해서 에러가 날 경우 무한정 기다릴 수 있으니 약 100분간 에러를 반환하면 즉시 적재를 멈추고 오류를 뱉어 개발자가 알 수 있도록 핸들링합니다.)
  • 404에러는 페이지가 존재하지 않을 경우 발생하는 에러인데, 사이트의 구조가 변경될 경우 발생할 수 있습니다. 이 경우 즉시 적재를 멈추고 오류를 뱉어 개발자가 알 수 있도록 핸들링합니다.
  • 500번대 에러는 주로 대상 서버에서의 에러로, 대상 서버에서 빠르게 수정될 확률이 높으니 잠시(약 10분) 쉰 뒤 이어서 적재하도록 핸들링합니다.(계속해서 에러가 날 경우 무한정 기다릴 수 있으니 약 100분간 에러를 반환하면 즉시 적재를 멈추고 오류를 뱉어 개발자가 알 수 있도록 핸들링합니다.)
def get_studies(start, end, sleep_count = 0):
"""
clinicaltrials.gov 에서 제공하는 API 에서 임상 연구 목록을 가져오는 메소드
"""
if sleep_count >= 10:
raise ValidationError('clinicaltrials.gov API 요청 제한')

CLINICALTRIALS_OPEN_API_BASE_URL = 'https://clinicaltrials.gov/api/query/full_studies'
response = requests.get(CLINICALTRIALS_OPEN_API_BASE_URL, params={'fmt': 'json', 'min_rnk': start, 'max_rnk': end})

if response.status_code == 404:
raise ValidationError('clinicaltrials.gov API 응답 없음')

if response.status_code == 401 or response.status_code == 403:
sleep(600)
get_studies(start, end, sleep_count+1)

if response.status_code//100 == 5:
sleep(60)
get_studies(start, end, sleep_count+1)

response_json = response.json()
if response_json['FullStudiesResponse']['NStudiesFound'] == 0:
return []
return response_json['FullStudiesResponse']['FullStudies']

Django crontab 적용

지금까지 잘 만든 적재코드가 주기적으로 돌아가도록 django crontab을 적용해봅시다.

django crontab은 내부적으로 linux crontab을 사용합니다.

따라서 django crontab이 동작하는 컴퓨터는 cron service가 동작하는 상태야야합니다.

출처: https://www.armourinfosec.com/linux-privilege-escalation-by-exploiting-cronjobs/

cron 표현식은 처음 요소부터 분, 시간, 일, 월, 요일을 지정합니다.

*로 되어있는 부분은 검사하지 않고 실행합니다.

*      *      *      *      *
분(0-59)  시간(0-23)  일(1-31)  월(1-12)   요일(0-7)
  • 15 * * * * 인 cronjob은 매 시 15분에 실행됩니다.
  • 0 3 * * * 인 cronjob은 매일 3시 00분에 실행됩니다.

이제 django-crontab을 적용해봅시다.

poetry add django-crontab

settings의 INSTALLED_APPS에 django_crontab을 추가해줍니다.

사용예제를 보면 다음과 같습니다.

CRONJOBS = [
('*/5 * * * *', 'myapp.cron.other_scheduled_job', ['arg1', 'arg2'], {'verbose': 0}),
('0 4 * * *', 'django.core.management.call_command', ['clearsessions']),
]

django내의 특정 함수를 실행시킬수도, command를 실행할수도 있습니다.

여기서는 미리 만들어둔 command를 실행시키도록 설정합니다.

CRONJOBS = [
('0 0 * * *', 'django.core.management.call_command', ['save_studies']),
]

매일 00시 00분에 임상연구가 적재되도록 설정했습니다.

이제 python3 manage.py crontab add를 하면 linux crontab에 cronjob이 추가됩니다.

crontab -l 명령어로 cronjob list를 볼 수 있습니다.

0 * * * * /python_path/bin/python /django_practice/manage.py crontab run 7db776a4dd9d0be25664f3261a744e51 # django-cronjobs for django_practice

다음 시간엔 적재한 임상연구를 한글로 번역하는 법에 대해 알아보도록 하겠습니다.

--

--