3주차 때는 2주차의 복용 일정 등록 및 관리 기능에 이어서 약올림의 또다른 핵심 서비스인 복약 정보 제공 및 관리 기능을 구현했다.
이 기능은 구현 이전부터 '약 상세 정보를 제공해주는 곳이 있나, 있으면 어디인가?' , '이 데이터를 우리 DB에 저장 할 것이냐 아니면 매번 OpenAPI로 요청할 것이냐', '어떤 정보를 제공해 줄 것인가?', 'OpenAPI를 요청하는 곳을 Client에서 할 것인가 Server에서 할 것인가?' 등에 대한 팀원간 논의가 활발하게 이루어진 부분이기도 하다.
이번에 구현했던 기능들은 기술적인 이슈나 원리를 아는 것 보다 '사례와 사용법'을 아는 것이 핵심이다. 그만큼 공식문서보다는 나보다 먼저 공공데이터, OpenAPI, xml정보의 json화를 고민했던 사람들이 기록으로 남겨둔 블로그 및 관련 기사를 더 많이 보았고, 도움이 많이 되었다.
얼굴도 모르는 분들이 열심히 작성해주신 글들의 도움을 받으면서 내 글도 누군가에게 이런 존재가 되었으면 좋겠다는 마음으로 더 꼼꼼하고 열심히 블로그를 작성해야겠다고 생각했다. 누군가 그런 말을 한 적이 있다고 한다. '개발자들은 신기해 자신이 그렇게 고생고생해서 얻은 해답, 코드를 왜 블로그에 통으로 다 올리는거지? 그걸 왜 다 공개하는거지?'
그런데 나는 이런게 개발자 문화의 아름다움이라고 생각한다. 개인만 알고 그걸 꽁꽁 숨기면 거기서 지식의 흐름은 멈추게 된다.
이미 길이 나온 것을 공유하지 않아서 누군가 그걸 알기 위해 또 써야하는 시간을, 정보를 자유롭게 접할 수 있게 공유함으로써 그 시간을 기존의 것을 더 발전시키거나 뒤이은 다른 문제를 해결하는데 쓰는게 더 좋지 않은가?
이런 문화, 생각을 페어 프로그래밍과 팀원 분들 (+스택오버플로우)에게서 배웠고 프로젝트를 하면서 연습했으며 깃헙을 관리하고 블로그를 정리하면서 실천하고 있다.
그리고 앞으로도 그런 문화에 푹 빠져있는 개발자로 성장하고 싶다.
1. '약 상세 정보를 제공해주는 곳이 있나, 있으면 어디인가?'
약올림 서비스에서 (직접 등록한 약이 아닌)이미지 인식을 통해 등록한 약의 경우 공공데이터에서 제공하는 상세 정보를 받아볼 수 있도록 구현하고자 하였다. 이 부분을 담당해주시기로 한 HI님께서 해당 데이터를 어디서 얻을 수 있을지 질문을 주셔서 생각보다 빨리 찾아보게 되었다.
(앞서 약 이미지 데이터를 얻기 위해 사용한 의약품안전나라에서 제공하는 공공데이터개방_낱알식별목록 csv 파일에는 약에 대한 복용법, 효능효법, 유효기간 등 상세 정보는 나와있지 않았기 때문이다. )
※ 아 그리고 이 프로젝트 기획을 제안드릴 때 부터 약 상세정보에 해당하는 공공데이터 제공이 반드시 있을것이라 생각했다. 왜냐하면
<식약처, '의약품 정보 제공 프로그램 개발' 지원한다…효능효과 등 '의약품 정보 약 12만건' 제공>이라는 뉴스도 보았고, <약학정보원>에 올라온 질문과 답변에서 '식약처의 공개 API 서비스를 활용해 보아라, 의약품 정보는 누구에게나 공개되어 있다.' 등의 답변을 보았기 때문이다.
그렇게 찾아보다가 <의약품안전나라>에서 <의약품 제품허가 상세정보> 중 <의약품 제품허가 상세정보조회>를 위한 open API를 제공한다는 것을 알아냈다.
여기에서 우리가 사용할 것은 9페이지의 < (2) [의약품 제품 허가 상세정보조회] 오퍼레이션 명세 > 이다.
활용신청을 하고,
여기서
이 요청주소를 엔드포인트로,
로그인 후 <의약품 제품 허가 정보 서비스>에 들어가면 내 일반인증키(UTF-8)가 있다.
이것과
여기서 요청변수(Request Parameter)에 해당하는 것들을 queryParams에 넣어서 요청하면
출력결과(Response Element)에 해당하는 것들을 출력해준다.
'필'이라고 되어있지만
아래 코드에서도 알 수 있듯이 주석처리 해줬을 때도 데이터가 잘 받아와졌다.
2. '이 데이터를 우리 DB에 저장 할 것이냐 아니면 매번 OpenAPI로 요청할 것이냐'
Medicines라는 DB테이블에 직접 입력한 약에 대한 정보와 카메라 인식을 통한 약 정보를 모두 관리하고 있기 때문에 카메라 인식을 통한 약 정보를 저장도 가능하긴 했다. 그래서 '이 데이터를 우리 DB에 저장 할 것이냐 아니면 매번 OpenAPI로 요청할 것이냐' 라는 논의가 나왔다.
이 부분은 HI님께서 OpenAPI 관리 측면에 대해서 잘 알아보시고 '알아보니 하루에 한 번 변경사항이 있는 약들의 업데이트가 진행되기 때문에 사용자에게 최신의 정보를 제공하기 위해선 매번 요청하는 것이 바람직 할 것 같다'는 의견을 주셨다.
해당 질문에 대해서 UX적인 측면을 고려하시고, 정확히 알아보신 후 논리로 의견을 내주셔서 깔끔하게 해결이 되었는데, HI님은 이런 부분에서 배울 점이 참 많은 분인 것 같다.
3. 'OpenAPI를 요청하는 곳을 Client에서 할 것인가 Server에서 할 것인가?' , '어떤 정보를 어떻게 제공해 줄 것인가?',
상세 정보를 OpenAI로 요청할 때는 공공데이터에서 제공하는 endpoint로 약의 이름을 parameter로 요청하는 것이기 때문에 client에서도 충분히 요청을 보낼 수 있다. 그럼에도 Server에서 요청을 처리하기로 한 것은 모바일 앱이라는 특성상 Client의 코드를 최대한 가볍게 하려는 의도도 있고, 한 알약에 대한 정보를 Server에서 한번에 모아서 json형태로 만든 후 전달하면 Client에서는 그냥 랜더링만 시켜주도록 하는게 효율적이라고 생각했기 때문이다.
어떤 정보를 어떤 형태로 응답해주는지는 제공되는 API문서를 꼼꼼히 읽어보는 것이 중요했다.
이 API문서에서 <1) [의약품 제품 허가 상세정보조회] 오퍼레이션 명세> 을 보면 <요청 메시지 명세>에 요청 parameter정보가 있고, <응답 메시지 명세>에 응답되는 것들이 매우 자세히 나와있다. 이 응답들 중에서 validity(약의 유효기간), capacity(용법/용량), effect(효과/효능)을 제공하고자 했다.
이제 응답 xml을 json화 시키는 것이 중요한데, 이게 정말 힘들었다. xml형식이 트리 자료구조처럼 계층적으로 되어있었기 때문이다.
HI님과 함께 고민하면서 처음에는 단순히
medicines = xmlobj.findAll('item')
find_list = ["ITEM_NAME", "EE_DOC_DATA","UD_DOC_DATA", "VALID_TERM"]
for i in find_list:
for result in xmlobj.find_all(i):
if i == "ITEM_NAME":
print("약품명: ", result.text)
elif i == "EE_DOC_DATA":
for data in result.find_all("PARAGRAPH"):
elif i == "UD_DOC_DATA":
for data in result.find_all("PARAGRAPH"):
print("용법용량: ", data.text)
elif i == "VALID_TERM":
print("유효기간: ", result.text)
이렇게 태그 기준으로만 뽑아주면 될 것이라고 생각했다. 그런데 이러면 해당 태그 안의 태그 들의 내용(text)는 뽑아주지 못하였다.
이 방법으로는 시작을 잘못 잡은 듯 하여 API문서에서 응답을 최대 어디까지 계층적으로 줄 수 있는지 파악 한 뒤, etree를 사용하여 그 패턴에 맞게 xml태그를 한꺼풀 한꺼풀 탐색하도록 해주었다. 그리고 가끔 text에 ' ' 이런 문자가 있는 등, 한 두개의 예외들은 모두 검사하여 따로 처리해주었다. 기능 구현이 아니라 알고리즘 완전탐색 문제를 푸는 것 같았다.😂
그렇게 완성되니 코드는 <더보기>를 클릭!
def get_open_api_info(name):
my_api_Key = unquote(MY_API_KEY)
url = OPEN_API_URI
queryParams = '?' + urlencode({ quote_plus('ServiceKey') : my_api_Key, quote_plus('item_name') : name })
request = Request(url + queryParams)
request.get_method = lambda: 'GET'
response_body = urlopen(request).read()
tree = etree.fromstring(response_body)
json_string = json.dumps(xmltodict.parse(response_body))
load_json_string = json.loads(json_string)
open_api_data = load_json_string["response"]["body"]["items"]["item"]
group_data = dict()
group_data["ITEM_NAME"] = open_api_data["ITEM_NAME"]#약이름
group_data["VALID_TERM"] = open_api_data["VALID_TERM"]#유효기간
group_data["EE_DOC_DATA"] = open_api_data["EE_DOC_DATA"]["DOC"]["SECTION"]["ARTICLE"]#효능효과
group_data["UD_DOC_DATA"] = open_api_data["UD_DOC_DATA"]["DOC"]["SECTION"]["ARTICLE"]#용법용량
results = {}
field_lists = ["ITEM_NAME", "VALID_TERM", "EE_DOC_DATA", "UD_DOC_DATA"]
for field in field_lists:
if field == "EE_DOC_DATA":
effect = []
if type(group_data["EE_DOC_DATA"])==list:
for i in range(len(group_data["EE_DOC_DATA"])):
effect.append(group_data["EE_DOC_DATA"][i]['@title'])
try:
if type(group_data["EE_DOC_DATA"][i]['PARAGRAPH'])==list:
for y in group_data["EE_DOC_DATA"][i]['PARAGRAPH']:
value_ = list(y.values())
if value_[0] == 'p' and value_[3]!=' ':
del value_[0:3]
effect.append(''.join(value_))
else:
effect.append(group_data["EE_DOC_DATA"][i]['PARAGRAPH']['#text'])
except KeyError:
if type(group_data['EE_DOC_DATA'][i])==list:
for y in group_data["EE_DOC_DATA"][i]['PARAGRAPH']:
value_ = list(y.values())
if value_[0] == 'p' and value_[3]!=' ':
del value_[0:3]
effect.append(''.join(value_))
else:
if type(group_data["EE_DOC_DATA"]['PARAGRAPH'])==list:
for y in group_data['EE_DOC_DATA']['PARAGRAPH']:
value_ = list(y.values())
if value_[0] == 'p' and value_[3]!=' ':
del value_[0:3]
effect.append(''.join(value_))
else:
effect.append(group_data['EE_DOC_DATA']['PARAGRAPH']['#text'])
results['effect'] = effect
elif field == "UD_DOC_DATA":
capacity = []
if type(group_data["UD_DOC_DATA"])==list:
for i in range(len(group_data["UD_DOC_DATA"])):
capacity.append(group_data["UD_DOC_DATA"][i]['@title'])
try:
if type(group_data["UD_DOC_DATA"][i]['PARAGRAPH'])==list:
for y in group_data["UD_DOC_DATA"][i]['PARAGRAPH']:
value_ = list(y.values())
if value_[0] == 'p' and value_[3]!=' ':
del value_[0:3]
capacity.append(''.join(value_))
else:
capacity.append(group_data["UD_DOC_DATA"][i]['PARAGRAPH']['#text'])
except KeyError:
if type(group_data['UD_DOC_DATA'][i])==list:
for y in group_data["UD_DOC_DATA"][i]['PARAGRAPH']:
value_ = list(y.values())
if value_[0] == 'p' and value_[3]!=' ':
del value_[0:3]
capacity.append(''.join(value_))
else:
if type(group_data["UD_DOC_DATA"]['PARAGRAPH'])==list:
for y in group_data['UD_DOC_DATA']['PARAGRAPH']:
value_ = list(y.values())
if value_[0] == 'p' and value_[3]!=' ':
del value_[0:3]
capacity.append(''.join(value_))
else:
capacity.append(group_data['UD_DOC_DATA']['PARAGRAPH']['#text'])
results['capacity'] = capacity
elif field == "ITEM_NAME":
results['name'] = group_data["ITEM_NAME"]
else:
results['validity'] = group_data["VALID_TERM"]
return results
여기까지 약통 페이지, 약 상세 페이지 기능 구현이 마무리 되었다.
이번에 구현했던 기능들은 기술적인 이슈나 원리를 아는 것 보다 '사례와 사용법'을 아는 것이 핵심이다. 그만큼 공식문서보다는 나보다 먼저 공공데이터, OpenAPI, xml정보의 json화를 고민했던 사람들이 기록으로 남겨둔 블로그 및 관련 기사를 더 많이 보았고, 도움이 많이 되었다.
얼굴도 모르는 분들이 열심히 작성해주신 글들의 도움을 받으면서 내 글도 누군가에게 이런 존재가 되었으면 좋겠다는 마음으로 더 꼼꼼하고 열심히 블로그를 작성해야겠다고 생각했다. 누군가 그런 말을 한 적이 있다고 한다. '개발자들은 신기해 자신이 그렇게 고생고생해서 얻은 해답, 코드를 왜 블로그에 통으로 다 올리는거지? 그걸 왜 다 공개하는거지?'
그런데 나는 이런게 개발자 문화의 아름다움이라고 생각한다. 개인만 알고 그걸 꽁꽁 숨기면 거기서 지식의 흐름은 멈추게 된다.
이미 길이 나온 것을 공유하지 않아서 누군가 그걸 알기 위해 또 써야하는 시간을, 정보를 자유롭게 접할 수 있게 공유함으로써 그 시간을 기존의 것을 더 발전시키거나 뒤이은 다른 문제를 해결하는데 쓰는게 더 좋지 않은가?
이런 문화, 생각을 페어 프로그래밍과 팀원 분들 (+스택오버플로우)에게서 배웠고 프로젝트를 하면서 연습했으며 깃헙을 관리하고 블로그를 정리하면서 실천하고 있다.
그리고 앞으로도 그런 문화에 푹 빠져있는 개발자로 성장하고 싶다.
'Project > [약올림] Final Project' 카테고리의 다른 글
💊 약올림 모바일 앱 서비스 README ⏰ (2) | 2021.01.22 |
---|---|
[Milestone Week 4(마지막 마무리)] Push Notification/개인정보 관리/로그아웃 (2) | 2021.01.22 |
[Milestone Week 2] 알람 일정 CRUD 기능 구현 (0) | 2021.01.22 |
[Milestone Week 2] 알약 등록 기능을 위한 알약 이미지 인식 준비~배포 (17) | 2021.01.22 |
[DeepLearning] 스마트폰으로 촬영된 알약 이미지 인식 알고리즘 개발을 위한 Reference 정리 (0) | 2021.01.22 |