시스템을 건드리기 전에 레거시 시스템을 먼저 읽어야 하는 이유: 헬스케어 현대화 사례
요약
헬스케어 시스템 현대화 과정에서 레거시 시스템의 동작 방식을 완벽히 이해하는 것이 왜 중요한지 설명합니다. 단순한 재작성(Rewrite)이나 벤더 교체가 실패한 사례를 통해, 기존 시스템이 처리하던 예외 케이스와 조건부 로직을 파악하는 것이 성공적인 현대화의 핵심임을 강조합니다.
핵심 포인트
- 레거시 시스템의 조건부 로직은 단순한 오류가 아닌 축적된 비즈니스 예외 케이스임
- 새로운 시스템 구축 시 기존 시스템의 동작 기준점(Behavioral baseline) 확보가 필수적임
- 단순 재작성이나 API 교체는 기존 시스템의 암묵적 지식을 반영하지 못해 실패할 수 있음
- 시스템 현대화 시 기술적 깔끔함보다 비즈니스 로직의 연속성이 중요함
모든 것의 시작이 된 전화 한 통
고객사의 CTO는 구체적인 문제를 언급하며 통화를 시작했습니다. 그들의 처방 처리 시스템(prescription processing system)이 실제 입력값의 약 40%에서 실패하고 있었고, 이 오류들은 수동 검토 대기열(manual review queue)로 넘어가고 있었는데, 이 대기열이 이미 핵심 인프라 역할을 하고 있는 상태였습니다. 원래 시스템을 구축했던 개발자는 18개월 전에 퇴사했습니다. 모바일 앱 통합(mobile app integration)은 90일 이내에 완료되어야 했습니다.
우리는 다른 어떤 말을 하기 전에 코드베이스(codebase)를 먼저 확인하겠다고 요청했습니다.
우리가 발견한 것은 2019년 당시 합리적이었던 상용 OCR 라이브러리(commercial OCR library)를 기반으로 구축된 시스템이었으며, 그 주변은 문서화되지 않은 채 수년간 축적된 조건부 로직(conditional logic)들로 둘러싸여 있었습니다. 각 추가 사항은 당시에는 타당했습니다. 손글씨 약어에 대한 수정, 특정 클리닉의 처방 형식에 대한 임시 방편(workaround), 신뢰도가 낮은 스캔에 대한 재시도 루프(retry loop) 등이 그것이었습니다. 이들이 모여 현재의 어떤 팀원도 전체를 온전히 파악할 수 없는 무언가를 만들어냈습니다.
40%의 실패율은 원래 시스템이 잘못 설계되었다는 신호가 아니었습니다. 그것은 시스템이 원래의 설계 가정(design assumptions)으로부터 너무 멀리 벗어나 교체가 필요한 상태라는 신호였습니다.
비즈니스 영향은 명확했습니다. 운영 직원 한 명이 수동 대기열을 처리하는 일 외에는 아무것도 하지 못하고 있었습니다. 조건부 로직에 대한 어떠한 변경도 문서화 없이는 너무 위험했기 때문에 새로운 기능 구현이 차단되었습니다. 시스템 유지보수 비용이 처음부터 다르게 구축했을 비용보다 더 많이 들고 있었습니다.
이전의 두 차례 시도가 실패했던 이유
McKinsey 연구에 따르면 기업들은 새로운 시스템을 구축하기보다 기존 시스템을 유지보수하는 데 IT 예산의 60~80%를 소비한다는 사실이 지속적으로 발견됩니다. 고객사는 그 굴레에서 벗어나기 위해 두 번의 시도를 했었습니다.
첫 번째 시도는 처음부터 다시 만드는 재작성 (Rewrite) 이었습니다. 한 계약업체가 4개월에 걸쳐 새로운 OCR 시스템을 구축했습니다. 기술적으로는 더 깔끔했지만, 비즈니스가 실제로 받는 입력값에 대해 기존 레거시 시스템 (Legacy system)이 보여주던 동작과 일치하지 않았습니다. 레거시 시스템이 해결했던 약품 이름들이 누락되었습니다. 레거시 시스템이 조용히 처리해 왔던 환자 식별의 예외 케이스 (Edge cases)들이 문제를 일으켰습니다. 계약업체는 비교할 수 있는 동작 기준점 (Behavioral baseline)이 없었기에, 사용자들이 오류를 보고하기 전까지는 무엇이 누락되었는지 알 방법이 없었습니다. 결국 재작성 작업은 중단되었습니다.
두 번째 시도는 벤더 교체 (Vendor replacement), 즉 기성품 형태의 헬스케어 OCR API를 도입하는 것이었습니다. 깨끗한 입력값에 대한 정확도는 레거시 시스템보다 높았지만, 해당 API는 구조화된 데이터 (Structured data)가 아닌 가공되지 않은 추출 텍스트 (Raw extracted text)를 반환했습니다. 이 출력값을 기존 데이터베이스 스키마 (Database schema)에 통합하려면 다운스트림 애플리케이션 (Downstream application)의 대부분을 다시 구축해야 했습니다. 통합 비용이 현대화 예산을 초과했습니다.
두 시도 모두 동일한 근본 원인을 공유하고 있었습니다. 팀이 시스템이 무엇을 하고 있는지 완전히 이해하기 전에 시스템을 교체하려고 시도했다는 점입니다.
Gartner의 추정에 따르면, 현대화 프로젝트의 70%가 이러한 이유로 실패합니다. 교체하려는 기술이 잘못되어서가 아니라, 교체를 시작하기 전에 시스템의 실제 동작에 대한 신뢰할 수 있는 지도 (Map)를 팀이 가지고 있지 않았기 때문입니다.
1단계: 코드 분석이 아닌 동작 기준점 (Behavioral Baseline) 확보
우리가 가장 먼저 한 일은 운영 시스템 (Production system)에 로깅 (Logging)을 추가하는 것이었습니다.
2주 동안 우리는 약 2,000건의 실제 처방전 제출 건을 캡처했습니다. 입력 이미지, 트리거된 모든 조건부 분기 (Conditional branch), 해결되었거나 실패한 모든 약품 이름, 환자 식별 결과, 그리고 최종 데이터베이스 쓰기 (Database write) 또는 큐 할당 (Queue assignment)까지 포함되었습니다. 이를 통해 우리는 동작 기준점 (Behavioral baseline)을 확보할 수 있었습니다. 즉, 시스템이 마주치는 모든 형식의 범위에 걸쳐 실제 운영 입력값에 대해 실제로 무엇을 수행했는지에 대한 기록을 얻은 것입니다.
이 골든 세트(Golden set)는 전체 마이그레이션(Migration)을 위한 행동 계약(Behavioral contract)이 되었습니다. 우리가 구축한 모든 교체 컴포넌트(Replacement component)는 실제 운영 트래픽(Production traffic)을 처리하기 전에, 이 세트에 대해 레거시 시스템의 출력값과 일치하거나 이를 능가해야 했습니다.
그 후 우리는 LLM 지원 분석(LLM-assisted analysis)을 사용하여 코드베이스(Codebase)를 읽고 매핑했습니다. 모든 조건부 분기(Conditional branch), 모든 폴백 경로(Fallback path), 모든 하드코딩된 값(Hardcoded value)을 포함하여 말이죠. 그 결과물은 각 컴포넌트가 무엇을 하는지를 설명하는 평이한 언어로 된 지도였습니다.
여기서 우리는 해당 분석이 무엇을 만들어냈는지에 대해 정확히 짚고 넘어가고자 합니다. 왜냐하면 프로젝트가 AI 도구에 과도하게 의존할 때 문제가 발생하는 지점이 바로 여기이기 때문입니다:
AI는 코드가 무엇을 하는지는 설명할 수 있습니다. 하지만 왜 특정 규칙이 추가되었는지는 설명할 수 없습니다.
다음 예시를 살펴보십시오:
if customer_id == 1234:
skip_verification()
LLM 분석은 이것이 특정 고객 ID에 대한 검증 우회(Verification bypass)임을 정확히 식별합니다. 하지만 그 고객이 테스트 계정인지, 계약상 예외 조항이 있는 오래된 기업 고객인지, 삭제되지 않은 개발자의 지름길(Shortcut)인지, 아니면 특정 규제 상황에 따른 컴플라이언스(Compliance) 우회책인지는 알려줄 수 없습니다. 그 답은 코드 안에 있는 것이 아니라 누군가의 기억, Slack 스레드, 또는 계약서 안에 존재합니다.
우리는 고객의 시스템에서 이와 같은 규칙을 여러 개 발견했습니다. 각 규칙에 대해 우리는 남은 팀원들에게 문의해야 했고, 어떤 경우에는 원래의 의도를 이해하기 위해 이메일 기록을 뒤져야 했습니다. 어떤 규칙들은 여전히 유효했습니다. 두 가지 규칙은 시스템의 다른 부분에서 변경된 사항에 의해 대체되었기에 안전하게 제거할 수 있었습니다. 하나는 교체 시스템에서도 정확히 재현되어야 하는 고객별 예외 사항을 반영하고 있었습니다.
LLM 분석은 발견(Discovery) 프로세스를 몇 주에서 며칠로 단축했습니다. 하지만 각 규칙이 왜 존재했는지, 그리고 여전히 필요한지 확인하는 이해관계자 검증(Stakeholder validation) 작업은 단축될 수 없었습니다. 그 부분은 조직 지식(Institutional knowledge)을 가진 인간이 필요했으며, 어떤 도구로도 이를 대체할 수 없었습니다.
결합된 분석을 통해 얻은 세 가지 발견 사항이 마이그레이션 계획을 형성했습니다:
발견 사항 1: 40%의 대기열 비율은 무작위가 아니었습니다. 이는 세 가지 입력 유형—수기 처방전, 하드코딩된 약물 목록에 없는 지역 브랜드 약물, 그리고 조명이 불량한 사진—주변에 집중되어 있었습니다. 시스템의 나머지 부분은 입력을 올바르게 처리했습니다. 대부분은 교체할 필요가 없었습니다.
발견 사항 2: 약물 이름 조회(medicine name lookup)가 가장 영향력이 큰 실패 지점이었습니다. 정적 목록(static list)은 수동으로 유지 관리되어 왔으며, 지역 제네릭(generics) 및 최신 브랜드 분자(branded molecules)에 대한 커버리지 공백이 있었습니다. 수동 대기열에 도달한 모든 실패는 목록이 인식하지 못하는 이름으로 추적되었습니다.
발견 사항 3: 환자 신원 확인(patient identity resolution) 로직은 정확했습니다. 이 로직은 다인 가구, 모호한 이름 일치, 확인 흐름(confirmation flows)을 정확하게 처리했습니다. 이를 교체하는 것은 상응하는 이득 없이 위험만 초래할 것입니다.
교체 전략: 무엇이 이동하고, 무엇이 남고, 무엇이 변했는가
현대화 사례 연구에서 종종 간과되는 사실에 대해 우리는 솔직해질 필요가 있습니다: 복잡성을 제거하는 것이 아니라, 이동시키는 것입니다.
기존 시스템은 애플리케이션 코드 내의 조건부 로직(conditional logic)을 통해 예외 케이스(edge cases)를 처리했습니다. 새로운 시스템은 동일한 예외 케이스를—어딘가에는 반드시 존재해야 하므로—비전 LLM(Vision LLM) 프롬프트 설계, RAG 파이프라인 임계값 튜닝(RAG pipeline threshold tuning), 그리고 에이전트 오케스트레이션(agent orchestration) 로직을 통해 처리합니다. 복잡성은 여전히 존재합니다. 변한 것은 그것이 어디에 위치하며 얼마나 유지보수 가능한가 하는 점입니다.
기존 시스템에서 복잡성은 문서화되지 않은 조건부 분기(conditional branches)에 존재했습니다. 무언가 고장 나면, 이를 진단하는 유일한 방법은 코드를 읽고 어떤 분기가 트리거되었는지 추측하는 것뿐이었습니다. 새로운 예외 케이스가 나타나면 개발자는 또 다른 분기를 추가했습니다. 변경 사항이 다른 부분을 망가뜨렸는지 테스트할 수 있는 체계적인 방법은 없었습니다.
새로운 시스템에서 복잡성은 독립적으로 버전 관리(versioning), 테스트, 모니터링이 가능한 컴포넌트 내에 존재합니다. 프롬프트(prompt) 변경 사항은 검토가 가능합니다. RAG 임계값(threshold) 변경은 배포 전에 골든 세트(golden set)를 통해 검증될 수 있습니다. 에이전트(agent)의 도구 호출(tool-call) 실패는 재현 가능한 구조화된 로그 엔트리(structured log entry)를 생성합니다. 진단 프로세스는 문서화되지 않은 코드를 읽는 것에서 구조화된 로그를 쿼리(query)하는 것으로 변화합니다.
이것이 실제 가치 제안(value proposition)입니다. 즉, "복잡성의 감소"가 아니라 "다룰 수 있는 형태의 복잡성"입니다.
마이그레이션 자체는 네 단계로 진행되었으며, 각 단계는 독립적으로 테스트 가능하고 각기 고유한 롤백(rollback) 경로를 가졌습니다.
1단계: OCR 엔진을 Vision LLM으로 교체
상용 OCR 라이브러리는 문서 구조에 대한 이해 없이 가공되지 않은 문자열(raw character strings)만을 출력했습니다. Vision LLM(저희는 Google Gemini를 사용했습니다)은 필기체, 약어, 가변적인 이미지 품질을 처리하면서 단 한 번의 패스(single pass)로 구조화된 필드(medicine name, patient name, dosage, frequency, clinic)를 추출합니다.
이 과정에서 이동된 것: 가공되지 않은 OCR 출력을 정규화하기 위해 존재했던 조건부 로직(abbreviations 처리, 결합된 항목 분리, 공백 제거 등)이 추출 프롬프트(extraction prompt)와 검증 레이어(validation layer)의 일부가 되었습니다. 분기(branching)는 여전히 존재하지만, 애플리케이션 코드 대신 프롬프트 설계와 구조화된 출력 스키마(structured output schema) 안에 존재하게 됩니다.
운영 환경(production)에 배포하기 전, 저희는 2,000개의 항목으로 구성된 전체 골든 세트(golden set)에 대해 Vision LLM을 실행하고 그 출력을 기존 OCR이 생성했던 결과와 비교했습니다. 기존 시스템이 올바르게 처리했던 입력값에 대해서는 회귀(regression)가 전혀 없었습니다. 반면 기존 시스템이 실패했던 입력값에 대해서는 상당한 개선이 이루어졌습니다.
롤백 경로: 기존 OCR 엔진을 4주 동안 병렬로 실행했습니다. Vision LLM의 신뢰도 점수(confidence score)가 보정된 임계값(calibrated threshold)보다 낮은 처방전은 자동으로 기존 OCR 경로로 재라우팅(rerouted)되었습니다.
2단계: 정적 약물 목록을 4단계 RAG 파이프라인으로 교체
정적 목록은 자동으로 확장될 수 없습니다. 새로운 파이프라인은 네 가지 순차적 단계로 구성됩니다: 빈도가 높은 약물을 위한 인메모리 캐시 (in-memory cache), 정확한 데이터베이스 매칭 (exact database match), OCR 노이즈 및 철자 변형을 위한 트리그램 퍼지 검색 (trigram fuzzy search), 그리고 음성적 변형 및 부분 일치를 위한 벡터 의미론적 검색 (vector semantic search). 어떤 단계도 통과하지 못한 의약품은 자동 스크래핑 (automated scraping)을 위해 대기열에 추가된 후 데이터베이스에 등록됩니다.
이 과정을 통해 변화된 점은, 어떤 약물을 정적 목록에 추가할지 선택함으로써 암묵적으로 내려졌던 커버리지 결정 사항들이 RAG 파이프라인 내에서 명시적으로 변했다는 것입니다. 즉, 임계값 조정 (threshold tuning), 신뢰도 컷오프 (confidence cutoffs), 퍼지 매칭 (fuzzy matching)을 위한 "충분히 유사함"의 정의 등이 포함됩니다. 이러한 결정 사항들은 이제 9개월 전 누군가가 마지막으로 업데이트한 스프레드시트에 고정되어 있는 것이 아니라, 코드 내에 문서화되어 있으며 데이터에 따라 조정 가능합니다.
임계값 조정 (threshold tuning)을 위해서는 골든 세트 (golden set)를 대상으로 상당한 캘리브레이션 (calibration)이 필요했습니다. 퍼지 매칭 (fuzzy matching)이 너무 관대하면 잘못된 약물과 매칭되는 거짓 양성 (false positives)이 발생합니다. 반대로 너무 엄격하면 대기열 발생률이 높아집니다. 우리는 새로운 파이프라인이 깨끗한 입력값에 대해서는 기존 목록의 해상도 (resolution rate)와 일치하면서도, 이전에는 수동 대기열로 넘어갔던 노이즈가 섞인 입력값에 대해서는 기존보다 뛰어난 성능을 보일 때까지 여러 임계값 조합을 반복하며 테스트했습니다.
롤백 경로 (Rollback path): 새로운 파이프라인이 신뢰도 임계값 (confidence threshold) 미만으로 반환하는 모든 이름에 대해서는 기존 의약품 목록이 폴백 (fallback)으로 유지되었습니다.
3단계: 환자 식별 로직을 교체하지 말고, 래핑(wrap)하세요.
행동 분석 결과, 이 컴포넌트가 올바르다는 것이 확인되었습니다. 우리는 이를 추출하여 골든 세트의 모든 입력 클래스에 대한 출력을 문서화하는 행동 테스트 스위트 (behavioral test suite)를 작성하였고, 깔끔한 API 인터페이스 뒤로 이를 래핑 (wrap)했습니다. 새 시스템은 기존 시스템과 동일한 방식으로 이를 호출했습니다.
이것은 프로젝트에서 가장 레버리지가 높은 단일 결정이었습니다. 더 깔끔하게 만들기 위해 작동 중인 컴포넌트를 교체하는 것은 현대화 프로젝트에서 회귀 (regression)가 발생하는 가장 흔한 원인 중 하나입니다. 만약 동작이 올바르고 변경 시 영향 범위 (blast radius)가 크다면, 그것을 래핑하고 다음 단계로 넘어가십시오.
Phase 4: 누적된 조건부 로직을 ReAct 에이전트로 교체합니다.
레거시 시스템은 하드코딩된 폴백 (fallback)과 구조화된 로깅 (logging) 없이 선형 파이프라인을 통해 처방전을 처리했습니다. 운영 환경에서의 장애를 디버깅하려면 코드를 읽고 어떤 분기 (branch)가 트리거되었는지 추적해야 했는데, 각 분기가 문서화되지 않은 방식으로 상호작용했기 때문에 이는 매우 어려운 작업이었습니다.
ReAct 에이전트는 완전한 도구 호출 로깅 (tool-call logging)과 함께 추출 및 매칭 단계를 오케스트레이션 (orchestration) 합니다. 모든 도구 호출은 입력값 및 출력값과 함께 기록됩니다. 어떤 운영 장애라도 해당 로그를 통해 정확하게 재현할 수 있습니다. 오케스트레이션의 복잡성은 여전히 존재합니다. 에이전트는 어떤 도구를 어떤 순서로 호출할지에 대해 결정을 내려야 하기 때문입니다. 하지만 이러한 결정들은 가시적이며 테스트 가능합니다.
트래픽 마이그레이션: 6주간의 병렬 운영
우리는 특정 날짜에 새 시스템으로 한꺼번에 전환하지 않았습니다. 모든 단계에서 자동 폴백 (fallback)을 적용하며 6주에 걸쳐 점진적으로 트래픽을 마이그레이션했습니다.
1~2주 차: 트래픽의 5%. 나머지 트래픽은 기존 시스템이 처리했습니다. 두 시스템 간의 출력 불일치 (discrepancy) 사항은 매일 기록되고 검토되었습니다. 가장 흔한 불일치는 약품명 형식 문제였습니다. 레거시 시스템이 가공되지 않은 OCR 출력을 반환한 반면, 새 시스템은 정형화된 이름 (canonical names)을 반환했습니다. 이것은 오류는 아니었지만, 이러한 변화가 퇴보 (regression)가 아닌 개선임을 확인하기 위해 다운스트림 (downstream) 검증이 필요했습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기