티스토리 뷰
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를 반환합니다.
반환받은 값을 통해 이후 동작을 정의하는 데에는 다음과 같은 방법이 있습니다.
- 반환된 값을 받아서 subscirbe() 메소드를 사용하여 이후 기능을 구현
const observer = this.getAPIData(pageNo);
observer.subscribe({
next(resposne) {
... // response 가공 후 DB에 저장
},
error(err) {
... // error 관련 처리
},
complete() {
... // 완료 후 처리
}
});
- 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 저장
- 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로 패키지를 바꿈