임상연구 적재 삽질기

June
9 min readJul 19, 2023

--

출처: https://panoply.io/data-warehouse-guide/3-ways-to-build-an-etl-process/

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

이번 편에서는 임상연구를 적재하며 얻은 경험을 공유하고자 합니다.

임상연구 수동 적재

우리는 임상 연구를 적재하는 서비스를 개발하고 있었습니다. 처음에는 크론 서버 없이 필요할 때마다 API를 호출하여 임상 연구를 적재하는 방식으로 작업을 수행했습니다.

그러나 추가 리소스를 투입하지 않고도 주기적으로 임상 연구를 적재하기 위해 크론 서비스를 도입하게 되었습니다.

cron 도입

cron을 도입하며 경험한 것들에 대해서 자세히 보고 싶으신 분들은 이전 글을 보고 오시면 됩니다.

우리는 django-crontab 을 도입하며 임상연구를 자동으로 적재하는 시스템을 구축했습니다.

많아진 data sources, edge cases

처음에는 국내 임상연구만 간단하게 가져오는 프로그램이었지만 이후, clinicaltrials.gov라는 곳에서 전세계 임상연구를 가져와 우리가 원하는 포맷으로 가공 후 전달해야하는 상황이 생겼습니다.

임상연구의 적재 단계는 다음과 같습니다.

임상연구 적재 과정
  1. clinicaltrials.gov에서 임상연구들을 크롤링해와 model의 original_data 컬럼에 저장합니다.
  2. 크롤링 해온 데이터를 extract해 model의 각 컬럼에 데이터를 넣어줍니다.
  3. extract된 데이터를 가져와 번역합니다.
  4. 서비스 할 수 있는 데이터인지 QA합니다.
  5. 임상연구가 적재 완료되어 서비스됩니다.

또, 임상연구를 업데이트하는 경우도 있었습니다.

  1. clinicaltrials.gov에서 임상연구들을 크롤링해옵니다.
  2. 크롤링해온 임상연구가 현재 가지고있는 데이터인지 확인합니다.
  3. 같은 임상연구 데이터가 있을 경우, original 임상연구 데이터를 복제하고, original_data 컬럼에 방금 크롤링해온 신규 데이터를 저장합니다.
  4. original_data를 바탕으로 extract를 진행합니다.
  5. 번역을 진행합니다.
  6. 서비스 할 수 있는 데이터인지 QA합니다.
  7. 기존 임상연구를 신규 임상연구로 교체합니다.

데이터 적재는 api개발과 다른 여러 어려움이 존재했습니다.

특히 포맷을 컨트롤할 수 없는 외부 데이터의 존재는 적재를 완료하기 전까지 안심할 수 없는 공포를 선사했습니다.

이렇게 컨트롤할 수 없는 데이터를 다루면서, 데이터에 따라 내부적으로 분기가 일어나면(예: 임상연구가 1상일 경우 A작업을, 임상연구 목적이 B일 경우 C작업을) 복잡도가 매우 증가하게 됩니다.

적재 중 오류가 발생할때마다 로컬에서 하나하나 다시 적재를 해보며 오류지점을 찾았고, 간신히 오류를 찾아 고쳐도 예상치 못한 사이드 이펙트로 다른곳에서 오류가 발생하게 되는 멘붕의 연속이었습니다.

우리는 이런 문제를 해결하기 위해 여러 개선방안들을 적용했습니다.

적재 단계 분리

우리는 각 작업 단계를 최대한 잘게 분리해 작업의 복잡도를 줄이고, 각 단계에 대한 트래킹이 되도록 했습니다.

(각 적재 작업이 길수록 도중 오류가 발생해서 각 단계의 실패 단계에 머물렀을 때 확인이 힘들어집니다. 따라서 각 단계를 최대한 잘게 쪼개는게 정신건강에 좋습니다.)

임상연구 model에 control_status_type이라는 컬럼을 추가해 해당 데이터가 어떤 단계에 머물러 있는지(예: 10/extract 해야함, 11/extract 실패함, 20/번역 해야함)를 파악했습니다.

동시성 제어

작업 단계를 분리하고 나니 여러 작업이 동시에 실행되며 데이터가 손상되는 이슈가 발생했습니다.

우리는 이 문제를 해결하기 위해 동시성 제어에 대해 알아보기 시작했습니다. 결과적으로 적용한 방법은 다음과 같습니다.

  1. 각 작업별로 transaction, lock을 적용해 적재 중 다른 작업에 의해 데이터가 변경되지 않도록 했습니다.
  2. 각 작업이 시작될 때 동일한 작업이 진행중인지 판별하는 로직을 넣어 같은 작업이 중복 실행됨을 방지했습니다.

Slack Webhook 적용

이전 글에서 적었던 것과 같이 slack webhook을 이용하여 cron 서버가 항상 정상작동 하고 있음을 확인했습니다.

TestCode 작성

우리는 적재 로직이 복잡하고, 데이터에 따라 edge cases가 많이 발생했기 때문에 testcode로 적재 코드의 안정성을 증명하고 적재로직 수정시의 사이드이펙트를 줄이려 시도했습니다.

따라서 적재로 로직과 api에서 Given-When-Then 패턴으로 모든 경우의 수를 구하고자 했습니다.

삽질의 흔적..(모든 경우의 수 구하기)

이러한 시도는 보기좋게 실패했으며, 이후 회고에서 첫 테스트코드 도입에 너무 완벽한 테스트를 도입하려 한 점이 오히려 부담으로 다가왔다는 점이 주요 실패 원인으로 꼽혔습니다.

이후 우리는 성공하는(개발자가 의도한) 명확한 경우의 수 하나를 테스트하는 코드를 먼저 작성하고, 이후 오류가 발생할 경우 해당 경우에 대해 테스트를 추가하는 방식으로 테스트 도입에 대한 부담감을 조금씩 줄여나갔습니다.

이러한 시도는 이후 다른 서비스를 제작하는 과정에서 e2e test, unit test를 성공적으로 도입하는 사례로 이어졌습니다.

너무 많은 번역비용 줄이기..

가장 처음 임상연구의 모든 데이터를 번역했을 때 첫 달 번역비용이 약 600만원이 넘게 나왔던 것으로 기억합니다.

임상연구를 처음 적재하다보니 적재되는 임상연구의 양이 많은것도 있었지만 서비스 규모에 비해 너무 많은 지출이 있었고, 번역된 데이터 중 실제 서비스에서는 서빙되지 않는 컬럼들도 있었습니다.

우리는 번역 비용을 줄이기 위해 가장 먼저 사용하지 않는 컬럼에 대해 번역을 진행하지 않기로 결정했습니다.

또, 번역 퀄리티가 중요하지 않은 데이터에 대해선 유료 번역 서비스가 아닌 무료 서비스 혹은 자체 자연어처리 기능을 이용하는 방법도 모색했습니다.

아래와 같이 FieldMixin을 만들어 mixin을 상속받은 컬럼에서 번역 관련 설정을 간편하게 할 수 있도록 개발했습니다.

class TranslateFieldMixin:
def __init__(self, translate_type: TranslateType = TranslateType.PAID_API, *args, **kwargs) -> None:
self.translate_type = translate_type
super().__init__(*args, **kwargs)

def translate(self, value, source_language, target_language):
if not value:
return None
if self.translate_type == TranslateType.PAID_API:
return # 유료 번역
elif self.translate_type == TranslateType.FREE_API:
return # 무료 번역

적재되는 데이터를 보면 꽤 많은 텍스트가 중복된다는 것을 확인할 수 있었습니다.

텍스트가 정해진 경우는 Enum을 이용해 텍스트로 저장하지 않도록 하여 해당 필드의 번역을 피했습니다.

아직 적용되지 않은 실험단계의 아이디어도 있습니다.

텍스트가 정해지지 않았지만 내용이 비슷한 경우는 word2vec과 같은 word embedding을 이용해 각 문장의 유사도를 분석하고, 이미 저장된 번역 텍스트를 재사용하는 아이디어를 실험중입니다.

개선할 점

임상연구 적재 기능을 개발하며 고민도 많이하고, 삽질도 많이 한 만큼 개선할 점과 아쉬운점들이 많이 남습니다.

지금은 우선순위가 아래로 내려가 있지만 다시 개선할 때가 오거나, 개인적으로 시간이 남을 때 조금씩 개선해나갈 예정입니다.

외부에서 사용하는 id값과 내부적으로 사용하는(foreign key등 사용) id값의 분리

django의 orm은 id값에 많은 의존성을 가지고 있습니다.

예를들어 데이터를 생성할지, 업데이트할지 결정할때 id값을 기준으로 판단하고, 각 row의 equal 연산에 id값을 이용합니다.

이 id값이 django의 orm과 api를 통해 임상연구 데이터를 제공받는 다른 서비스에 활용되어 임상연구의 업데이트 과정에서 id값을 그대로 유지하기 위해 많은 노력을 기울였습니다.

만약 다시 개발하게 된다면 외부에 보여지는 id와 내부적으로 활용하는 id를 분리하는 방안을 검토할 것입니다.

Over Engineering

위에서 서술한대로 적재 과정에서 많은 고통을 받기도 했고, 테스트코드 도입 시도에서 나타난것처럼 처음부터 완벽하게 하려고 하다보니 완벽한 코드 아키텍처를 도입하기 위해 많은 고민과, 오랜 기간의 대공사가 있었습니다.

(약 반년간의 개발 기간이 있었습니다.)

스스로는 많이 성장했지만, 회고해봤을때 서비스의 입장에서 제공하려는 서비스에 비해 너무 많은 고민을 들였다는 생각이 들었습니다.

이후 블로그 글을 작성하며 거의 같은 기능을 가진 임상연구 적재 기능을 개발하니 약 일주일 안에 개발할 수 있었습니다.

Logging

api와 달리 제어할 수 없는 외부 데이터에 의존하고, 너무 많은 경우의 수를 가진 적재 로직에서 Logging은 정말 많이 고민하고 챙길수록 여러모로 좋다는걸 깨달았습니다.

오류가 발생할때나 외부 데이터가 알게모르게 조금 변경되었을 때, 새로운 데이터를 추출해내는 등의 기능 추가가 이루어 질 때 로그는 우리에게 많은 정보를 제공해줍니다.

우리는 로그를 보고 삽질 시간을 획기적으로 줄일 수 있습니다.

지금은 각 적재 단계를 나눈것 이외에 적재 과정을 트래킹할 수 있는 수단이 마땅히 존재하지 않아 아쉬움으로 남습니다.

번역비용 줄이기

위에서 설명한 대로 문장의 유사도를 이용한 번역 재활용, transformer model을 이용한 임상연구 번역 모델 개발 등의 주로 자연어 처리 관련되어 개선할 수 있는 여지가 많이 남아 있습니다.

적재 성능 최적화

이 부분은 위에서 서술한 Logging부분이 먼저 채워지고 진행하면 좋을 것이라 생각하고 있습니다.

현재 적재 로직은 데이터 스크래핑, 번역 부분에서 병목 현상이 발생하고 있어 이 부분에서 개선 여지가 있습니다.

특히 데이터 스크래핑 부분은 굳이 스크래핑을 하지 않고, 데이터 제공 서비스에서 제공하는 압축파일을 다운받아 바로 extract하는식으로 개발하면 매우 빨라질것으로 기대하고 있습니다.

지금까지 임상연구를 적재하며 겪었던 이슈, 개선 과정, 더 개선할점에 대해 공유드렸습니다. 감사합니다.

--

--