Post

AI가 통계를 바꾼 순간 - 프롬프트 엔지니어링부터 비동기 전환까지

들어가며

병원 통계 기능은 데이터와 그래프를 함께 보여주어 사용자에게 의미 있는 정보를 전달하려는 목적을 가진다. 하지만 처음 이 기능을 접하는 사람에게는 여전히 복잡하고 어렵게 느껴질 때가 있었다.
“AI를 활용해 통계를 더 쉽게 알려주면 어떨까?” 그래프를 직접 해석하기보다 AI가 자연어로 설명해 준다면 누구나 빠르게 이해할 수 있을 거라 생각했다. 이 글은 그 아이디어를 실제 서비스에 녹이는 과정에서 겪은 시행착오와 배운 점을 정리한 기록이다.

1. 프롬프트 엔지니어링으로 통계 읽는 눈을 만들다

여러 프롬프트를 비교하며 어떤 형태가 가장 유용한지 검증했다. 세그먼트 분석, 리스크 탐지, 스토리라인 분석, 자유 탐색 등 다양한 접근을 시도했지만, “집계 수치나 그래프에서 한눈에 보이지 않는 인사이트를 찾아달라”고 요청했을 때 가장 가치 있는 응답을 얻을 수 있었다.

단순히 “이번 주는 지난주보다 환자가 늘었다” 같은 1차원 요약보다, 전주 대비·상하위 5일 비교·요일 효과처럼 사람이 놓치기 쉬운 비교 관점을 제시했을 때 AI의 강점이 선명해졌다.

아래는 어떤 핵심 지표를 조합해 인사이트를 뽑을지 정리한 도식이다.

그래서 목표를 두 가지로 잡았다.
첫째, 그래프/표를 대신 읽어주는 설명자. 통계 페이지를 처음 보는 사람도 AI 설명만으로 전체 맥락을 이해할 수 있도록 프롬프트를 설계했다.
둘째, 분석의 킥. 기초 설명이 안정화된 뒤에는 이상치 탐지와 비교 분석으로 운영에 바로 쓰일 문장을 만들어 냈다. 예: “이번 주 화요일은 평균 대비 40% 낮은 진료 건수로 지속 하락 추세.”

프롬프트는 우선 GPT‑5로 초안을 만들고, 그 지시 체계와 예시를 정제해 경량 모델(5‑mini)에서 재현하도록 지식 증류했다. 이 방식으로 속도·비용은 낮추고 품질은 유지했다. 또한 출력 JSON 스키마를 고정하고 형식이 어긋나면 자동 재시도하도록 가드레일을 두어 파싱 오류를 사실상 제거했다. 결국 우리는 “숫자를 요약”이 아니라 숫자 사이의 관계를 해석하게 만드는 법을 익혔다.

2. 비동기 전환으로 처리량을 높이다

초기에는 모든 처리가 동기(Synchronous)로 동작했다. 요청이 들어오면 서버는 데이터를 수집하고 AI API에 요청을 보낸 뒤 응답이 올 때까지 메인 스레드에서 대기했다. 테스트 환경 기준 AI 응답까지 평균 약 8초가 걸렸고(모델 연산+네트워크+토큰 생성 포함) 이 동안 해당 스레드는 다른 요청을 처리할 수 없었다.

1
2
3
4
5
6
// Before: 동기 처리 (메인 스레드 ≈8초 점유)
public AnalysisResult analyze(Long id) {
    AnalysisData data = dataCollector.collect(id);      // ≈0.2s
    String aiResponse = aiClient.analyze(data);         // ≈8s (blocking on main)
    return responseParser.parse(aiResponse);
}

요청이 몰리면 스레드 풀이 빠르게 고갈되고 큐가 쌓였다. 결국 하나의 AI 호출이 전체 처리량의 병목이 됐다.

해결책은 비동기(Asynchronous) 전환이었다. @AsyncCompletableFuture데이터 수집(메인)AI 호출(백그라운드)을 분리하고, 비동기 전담 서비스에서 스레드 풀로 병렬 처리하도록 구성했다. 이렇게 역할을 분리하면 메인 트랜잭션은 짧게 끝나고, @Transactional@Async를 한 클래스에 섞어 쓸 때 생길 수 있는 프록시 이슈도 자연스럽게 피할 수 있다.

1
2
3
4
5
// After: 메인 서비스 (데이터 수집 + 비동기 호출, API 계약 유지)
public AnalysisResult analyze(Long id) {
    AnalysisData data = dataCollector.collect(id); // ≈0.2s
    return asyncService.analyzeAsync(data).get(180, TimeUnit.SECONDS);
}
1
2
3
4
5
6
7
8
// After: 비동기 전담 서비스 (백그라운드에서 AI 처리)
@Async("analysisTaskExecutor")
public CompletableFuture<AnalysisResult> analyzeAsync(AnalysisData data) {
    return CompletableFuture.supplyAsync(() -> {
        String aiResponse = aiClient.analyze(data); // ≈8s (main thread free)
        return responseParser.parse(aiResponse);
    });
}

효과

  • 메인 스레드 점유: 8초 → 0.2초
  • 동시 처리 능력: 약 40배(이론치)
  • 개별 응답 속도: 2~4% 개선(모델 연산 외 지연 감소)

현재는 백그라운드 스레드 풀(background-thread-1~N) 기반으로 로깅·모니터링 중이며, 이후 큐 기반 처리(예: 메시지 브로커)나 응답 캐싱으로 더 끌어올릴 계획이다.

주의: 이번 단계는 API 계약을 바꾸지 않은 제한적 비동기다. 완전한 비동기로 가려면 “즉시 응답 → 완료 시 WebSocket 푸시 또는 Redis/DB 저장 후 폴링” 같은 패턴으로 확장해야 한다.

3. 프롬프트 DB 이관으로 배포 없이 개선하기

초기에는 프롬프트를 application.properties에 하드코딩했다. 문구를 조금만 바꿔도 수정 → 빌드 → 배포가 필요했다. 이를 DB 기반 관리로 전환해 AI 호출 시점에 DB에서 프롬프트를 읽어오도록 변경했고, 병원·카테고리·타입별로 버전 관리가 가능해졌다. 이제 운영자가 직접 수정하면 즉시 반영된다.

효과

  • 운영 효율: 재배포 불필요
  • 개선 속도: 실시간 수정으로 빠른 루프
  • 데이터 기반: 요청/응답 로그로 프롬프트 효과 측정

추가로 DB 조회 구간에는 캐시를 붙여 로드 성능을 높일 예정이다.

4. Spring AI 도입으로 유연한 구조 만들기

초기에는 OpenAI API를 RestClient로 직접 호출했다. 모델명/옵션/파싱을 매 서비스에서 관리하다 보니 중복이 늘고 유지보수가 어려웠다. 그래서 Spring AI로 추상화를 도입했다. Spring AI는 OpenAI·Azure·Anthropic 등 여러 벤더를 공통 인터페이스로 다룰 수 있게 해 준다.

1
2
3
4
5
OpenAiChatOptions.builder()
    .model(modelOptions.getModelName())
  .temperature(1.0)
    .maxCompletionTokens(modelOptions.getMaxCompletionTokens())
  .build();

효과: 모델 교체 시 코드 변경 최소화, 옵션·요청·응답 구조 일원화, 공통 로깅/토큰 집계로 운영 가시성 향상.
: 모델별로 지원 옵션과 범위가 다를 수 있으니 전략 클래스/검증 계층을 둬 안전하게 매핑하자.

📊 성과 요약

아래 표로 핵심 변화를 정리했다. 이어서 각 항목을 간단히 훑어보자.

구분개선 내용주요 변화성과 지표 / 효과
비동기 처리 전환@Async + CompletableFuture, 전담 서비스 분리요청 블로킹 제거, 동시 처리 향상⏱️ 메인 스레드 8초 → 0.2초
🚀 응답 속도 2~4% 개선
프롬프트 DB 이관DB 관리 + 즉시 반영배포 없이 수정, 운영 루프 단축🔁 운영 효율 향상
⚡ 개선 주기 단축
Spring AI 도입RestClient → 추상화모델 교체/확장 용이, 코드 중복 감소🧩 유지보수성 향상
📦 구조 일관성
UX 개선(사이드 패널)그래프와 AI 결과 동시 노출스크롤 없이 비교, 가시성 개선👁️ 피로도 감소
📈 활용성 증가

5. 사이드 패널로 통계와 인사이트를 한눈에 보기

처음에는 통계 테이블 하단에 분석 결과를 표시했지만, 스크롤이 많아 그래프와 나란히 보기 어려웠다. 그래서 사이드 패널을 도입했다. 버튼을 누르면 우측에서 패널이 열리고, 메인 영역은 살짝 좁아진다. 덕분에 그래프와 AI 결과를 동시에 비교할 수 있다. 완료 시 패널에 즉시 표시되며, X 버튼으로 언제든 닫을 수 있다.

또한 홈페이지 통계에서는 분석에 더해 건강 콘텐츠 초안 제안을 함께 제공해 데이터에서 행동으로 자연스럽게 이어지도록 했다. 이제 사용자는 스크롤 없이 통계 그래프 + AI 분석 + 콘텐츠 제안을 한 화면에서 확인할 수 있다.

🎯 마치며 — “AI와 함께 일하는 법을 배우는 중”

가장 크게 배운 점은, AI 기능을 붙이는 일은 모델을 호출하는 일이 아니라 경험을 설계하는 일이라는 사실이다. 프롬프트 한 줄, 호출 구조, 결과를 보여주는 위치 하나까지 모두 사용자의 흐름과 맞닿아 있었다. 그래서 “AI가 대신 일한다”가 아니라 “AI와 함께 일한다”로 관점을 바꿨다.

이 기반 위에서 앞으로는

  • 분석 결과와 서비스 기능을 자연스럽게 연결하는 흐름,
  • 프롬프트 품질을 높이는 로그 기반 자동 최적화,
  • 반복 호출 구간에 대한 응답 캐싱/성능 고도화

를 점진적으로 확장할 계획이다. 이 프로젝트는 AI가 데이터를 해석하고 사람은 더 빠르게 결정하는 새로운 협업 방식을 실험한 과정이었고, 그 실험은 지금도 계속되고 있다.

This post is licensed under CC BY 4.0 by the author.