AI 실습 공간
AI 기술을 직접 체험하고 코드를 테스트할 수 있는 인터랙티브 실습 공간입니다.
데이터 정제 및 시각화 실습
OpenAI GPT-4 Turbo를 활용한 데이터 정제 코드 생성 및 Tableau AI, PowerBI Copilot을 이용한 시각화 실습을 진행합니다.
설문 자동화 실습
Typeform AI와 SurveyMonkey Genius를 활용한 맞춤형 설문지 자동 생성 및 응답 데이터 분석 실습을 진행합니다.
CRM 자동화 실습
Salesforce Einstein과 HubSpot AI를 활용한 CRM 자동화 및 고객 관계 관리 향상 실습을 진행합니다.
문서 요약/생성 실습
Notion AI와 Elicit을 활용한 의료/제약 문서 자동 요약 및 보고서 생성 실습을 진행합니다.
이미지/문서 정보 추출 실습
Google Cloud Vision AI와 Azure Computer Vision을 활용한 의료 이미지 및 복잡한 문서에서 정보 추출 실습을 진행합니다.
AI 챗봇 실습
OpenAI GPT와 Azure OpenAI를 활용한 의료/제약 특화 AI 챗봇 개발 실습을 진행합니다.
AI 실습 환경 설정 가이드
실제 AI 기능을 구현하고 배포하기 위한 단계별 가이드입니다. 클라우드 설정부터 API 통합, 배포까지 필요한 모든 정보를 제공합니다.
실습 안내
위의 6개 데모는 Mock 데이터를 사용한 가상 테스트입니다. 아래 가이드를 따라 실제 API를 구축하고 연동하면 실제 AI 기능을 사용할 수 있습니다. 각 단계를 클릭하여 상세 내용을 확인하세요.
클라우드 설정
AWS, Azure, GCP 설정
개발 환경
Python, Node.js 설정
API 통합
AI API 연동 및 구현
배포
서비스 배포 및 운영
1. 클라우드 환경 설정
의료/제약 분야에서의 AI 활용: 클라우드 설정 시 의료 데이터의 보안과 개인정보 보호를 위한 HIPAA, GDPR 등의 규정을 준수하는 설정이 필요합니다. 아래 가이드는 의료 데이터 처리에 적합한 클라우드 환경 설정 방법을 제공합니다.
AWS 설정 의료용 AI 서비스
- AWS 계정 생성 및 로그인
- IAM 콘솔에서 새 사용자 생성 및 API 접근 권한 부여
- 의료 데이터용 서비스 설정:
- Amazon Comprehend Medical: 의료 문서에서 진단, 약물, 절차 등 추출
- Amazon HealthLake: FHIR 형식의 의료 데이터 저장 및 분석
- AWS HealthScribe: 의사-환자 대화 음성 분석 및 문서화
- HIPAA 준수를 위한 보안 설정:
- AWS BAA(Business Associate Addendum) 서명
- S3 버킷 암호화 설정
- CloudTrail 로깅 활성화
- AWS SDK 설치:
pip install boto3 aws-healthcare
AWS Comprehend Medical 예제 코드:
import boto3
comprehend_medical = boto3.client(service_name='comprehendmedical')
text = "환자는 고혈압과 당뇨병이 있으며 하루 아토바스타틴 40mg을 복용하고 있습니다."
result = comprehend_medical.detect_entities(Text=text)
for entity in result['Entities']:
print(f"Entity: {entity['Text']}, Type: {entity['Type']}, Category: {entity['Category']}")Azure 설정 제약 연구용 AI
- Microsoft Azure 계정 생성 및 로그인
- Azure Portal에서 의료용 리소스 그룹 생성
- 제약 연구용 AI 서비스 설정:
- Azure Health Insights: 임상 데이터 분석 및 환자 향상 예측
- Azure Text Analytics for Health: 의료 문서에서 정보 추출
- Azure Machine Learning: 약물 반응 예측 모델 구축
- HIPAA 및 GDPR 준수를 위한 설정:
- Azure Security Center 활성화
- 저장소 암호화 설정
- Azure Monitor 및 Log Analytics 설정
- Azure SDK 설치:
pip install azure-ai-textanalytics azure-healthinsights
Azure Text Analytics for Health 예제 코드:
from azure.core.credentials import AzureKeyCredential
from azure.ai.textanalytics import TextAnalyticsClient
endpoint = "YOUR_ENDPOINT"
key = "YOUR_KEY"
text_analytics_client = TextAnalyticsClient(
endpoint=endpoint,
credential=AzureKeyCredential(key)
)
documents = ["환자는 지난 2주간 가슴 통증을 호소하였으며, 심전도에서 ST부분 상승이 관찰되었습니다."]
result = text_analytics_client.analyze_healthcare_entities(documents)
docs = [doc for doc in result if not doc.is_error]
for doc in docs:
for entity in doc.entities:
print(f"Entity: {entity.text}, Category: {entity.category}, Subcategory: {entity.subcategory}")GCP 설정 임상시험 분석
- Google Cloud Platform 계정 생성 및 로그인
- 의료 데이터 분석용 프로젝트 생성
- 임상시험 분석용 API 활성화:
- Google Cloud Healthcare API: 의료 데이터 저장 및 관리
- Google Cloud Life Sciences API: 생명과학 데이터 처리
- Google Cloud AutoML: 임상시험 결과 예측 모델 구축
- 의료 데이터 보안 설정:
- Cloud IAM 권한 세분화
- Cloud KMS를 사용한 데이터 암호화
- Cloud Audit Logs 활성화
- GCP SDK 설치:
pip install google-cloud-healthcare google-cloud-automl
Google Cloud Healthcare API 예제 코드:
from google.cloud import healthcare
client = healthcare.HealthcareClient()
# FHIR 스토어 접근 경로 설정
fhir_store_name = f"projects/{project_id}/locations/{location}/datasets/{dataset_id}/fhirStores/{fhir_store_id}"
# 환자 리소스 검색
resource_type = "Patient"
request = client.projects().locations().datasets().fhirStores().fhir().search(
parent=fhir_store_name,
resourceType=resource_type
)
response = request.execute()
# 결과 처리
for entry in response.get('entry', []):
resource = entry.get('resource', {})
patient_id = resource.get('id')
print(f"Patient ID: {patient_id}")2. 로컬 개발 환경 설정
의료/제약 분야에서의 로컬 개발: 의료 데이터를 다루는 애플리케이션 개발 시 실제 환자 데이터 대신 테스트용 가상 데이터를 사용하는 것이 중요합니다. 아래 가이드는 의료 데이터 분석을 위한 로컬 환경 설정 방법을 제공합니다.
Python 개발 환경 설정 의료 데이터 분석
- Python 3.8 이상 설치
- Anaconda 또는 Miniconda 설치 (의료 데이터 분석을 위한 패키지 관리)
- 의료 데이터 분석용 가상환경 생성:
conda create -n medical-ai python=3.8 - 가상환경 활성화:
conda activate medical-ai - 의료 데이터 분석용 패키지 설치:
pip install fastapi uvicorn pandas numpy scikit-learnpip install pydicom nibabel biopython(의료 이미지 처리)pip install lifelines statsmodels(생존 분석 및 통계)
DICOM 의료 이미지 처리 예제 코드:
import pydicom
import matplotlib.pyplot as plt
import numpy as np
# DICOM 파일 읽기
def read_dicom_image(file_path):
dicom_data = pydicom.dcmread(file_path)
img = dicom_data.pixel_array
return img, dicom_data
# 이미지 전처리 및 표시
def process_and_display(img):
# 이미지 정규화
img_normalized = (img - np.min(img)) / (np.max(img) - np.min(img))
plt.figure(figsize=(10, 8))
plt.imshow(img_normalized, cmap='bone')
plt.colorbar()
plt.title('DICOM 의료 이미지')
plt.show()
return img_normalizedNode.js 개발 환경 설정 의료 대시보드
- Node.js 16.x 이상 설치
- npm 또는 yarn 설치
- 의료 대시보드용 Next.js 프로젝트 생성:
npx create-next-app@latest medical-dashboard --typescript - 프로젝트 디렉토리로 이동:
cd medical-dashboard - 의료 데이터 시각화용 패키지 설치:
npm install axios tailwindcss postcss autoprefixernpm install chart.js react-chartjs-2(의료 데이터 차트)npm install @visx/visx(고급 데이터 시각화)npm install cornerstone-core cornerstone-tools(의료 이미지 뷰어)
임상시험 데이터 시각화 컴포넌트 예제:
import React from 'react';
import { Line } from 'react-chartjs-2';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
const ClinicalTrialChart = () => {
const data = {
labels: ['Week 0', 'Week 4', 'Week 8', 'Week 12', 'Week 16', 'Week 20'],
datasets: [
{
label: '실험군',
data: [65, 59, 52, 45, 40, 35],
borderColor: 'rgb(53, 162, 235)',
backgroundColor: 'rgba(53, 162, 235, 0.5)',
},
{
label: '대조군',
data: [65, 62, 60, 57, 55, 53],
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.5)',
},
],
};
const options = {
responsive: true,
plugins: {
legend: {
position: 'top' as const,
},
title: {
display: true,
text: '임상시험 결과: 실험군 vs 대조군',
},
},
};
return <Line options={options} data={data} />;
};
export default ClinicalTrialChart;Docker 설정 재현성 환경
- Docker Desktop 설치
- Docker Compose 설치
- 의료 데이터 분석용 Dockerfile 작성:
FROM python:3.8-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] - 의료 애플리케이션용 Docker Compose 파일 작성:
version: '3' services: backend: build: ./backend ports: - "8000:8000" volumes: - ./backend:/app environment: - DATABASE_URL=postgresql://postgres:postgres@db:5432/medicaldb - PYTHONPATH=/app depends_on: - db frontend: build: ./frontend ports: - "3000:3000" volumes: - ./frontend:/app environment: - NEXT_PUBLIC_API_URL=http://localhost:8000 depends_on: - backend db: image: postgres:13 ports: - "5432:5432" environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres - POSTGRES_DB=medicaldb volumes: - postgres_data:/var/lib/postgresql/data volumes: postgres_data: - 컨테이너 빌드 및 실행:
docker-compose up -d
3. API 연동 및 테스트
의료/제약 분야의 API 연동: 의료 데이터를 처리하는 API를 개발할 때는 데이터 보안, 개인정보 보호, 규제 준수가 매우 중요합니다. 아래 가이드는 의료 데이터 처리를 위한 안전한 API 개발 및 연동 방법을 제공합니다.
의료 데이터용 FastAPI 백엔드 구축 FHIR 지원
from fastapi import FastAPI, HTTPException, Depends, Security
from fastapi.security import APIKeyHeader
from pydantic import BaseModel
from typing import List, Optional
import uvicorn
import json
from datetime import datetime
app = FastAPI(title="의료 데이터 API", description="FHIR 표준을 지원하는 의료 데이터 API")
# API 키 보안 설정
API_KEY = "your-secure-api-key"
api_key_header = APIKeyHeader(name="X-API-Key")
def get_api_key(api_key: str = Security(api_key_header)):
if api_key != API_KEY:
raise HTTPException(status_code=403, detail="Invalid API Key")
return api_key
# FHIR 환자 모델
class PatientResource(BaseModel):
resourceType: str = "Patient"
id: Optional[str] = None
identifier: List[dict] = []
name: List[dict] = []
gender: Optional[str] = None
birthDate: Optional[str] = None
address: Optional[List[dict]] = None
telecom: Optional[List[dict]] = None
active: bool = True
# 환자 데이터 저장소 (실제로는 데이터베이스 사용)
patients_db = {}
@app.post("/fhir/Patient", response_model=PatientResource)
async def create_patient(patient: PatientResource, api_key: str = Depends(get_api_key)):
if not patient.id:
patient.id = f"patient-{len(patients_db) + 1}"
patients_db[patient.id] = patient.dict()
return patient
@app.get("/fhir/Patient/{patient_id}", response_model=PatientResource)
async def get_patient(patient_id: str, api_key: str = Depends(get_api_key)):
if patient_id not in patients_db:
raise HTTPException(status_code=404, detail="Patient not found")
return patients_db[patient_id]
@app.get("/fhir/Patient", response_model=List[PatientResource])
async def search_patients(
name: Optional[str] = None,
gender: Optional[str] = None,
api_key: str = Depends(get_api_key)
):
results = []
for patient in patients_db.values():
match = True
if name and not any(name.lower() in n.get("family", "").lower() or
name.lower() in n.get("given", [""])[0].lower()
for n in patient.get("name", [])):
match = False
if gender and patient.get("gender") != gender:
match = False
if match:
results.append(patient)
return results
# 진단 모델
class DiagnosisResource(BaseModel):
resourceType: str = "Condition"
id: Optional[str] = None
subject: dict # 환자 참조
code: dict # 진단 코드 (ICD-10, SNOMED CT 등)
clinicalStatus: dict
verificationStatus: dict
recordedDate: str
recorder: Optional[dict] = None # 의료인 참조
# 진단 데이터 저장소
diagnoses_db = {}
@app.post("/fhir/Condition", response_model=DiagnosisResource)
async def create_diagnosis(diagnosis: DiagnosisResource, api_key: str = Depends(get_api_key)):
if not diagnosis.id:
diagnosis.id = f"condition-{len(diagnoses_db) + 1}"
# 환자 존재 여부 확인
patient_id = diagnosis.subject.get("reference", "").replace("Patient/", "")
if patient_id not in patients_db:
raise HTTPException(status_code=404, detail="Referenced patient not found")
diagnoses_db[diagnosis.id] = diagnosis.dict()
return diagnosis
@app.get("/fhir/Condition/{diagnosis_id}", response_model=DiagnosisResource)
async def get_diagnosis(diagnosis_id: str, api_key: str = Depends(get_api_key)):
if diagnosis_id not in diagnoses_db:
raise HTTPException(status_code=404, detail="Diagnosis not found")
return diagnoses_db[diagnosis_id]
if __name__ == "__main__":
uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)실행 방법: python app.py
의료 대시보드용 Next.js 프론트엔드 환자 데이터 시각화
// pages/patients/[id].tsx
import { useRouter } from 'next/router';
import { useState, useEffect } from 'react';
import axios from 'axios';
import { Line, Bar } from 'react-chartjs-2';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
Title,
Tooltip,
Legend
);
// API 설정
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
const API_KEY = process.env.NEXT_PUBLIC_API_KEY || 'your-secure-api-key';
// API 클라이언트 설정
const apiClient = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY,
},
});
export default function PatientDetail() {
const router = useRouter();
const { id } = router.query;
const [patient, setPatient] = useState(null);
const [diagnoses, setDiagnoses] = useState([]);
const [vitals, setVitals] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// 환자 데이터 가져오기
useEffect(() => {
if (!id) return;
const fetchPatientData = async () => {
try {
setLoading(true);
// 환자 정보 가져오기
const patientResponse = await apiClient.get(`/fhir/Patient/${id}`);
setPatient(patientResponse.data);
// 환자의 진단 정보 가져오기
const diagnosesResponse = await apiClient.get(`/fhir/Condition?patient=${id}`);
setDiagnoses(diagnosesResponse.data);
// 환자의 활력징후 가져오기 (예: 혈압, 맥박, 체온 등)
const vitalsResponse = await apiClient.get(`/fhir/Observation?patient=${id}&category=vital-signs`);
setVitals(vitalsResponse.data);
setLoading(false);
} catch (err) {
setError(err.message);
setLoading(false);
}
};
fetchPatientData();
}, [id]);
// 활력징후 차트 데이터 준비
const prepareVitalsChartData = () => {
if (!vitals || vitals.length === 0) return null;
// 혈압 데이터 필터링
const bpReadings = vitals.filter(v =>
v.code?.coding?.some(c => c.code === '85354-9') // LOINC 코드: 혈압
);
// 날짜별로 정렬
bpReadings.sort((a, b) => new Date(a.effectiveDateTime) - new Date(b.effectiveDateTime));
// 차트 데이터 구성
return {
labels: bpReadings.map(bp => new Date(bp.effectiveDateTime).toLocaleDateString()),
datasets: [
{
label: '수축기 혈압',
data: bpReadings.map(bp => bp.component.find(c => c.code.coding[0].code === '8480-6')?.valueQuantity.value),
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.5)',
},
{
label: '이완기 혈압',
data: bpReadings.map(bp => bp.component.find(c => c.code.coding[0].code === '8462-4')?.valueQuantity.value),
borderColor: 'rgb(53, 162, 235)',
backgroundColor: 'rgba(53, 162, 235, 0.5)',
}
]
};
};
// 진단 차트 데이터 준비
const prepareDiagnosisChartData = () => {
if (!diagnoses || diagnoses.length === 0) return null;
// 진단 카테고리별 집계
const categories = {};
diagnoses.forEach(d => {
const category = d.code.coding[0].display;
categories[category] = (categories[category] || 0) + 1;
});
return {
labels: Object.keys(categories),
datasets: [
{
label: '진단 분포',
data: Object.values(categories),
backgroundColor: [
'rgba(255, 99, 132, 0.5)',
'rgba(53, 162, 235, 0.5)',
'rgba(255, 206, 86, 0.5)',
'rgba(75, 192, 192, 0.5)',
'rgba(153, 102, 255, 0.5)',
],
}
]
};
};
if (loading) return <div>로딩 중...</div>;
if (error) return <div>오류 발생: {error}</div>;
if (!patient) return <div>환자 정보를 찾을 수 없습니다.</div>;
const vitalsChartData = prepareVitalsChartData();
const diagnosisChartData = prepareDiagnosisChartData();
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">환자 정보</h1>
<div className="bg-white shadow rounded-lg p-6 mb-6">
<h2 className="text-xl font-semibold mb-4">기본 정보</h2>
<div className="grid grid-cols-2 gap-4">
<div>
<p><strong>이름:</strong> {patient.name[0]?.family} {patient.name[0]?.given?.join(' ')}</p>
<p><strong>성별:</strong> {patient.gender === 'male' ? '남성' : '여성'}</p>
<p><strong>생년월일:</strong> {patient.birthDate}</p>
</div>
<div>
<p><strong>연락처:</strong> {patient.telecom?.[0]?.value}</p>
<p><strong>주소:</strong> {patient.address?.[0]?.text}</p>
</div>
</div>
</div>
{vitalsChartData && (
<div className="bg-white shadow rounded-lg p-6 mb-6">
<h2 className="text-xl font-semibold mb-4">혈압 기록</h2>
<div className="h-64">
<Line
data={vitalsChartData}
options={{
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: '혈압 추이'
}
}
}}
/>
</div>
</div>
)}
{diagnosisChartData && (
<div className="bg-white shadow rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4">진단 분포</h2>
<div className="h-64">
<Bar
data={diagnosisChartData}
options={{
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: '진단 카테고리별 분포'
}
}
}}
/>
</div>
</div>
)}
</div>
);
}의료 영상 분석 API X-Ray 이미지 분석
from fastapi import FastAPI, File, UploadFile, HTTPException, Depends, Security
from fastapi.security import APIKeyHeader
from fastapi.middleware.cors import CORSMiddleware
import numpy as np
import pydicom
import tensorflow as tf
import io
from PIL import Image
import uvicorn
app = FastAPI(title="의료 영상 분석 API", description="X-Ray 이미지 분석을 위한 API")
# CORS 설정
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 실제 환경에서는 특정 도메인으로 제한
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# API 키 보안 설정
API_KEY = "your-secure-api-key"
api_key_header = APIKeyHeader(name="X-API-Key")
def get_api_key(api_key: str = Security(api_key_header)):
if api_key != API_KEY:
raise HTTPException(status_code=403, detail="Invalid API Key")
return api_key
# 모델 로드 (실제로는 사전 훈련된 모델 사용)
# model = tf.keras.models.load_model('path/to/your/model')
# 가상의 모델 예측 함수
def predict_xray(image_array):
# 실제로는 모델을 통한 예측 수행
# predictions = model.predict(image_array)
# 가상의 예측 결과
predictions = {
"pneumonia": 0.85,
"normal": 0.15,
"tuberculosis": 0.05,
"covid19": 0.03,
"pleural_effusion": 0.02
}
return predictions
@app.post("/api/analyze/xray")
async def analyze_xray(
file: UploadFile = File(...),
api_key: str = Depends(get_api_key)
):
# 파일 확장자 확인
if not file.filename.lower().endswith(('.dcm', '.png', '.jpg', '.jpeg')):
raise HTTPException(status_code=400, detail="지원되지 않는 파일 형식입니다. DICOM, PNG, JPG 파일만 허용됩니다.")
try:
# DICOM 파일 처리
if file.filename.lower().endswith('.dcm'):
contents = await file.read()
with io.BytesIO(contents) as dicom_buffer:
dicom_data = pydicom.dcmread(dicom_buffer)
image_array = dicom_data.pixel_array
# 전처리 (정규화, 크기 조정 등)
image_array = (image_array - np.min(image_array)) / (np.max(image_array) - np.min(image_array))
image_array = np.uint8(image_array * 255)
# 일반 이미지 파일 처리
else:
contents = await file.read()
with Image.open(io.BytesIO(contents)) as img:
# 그레이스케일로 변환
img = img.convert('L')
# 크기 조정 (예: 224x224)
img = img.resize((224, 224))
image_array = np.array(img)
# 모델 입력 형태로 변환
image_array = np.expand_dims(image_array, axis=0)
if len(image_array.shape) == 3: # 채널 추가
image_array = np.expand_dims(image_array, axis=-1)
# 예측 수행
predictions = predict_xray(image_array)
# 결과 반환
sorted_predictions = dict(sorted(predictions.items(), key=lambda x: x[1], reverse=True))
return {
"success": True,
"predictions": sorted_predictions,
"primary_finding": max(predictions.items(), key=lambda x: x[1])[0],
"confidence": max(predictions.values())
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"이미지 분석 중 오류가 발생했습니다: {str(e)}")
if __name__ == "__main__":
uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)API 테스트 및 검증 규제 준수
- API 보안 테스트:
- 인증 및 권한 부여 메커니즘 검증
- API 키 관리 및 로테이션 정책 수립
- HTTPS/TLS 구성 확인
- 데이터 무결성 테스트:
- FHIR 리소스 유효성 검사
- 데이터 일관성 및 관계 검증
- 입력 데이터 검증 및 살균
- 규제 준수 검증:
- HIPAA 준수 확인 (미국)
- GDPR 준수 확인 (유럽)
- 개인정보보호법 준수 확인 (한국)
- 감사 로깅 및 추적 기능 검증
- 성능 및 확장성 테스트:
- 부하 테스트 (JMeter, Locust 등 사용)
- 응답 시간 및 처리량 측정
- 자원 사용량 모니터링
- API 문서화:
- Swagger/OpenAPI 스펙 생성
- API 사용 예제 및 샘플 코드 제공
- 오류 코드 및 처리 방법 문서화
API 테스트 스크립트 예제 (Python/pytest):
import pytest
import requests
import json
API_URL = "http://localhost:8000"
API_KEY = "your-secure-api-key"
# 테스트 환자 데이터
test_patient = {
"resourceType": "Patient",
"name": [
{
"family": "김",
"given": ["철수"]
}
],
"gender": "male",
"birthDate": "1980-01-01",
"address": [
{
"text": "서울시 강남구"
}
],
"telecom": [
{
"system": "phone",
"value": "010-1234-5678"
}
]
}
@pytest.fixture
def api_client():
session = requests.Session()
session.headers.update({
"Content-Type": "application/json",
"X-API-Key": API_KEY
})
return session
@pytest.fixture
def created_patient(api_client):
# 테스트용 환자 생성
response = api_client.post(f"{API_URL}/fhir/Patient", json=test_patient)
assert response.status_code == 200
patient_data = response.json()
yield patient_data
# 테스트 후 정리 (환자 삭제 등)
# api_client.delete(f"{API_URL}/fhir/Patient/{patient_data['id']}")
def test_create_patient(api_client):
response = api_client.post(f"{API_URL}/fhir/Patient", json=test_patient)
assert response.status_code == 200
data = response.json()
assert data["resourceType"] == "Patient"
assert data["name"][0]["family"] == "김"
assert data["gender"] == "male"
def test_get_patient(api_client, created_patient):
response = api_client.get(f"{API_URL}/fhir/Patient/{created_patient['id']}")
assert response.status_code == 200
data = response.json()
assert data["id"] == created_patient["id"]
assert data["name"][0]["family"] == "김"
def test_search_patients(api_client, created_patient):
# 이름으로 검색
response = api_client.get(f"{API_URL}/fhir/Patient?name=김")
assert response.status_code == 200
data = response.json()
assert len(data) > 0
assert any(p["id"] == created_patient["id"] for p in data)
# 성별로 검색
response = api_client.get(f"{API_URL}/fhir/Patient?gender=male")
assert response.status_code == 200
data = response.json()
assert len(data) > 0
assert any(p["id"] == created_patient["id"] for p in data)
def test_api_security(api_client):
# 잘못된 API 키로 요청
headers = {
"Content-Type": "application/json",
"X-API-Key": "wrong-api-key"
}
response = requests.get(f"{API_URL}/fhir/Patient", headers=headers)
assert response.status_code == 403 # Forbidden
def test_data_validation(api_client):
# 잘못된 환자 데이터 (필수 필드 누락)
invalid_patient = {
"resourceType": "Patient",
# name 필드 누락
"gender": "unknown" # 잘못된 성별 값
}
response = api_client.post(f"{API_URL}/fhir/Patient", json=invalid_patient)
assert response.status_code in [400, 422] # Bad Request 또는 Unprocessable Entity4. 배포 및 운영
의료/제약 분야의 배포 및 운영: 의료 애플리케이션은 높은 가용성, 확장성, 보안성이 요구됩니다. Kubernetes를 활용한 컨테이너 오케스트레이션은 이러한 요구사항을 충족하는 최적의 솔루션입니다. 아래 가이드는 의료 데이터 처리 애플리케이션을 안전하게 배포하고 운영하는 방법을 제공합니다.
Kubernetes 기반 배포 고가용성
- Kubernetes 클러스터 설정:
- 관리형 Kubernetes 서비스 선택 (EKS, GKE, AKS)
- HIPAA/GDPR 규정 준수를 위한 클러스터 보안 설정
- 네트워크 정책 및 서비스 메시 구성
- 의료 애플리케이션 컨테이너화:
- 보안 강화 베이스 이미지 사용
- 컨테이너 취약점 스캔 통합
- 비밀 정보 관리 (Kubernetes Secrets, Vault)
- Kubernetes 매니페스트 작성:
- Deployment, StatefulSet, Service 리소스 구성
- PodDisruptionBudget을 통한 가용성 보장
- HorizontalPodAutoscaler를 통한 자동 확장
의료 API 서버 Deployment 예제:
apiVersion: apps/v1
kind: Deployment
metadata:
name: medical-api
namespace: healthcare
labels:
app: medical-api
tier: backend
spec:
replicas: 3
selector:
matchLabels:
app: medical-api
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: medical-api
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8000"
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
containers:
- name: medical-api
image: your-registry/medical-api:1.0.0
imagePullPolicy: Always
ports:
- containerPort: 8000
name: http
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 15
periodSeconds: 20
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: medical-api-secrets
key: database-url
- name: API_KEY
valueFrom:
secretKeyRef:
name: medical-api-secrets
key: api-key
- name: LOG_LEVEL
value: "info"
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- medical-api
topologyKey: "kubernetes.io/hostname"
---
apiVersion: v1
kind: Service
metadata:
name: medical-api
namespace: healthcare
spec:
selector:
app: medical-api
ports:
- port: 80
targetPort: 8000
name: http
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: medical-api
namespace: healthcare
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: medical-api
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80CI/CD 파이프라인 구축 자동화
- CI/CD 도구 선택:
- Jenkins, GitLab CI, GitHub Actions, ArgoCD 등
- 의료 규제 준수를 위한 감사 추적 기능 설정
- 파이프라인 단계 구성:
- 코드 품질 검사 및 정적 분석
- 보안 취약점 스캔 (SAST, DAST)
- 컨테이너 이미지 빌드 및 스캔
- 자동화된 테스트 수행
- 승인 워크플로우 구현
- Kubernetes 배포 자동화
GitHub Actions CI/CD 파이프라인 예제:
name: Medical API CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}/medical-api
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.8'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov flake8 bandit
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
- name: Security scan with bandit
run: bandit -r . -x tests/
- name: Run tests with pytest
run: pytest --cov=./ --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
build:
name: Build and Push
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
name: Deploy to Kubernetes
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Set up kubeconfig
uses: azure/k8s-set-context@v3
with:
kubeconfig: ${{ secrets.KUBE_CONFIG }}
- name: Deploy to Kubernetes
uses: azure/k8s-deploy@v4
with:
namespace: healthcare
manifests: |
kubernetes/deployment.yaml
kubernetes/service.yaml
kubernetes/hpa.yaml
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
strategy: canary
percentage: 20
- name: Verify deployment
run: |
kubectl rollout status deployment/medical-api -n healthcare
- name: Promote deployment
if: success()
uses: azure/k8s-deploy@v4
with:
namespace: healthcare
manifests: |
kubernetes/deployment.yaml
kubernetes/service.yaml
kubernetes/hpa.yaml
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
strategy: canary
percentage: 100모니터링 및 로깅 관측성
- 모니터링 스택 구축:
- Prometheus, Grafana, Alertmanager 설정
- 의료 애플리케이션 특화 대시보드 구성
- SLO/SLI 정의 및 모니터링
- 로깅 시스템 구축:
- EFK/ELK 스택 (Elasticsearch, Fluentd/Logstash, Kibana)
- 구조화된 로깅 구현
- 민감 정보 마스킹 및 감사 로그 분리
- 알림 및 대응 체계:
- 중요도별 알림 설정
- 온콜 로테이션 및 에스컬레이션 정책
- 인시던트 대응 플레이북 작성
Prometheus 모니터링 설정 예제:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: medical-api-monitor
namespace: monitoring
labels:
release: prometheus
spec:
selector:
matchLabels:
app: medical-api
namespaceSelector:
matchNames:
- healthcare
endpoints:
- port: http
path: /metrics
interval: 15s
scrapeTimeout: 10s
---
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: medical-api-alerts
namespace: monitoring
labels:
release: prometheus
spec:
groups:
- name: medical-api
rules:
- alert: HighErrorRate
expr: sum(rate(http_requests_total{job="medical-api",status=~"5.."}[5m])) / sum(rate(http_requests_total{job="medical-api"}[5m])) > 0.05
for: 5m
labels:
severity: critical
service: medical-api
annotations:
summary: "High error rate on Medical API"
description: "Medical API error rate is above 5% (current value: {{ $value }})"
- alert: SlowResponseTime
expr: histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job="medical-api"}[5m])) by (le)) > 0.5
for: 5m
labels:
severity: warning
service: medical-api
annotations:
summary: "Slow response time on Medical API"
description: "Medical API 95th percentile response time is above 500ms (current value: {{ $value }}s)"
- alert: HighCPUUsage
expr: sum(rate(container_cpu_usage_seconds_total{container="medical-api"}[5m])) / sum(kube_pod_container_resource_limits_cpu_cores{container="medical-api"}) > 0.8
for: 10m
labels:
severity: warning
service: medical-api
annotations:
summary: "High CPU usage on Medical API"
description: "Medical API CPU usage is above 80% of limit (current value: {{ $value }})"
- alert: HighMemoryUsage
expr: sum(container_memory_working_set_bytes{container="medical-api"}) / sum(kube_pod_container_resource_limits_memory_bytes{container="medical-api"}) > 0.8
for: 10m
labels:
severity: warning
service: medical-api
annotations:
summary: "High memory usage on Medical API"
description: "Medical API memory usage is above 80% of limit (current value: {{ $value }})"
- alert: PodCrashLooping
expr: increase(kube_pod_container_status_restarts_total{container="medical-api"}[15m]) > 3
for: 10m
labels:
severity: critical
service: medical-api
annotations:
summary: "Medical API pod is crash looping"
description: "Pod {{ $labels.pod }} has restarted {{ $value }} times in the last 15 minutes"재해 복구 및 비즈니스 연속성 중요
- 백업 및 복구 전략:
- 데이터베이스 정기 백업 자동화
- 지역 간 백업 복제
- 복구 시간 목표(RTO) 및 복구 지점 목표(RPO) 정의
- 멀티 리전 배포:
- 글로벌 로드 밸런싱 구성
- 리전 간 데이터 동기화 전략
- 장애 조치(Failover) 자동화
- 비즈니스 연속성 계획:
- 재해 복구 시나리오 및 플레이북
- 정기적인 DR 훈련 및 테스트
- 규제 준수를 위한 문서화
데이터베이스 백업 CronJob 예제:
apiVersion: batch/v1
kind: CronJob
metadata:
name: medical-db-backup
namespace: healthcare
spec:
schedule: "0 2 * * *" # 매일 오전 2시에 실행
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 3
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
containers:
- name: db-backup
image: your-registry/db-backup:1.0.0
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -c
- |
# 백업 파일명 생성 (날짜 포함)
BACKUP_FILE="medical_db_$(date +%Y%m%d_%H%M%S).sql.gz"
# 데이터베이스 덤프 생성 및 압축
PGPASSWORD=$DB_PASSWORD pg_dump -h $DB_HOST -U $DB_USER -d $DB_NAME | gzip > /backups/$BACKUP_FILE
# 백업 파일 암호화 (HIPAA 준수)
gpg --symmetric --batch --passphrase "$ENCRYPTION_KEY" --cipher-algo AES256 /backups/$BACKUP_FILE
# 클라우드 스토리지에 업로드
aws s3 cp /backups/$BACKUP_FILE.gpg s3://$S3_BUCKET/backups/
# 성공 로그
echo "Backup completed and uploaded to S3: $BACKUP_FILE.gpg"
# 오래된 로컬 백업 정리 (7일 이상)
find /backups -type f -name "*.gpg" -mtime +7 -delete
env:
- name: DB_HOST
valueFrom:
secretKeyRef:
name: medical-db-credentials
key: host
- name: DB_USER
valueFrom:
secretKeyRef:
name: medical-db-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: medical-db-credentials
key: password
- name: DB_NAME
valueFrom:
secretKeyRef:
name: medical-db-credentials
key: database
- name: ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: backup-encryption
key: key
- name: S3_BUCKET
valueFrom:
configMapKeyRef:
name: backup-config
key: s3-bucket
volumeMounts:
- name: backup-volume
mountPath: /backups
volumes:
- name: backup-volume
persistentVolumeClaim:
claimName: backup-pvc
restartPolicy: OnFailure추가 학습 자료
AI 기술에 대한 더 깊은 이해와 실습을 위한 추천 자료입니다:
- OpenAI 공식 문서 및 튜토리얼
GPT 모델 활용 가이드
- AWS 의료 산업 AI 서비스 가이드
의료 서비스 리소스
- Azure 의료 산업 AI 솔루션
Azure OpenAI를 사용하여 의료 서비스 개선
- Hugging Face 의료 특화 모델 라이브러리
텍스트 분석 및 이미지 처리를 위한 사전 훈련된 모델
- LangChain 소개
LangChain 활용을 위한 공식 문서
- FastAPI 개발
FastAPI 개발을 위한 깃허브 레파지토리
- 안정적인 AI 인프라 공급을 위한 Kubernetes를 활용한 배포 전략
안정적인 AI 인프라 공급을 위한 Kubernetes를 활용한 배포 가이드