티스토리 뷰

Github Repo

https://github.com/wanted-wecode-subjects/humanscape-subject

개요

  • 임상실험 데이터 수집 API 구현

기간

  • 2021.11.15 - 2021.11.17

개발 환경

  • TypeScript
  • NestJS
  • Sqlite3

사용 라이브러리

  • typeorm
  • class-validator
  • class-transformer
  • moment-timezone
  • date-fns
  • xml2json-light
  • schedule
  • axios

구현 사항

요구사항

  • 임상정보 수집 batch task
  • 특정 임상정보 읽기
  • 최근 일주일내에 업데이트 된 임상정보 리스트 조회
    • pagination

DB 모델링

임상정보 리스트 조회 API

https://pocky-humanscape-subject.herokuapp.com/clinical

axios

nodejs에는 fetch가 없습니다.
따라서 이에 대한 대책으로 node-fetch 또는 axios 패키지를 사용합니다.
nestjs에서는 @nestjs/axios를 제공합니다.
해당 패키지의 HttpModule을 사용하려는 모듈에 import 하여 HttpService를 사용할 수 있습니다.

clinical.module.ts

@Module({
  imports: [
    TypeOrmModule.forFeature([ClinicalRepository]),
    HttpModule,
    StepModule,
  ],
  controllers: [ClinicalController],
  providers: [ClinicalService],
})
export class ClinicalModule {}

clinical.service.ts

export class ClinicalService {
  constructor(
    @InjectRepository(ClinicalRepository)
    private readonly clinicalRepository: ClinicalRepository,
    private httpService: HttpService,
    private stepService: StepService,
  ) {}

get

HttpService.get() 메소드를 사용하여 get 요청을 할 수 있습니다.

clinical.service.ts

  getAPIData(pageNo, start = 0) {
    let url =
      'http://apis.data.go.kr/1470000/MdcinClincTestInfoService/getMdcinClincTestInfoList';
    url += '?' + `ServiceKey=${process.env.SERVICE_KEY}`;
    url += '&' + `numOfRows=${CLINICAL_CONSTANT.NUM_OF_ROWS}`;
    url += '&' + `pageNo=${pageNo}`;

    return this.httpService.get(url);

그러나 axios의 get 메소드는 rxjs를 사용하여 이벤트 발생에 따른 동작을 수행합니다. 그래서 Observable 객체에 래핑된 AxiosResponse를 반환합니다.
반환받은 값을 통해 이후 동작을 정의하는 데에는 다음과 같은 방법이 있습니다.

  1. 반환된 값을 받아서 subscirbe() 메소드를 사용하여 이후 기능을 구현
const observer = this.getAPIData(pageNo);
observer.subscribe({
  next(resposne) {
    ... // response 가공 후 DB에 저장
  },
  error(err) {
    ... // error 관련 처리
  },
  complete() {
    ... // 완료 후 처리
  }
});
  1. get()의 반환값에 pipe() 메소드를 사용하여 함수형 프로그래밍으로 이후 기능을 구현
  ...
  const observer = this.httpService.get(url).pipe(map(axiosResponse) => axiosResponse);
  const doSomething = () => {
    const jsonResponse = xml2json.xml2json(axiosResponse.data);
    const items = jsonResponse.response.body.items.item; // 임상 실험 데이터 배열

    if (!items) {
      return;
    }

    return items;
  }
  // api의 임상정보 데이터 배열(items)이 res에 담김
  const res = await lastValueFrom(observer).then(doSomething);
  ... // DB에 clinical 저장
  1. get()의 반환값에 toPromise() 메소드를 사용하여 promise 객체로 바꾼뒤, 이후 기능을 구현
  async getAPIData(pageNo, start = 0): Promise<Clinical[]> {
    let url =
      'http://apis.data.go.kr/1470000/MdcinClincTestInfoService/getMdcinClincTestInfoList';
    url += '?' + `ServiceKey=${process.env.SERVICE_KEY}`;
    url += '&' + `numOfRows=${CLINICAL_CONSTANT.NUM_OF_ROWS}`;
    url += '&' + `pageNo=${pageNo}`;

    return this.httpService
      .get(url)
      .toPromise()
      .then(async (axiosResponse) => {
        const jsonResponse = xml2json.xml2json(axiosResponse.data);
        const items = jsonResponse.response.body.items.item; // 임상 실험 데이터 배열

        if (!items) {
          return;
        }

        // DB에 insert
        for (let i = start; i < items.length; i++) {
          await this.createClinical(items[i]);
        }

        return items;
      });
  }

이외에도 다양한 방법이 있겠지만, 과제 진행 당시에는 조급한 마음에 다른 선택지가 보이지 않고 세번째 방법을 선택하여 코드를 작성하였습니다.
(참고로 세번째 방법의 toPromise()는 deprecate 될 예정입니다)

 

참고: https://min9nim.vercel.app/2020-04-24-rxjs/
https://ichi.pro/ko/rxjs-7ui-saeloun-gineung-177144942872256
https://docs.nestjs.kr/techniques/http-module

xml2json-light

참고한 openAPI 데이터는 반환형이 XML로 고정되어 있기 때문에 이를 json으로 바꿀 필요가 있었습니다.
때문에 이와 관련된 라이브러리인 xml2json-light를 사용하였습니다.
xml2json-light의 xml2json() 함수를 사용하면 XML 문자열이 JSON 객체로 변환됩니다.

import * as xml2json from 'xml2json-light';

const xml = `<APPLY_ENTP_NAME>한국아스트라제네카</APPLY_ENTP_NAME>
<APPROVAL_TIME>2012-03-22 00:00:00</APPROVAL_TIME>
<LAB_NAME>학교법인 고려중앙학원 고려대학교의과대학부속병원:서울대학교병원:분당서울대학교병원:연세대학교의과대학 강남세브란스병원:전북대학교병원:연세대학교의과대학 강남세브란스병원</LAB_NAME>
<GOODS_NAME>AZD8931</GOODS_NAME>
<CLINIC_EXAM_TITLE>HER2 상태에 따른 트라스투주맙 치료에 적합하지 않고, 일차요법에서 진행 된 전이성, 위암 또는 위-식도 접합부 암환자에서 파클리탁셀 단독 투여 대비 파클리탁셀과 병용한 AZD8931의 유효성, 안전성 및 약물동력학 평가를 위한 제 2a상 다기관 무작위배정 이중맹검 위약대조 시험 (SAGE)</CLINIC_EXAM_TITLE>
<CLINIC_STEP_NAME>2a상</CLINIC_STEP_NAME>`;
const json = xml2json.xml2json(xml); 
console.log(json);

console

{
  APPLY_ENTP_NAME: ‘한국아스트라제네카’,
  APPROVAL_TIME: ‘2012-03-22 00:00:00’,
  LAB_NAME: ‘학교법인 고려중앙학원 고려대학교의과대학부속병원:서울대학교병원:분당서울대학교병원:연세대학교의과대학 강남세브란스병원:전북대학교병원:연세대학교의과대학 강남세브란스병원’,
  GOODS_NAME: ‘AZD8931’,
  CLINIC_EXAM_TITLE: ‘HER2 상태에 따른 트라스투주맙 치료에 적합하지 않고, 일차요법에서 진행 된 전이성, 위암 또는 위-식도 접합부 암환자에서 파클리탁셀 단독 투여 대비 파클리탁셀과 병용한 AZD8931의 유효성, 안전성 및 약물동력학 평가를 위한 제 2a상 다기관 무작위배정 이중맹검 위약대조 시험 (SAGE)’,
  CLINIC_STEP_NAME: ‘2a상’
}

batch

nestjs에서는 Node.js node-cron 패키지와 통합되는 @nestjs/schedule 패키지를 제공합니다.

@cron

cron은 linux의 cron 처럼 특정 시간마다 정의한 메소드를 호출할 수 있도록 도와주는 데코레이터입니다.
데코레이터에 전달되는 값은 초 분 시 일 월 요일 순이며, *을 입력하면 매번 실행, 범위를 지정하고 싶다면(예: 1분~30분마다 실행) 1-30 과 같이 작성할 수 있습니다. 또한 /를 사용하여 주기적으로 실행할 수 있습니다.

예시

  • * * * * * *: 매 초 실행
  • 45 * * * * *: 매분 45초에 실행
  • 0 10 * * * *: 매시간 10분 0초에 실행
  • 0 */30 9-17 * * *: 오전 9시부터 오후 5시까지 30분마다 실행
  • 0 30 11 * * 1-5: 월요일(1)부터 금요일(5) 오전 11시 30분에 실행

이번 프로젝트에서는 월~토요일 자정 정각에 batch를 돌릴 수 있도록 cron을 설정하였습니다.

clinical.service.ts

  @Cron('0 0 0 * * 1-6')
  async batchData(): Promise<void> {
    ...

참고: https://docs.nestjs.kr/techniques/task-scheduling

회고

Fact(사실)

  • xml2json이 일부 피씨에서 설치되지 않는 이슈가 있었다.

Feeling(느낀점)

  • 맥북에어 m1에서도 작동하고 윈도우에서도 작동하는데 맥미니 m1과 일부 윈도우에서 설치가 되지 않는 것이 의아했다.

Finding(교훈)

  • npm 패키지도 모든 피씨에서 동일하게 작동하지 않는다는 것을 깨달았다.

Future action(향후 계획)

  • xml2json이 아닌 xml2json-light로 패키지를 바꿈

Feedback(피드백)

Comments