Loading the catalog…
Loading the catalog…
이번 한 주간 "안아수달" 프로젝트 백엔드(anasudal_BE)와 프론트엔드(anasudal_FE)는 서비스의 핵심 기능 개발과 인프라 안정화에 집중했다. 특히 백엔드는 초기 인프라 비용을 대폭 절감하고, 배포 과정을 간소화하며, Gemini API의 안정성을 높이는 데 주력했다. 안아수달 서비스 소개 안아수달은 아이 발달이 걱정될 때, 검증된 공공 자료로만 확인해볼 영역을 찾아주고 가까운 발달재활 기관을 추천하는 서비스다. 부모가 아이의 발달 상황을 입력하면, 시스템은 1,473개의 지식 조각에서 월령에 맞는 근거를 찾아 Gemini가 답변을 생성하고, 그 근거 안에서 확인해볼 치료 영역을 뽑아 전국 2,866곳 중 해당 영역을 다루는 기관 3곳을 추천한다. 진단 대신 '확인해볼 영역'을 안내하며, 로그인 없이 대화 원문을 저장하지 않아 개인 정보를 보호하는 것을 원칙으로 한다. 인프라 비용 절감과 배포 안정성 확보 이번 주 작업의 가장 큰 줄기는 인프라 비용 절감과 배포 과정의 안정화였다. 초기 AWS 인프라는 월 94달러의 비용이 발생했는데, 이를 해커톤 검증용 최소 비용 수준인 24달러로 낮추면서도 핵심 요구사항(ECR, ECS, EC2, SSM)을 유지하는 것이 목표였다. 문제: 높은 인프라 비용과 복잡한 배포 과정 기존 인프라는 ECS Fargate, RDS PostgreSQL, ElastiCache Redis를 각각 독립적으로 운영하여 월 94달러의 비용이 들었다. 또한, 배포를 위한 IAM 사용자 생성 과정이 복잡했고, 수동으로 키를 입력해야 하는 불편함이 있었다. Windows PowerShell 환경에서는 스크립트 호환성 문제도 발생했다. 기존 계정에 다른 프로젝트( syak )가 있어 리소스 간의 의도치 않은 간섭 가능성도 배제할 수 없었다. HTTPS 미적용으로 프론트엔드와의 통신에 제약이 있는 점도 개선이 필요했다. 판단: 단일 EC2 인스턴스 기반의 통합 환경 구축 비용 절감과 배포 편의성을 동시에 잡기 위해, 단일 EC2 인스턴스 위에 모든 백엔드 구성 요소를 통합하는 방향으로 전환했다. ECS 시작 유형 변경: Fargate를 EC2 시작 유형으로 변경하여 게이트웨이 인스턴스가 컨테이너 인스턴스 역할을 겸하게 했다. 데이터베이스 및 캐시 통합: RDS와 ElastiCache를 제거하고, pgvector/pgvector:pg16 및 redis:7-alpine 컨테이너를 EC2 인스턴스 위에서 실행했다. 데이터는 EBS의 /var/lib/anasudal 에 저장하여 영속성을 확보했다. NAT Gateway 제거: EC2 인스턴스가 퍼블릭 서브넷에서 EIP를 통해 직접 외부와 통신하므로 월 43달러에 달하는 NAT Gateway를 제거했다. 보안 그룹 간소화: 프라이빗 서브넷과 API/DB 보안 그룹을 제거하고, 외부에 열린 포트는 80/443만 유지했다. AMI 교체 및 리소스 증설: ARM 기반 t4g.micro 인스턴스를 t3.small (2GB) x86 ECS 최적화 AMI로 교체하여 Postgres, Redis, API를 안정적으로 운영할 수 있도록 했다. 루트 볼륨도 8GB에서 30GB로 확장했다. HTTPS 적용: CloudFront를 앞에 두어 도메인 없이 *.cloudfront.net HTTPS를 얻도록 구성했다. 캐시는 끄고 모든 헤더와 쿼리를 오리진(EC2)에 넘겼다. 배포 과정의 편의성과 보안을 위해 IAM 정책을 강화하고 부트스트랩 스크립트를 개선했다. 프로젝트 전용 IAM 사용자: anasudal-deploy IAM 사용자를 생성하고, anasudal-* 이름 규칙과 Project=anasudal 태그를 따르는 리소스만 접근하도록 권한을 분리했다. 기존 syak 프로젝트의 리소스는 ARN을 명시하여 하드 차단하고, 태그 없는 타 리소스에 Project 태그를 붙여 보호를 해제하는 행위도 Deny 정책으로 막았다. 간소화된 부트스트랩 스크립트: login.sh (Linux/macOS) 및 login.ps1 (Windows PowerShell) 스크립트를 통해 루트 액세스 키 두 줄만 입력하면 IAM 사용자 생성부터 키 등록, 권한 점검까지 자동으로 처리되도록 개선했다. 특히 Windows 환경에서는 .csv 파일이나 메모장 경로를 입력받아 키를 자동으로 파싱하고, 터미널 붙여넣기가 어려운 상황에 대비했다. 배포 문제 해결: systemd 교착 상태 해결을 위해 systemctl restart ecs 호출을 --no-block 으로 변경했고, ec2:DisassociateAddress 동작이 태그 없는 ENI에 적용되어 배포를 막던 IAM Deny 정책도 수정했다. 변경 전/후: 간소화된 인프라와 자동화된 키 입력 1. (BE) 인프라 분리 및 보호 IAM 정책 ( infra/iam/anasudal-deploy-infra.json ) 기존 프로젝트 리소스에 대한 접근을 명시적으로 거부하고, anasudal 프로젝트 리소스만 관리할 수 있도록 IAM 정책을 강화했다. { "Version": "2012-10-17", "Statement": [ { "Sid": "NetworkAndDataPlane", "Effect": "Allow", "Action": [ "ec2:*", "rds:*", "elasticache:*" ], "Resource": "*" }, { "Sid": "DenyTouchingOtherProjects", "Effect": "Deny", "Action": [ "ec2:TerminateInstances", "ec2:StopInstances", "ec2:RebootInstances", "ec2:DeleteVpc", "ec2:DeleteSubnet", "ec2:DeleteSecurityGroup", // ... (중략) ... "rds:DeleteDBInstance", "rds:StopDBInstance", "rds:RebootDBInstance", "rds:ModifyDBInstance", "rds:DeleteDBSubnetGroup", "rds:DeleteDBParameterGroup", "elasticache:DeleteCacheCluster", "elasticache:ModifyCacheCluster", "elasticache:DeleteCacheSubnetGroup" ], "Resource": "*", "Condition": { "StringNotEqualsIfExists": { "aws:ResourceTag/Project": "anasudal" } } }, { "Sid": "DenyAccountWideDamage", "Effect": "Deny", "Action": [ "ec2:DeleteDefaultVpc", // ... (중략) ... "iam:CreateUser", "iam:DeleteUser", "iam:CreateAccessKey", "iam:DeleteAccessKey", "iam:CreatePolicy", "iam:DeletePolicy", "iam:AttachUserPolicy", "iam:DetachUserPolicy" ], "Resource": "*" } ] } DenyTouchingOtherProjects 정책은 Project 태그가 anasudal 이 아닌 리소스에 대해 특정 파괴적인 작업을 명시적으로 거부한다. 또한, DenyAccountWideDamage 정책은 IAM 사용자/정책 조작과 같은 권한 상승 가능성이 있는 작업을 금지하여 보안을 강화했다. 2. (BE) Windows PowerShell을 위한 자동화된 키 입력 ( infra/scripts/login.ps1 ) AWS CLI 키를 수동으로 붙여넣는 대신, 파일이나 메모장에서 자동으로 키를 찾아 입력하는 기능을 추가하여 사용자 경험을 개선했다. # ... (중략) ... # -- 키 읽기 도우미 ----------------------------------------------------------- # 형식을 가리지 않는다. csv / 메모장 메모 / 아무 텍스트에서나 정규식으로 찾아낸다. # 액세스 키 ID : AKIA/ASIA + 영숫자 16자 # 시크릿 : base64 문자 40자 function Get-AwsKeyFromText ($text) { if (-not $text) { return $null } $id = $null; $sec = $null # (1) key = value 표기 (메모장 템플릿, 환경변수, ~/.aws/credentials 형식) $m = [regex]::Match($text, '(?im)^\s*(?:aws[_ ]?)?access[_ ]?key[_ ]?id\s*[=:]\s*["'']?([A-Z0-9]{16,128})') if ($m.Success) { $id = $m.Groups[1].Value } $m = [regex]::Match($text, '(?im)^\s*(?:aws[_ ]?)?secret[_ ]?access[_ ]?key\s*[=:]\s*["'']?([A-Za-z0-9+/=]{30,128})') if ($m.Success) { $sec = $m.Groups[1].Value } # (2) 콘솔에서 받은 .csv — 헤더 이름으로 열을 찾는다. # IAM 사용자 csv 에는 비밀번호 열이 섞여 있어서, 위치로 찍지 않고 이름으로 찾아야 한다. if (-not ($id -and $sec)) { try { foreach ($r in @($text | ConvertFrom-Csv)) { foreach ($p in $r.PSObject.Properties) { $n = ($p.Name -replace '[^A-Za-z]', '').ToLower() if ($n -match 'accesskeyid') { $id = $p.Value } if ($n -match 'secretaccesskey') { $sec = $p.Value } } } } catch { } # ConvertFrom-Csv 가 실패해도 무시 } # (3) 생김새 기반 폴백 — 헤더 없이 키만 있는 경우 if (-not $id) { $m = [regex]::Match($text, '(?m)\b(AKIA|ASIA)[A-Z0-9]{16}\b'); if ($m.Success) { $id = $m.Value } } if (-not $sec) { $m = [regex]::Match($text, '(?m)\b[A-Za-z0-9+/=]{40}\b'); if ($m.Success) { $sec = $m.Value } } if ($id -and $sec) { [PSCustomObject]@{ AccessKeyId = $id; SecretAccessKey = $sec } } else { $null } } # ... (중략) ... Get-AwsKeyFromText 함수는 CSV 파일, 메모장 내용, 또는 일반 텍스트에서 AWS 액세스 키 ID와 시크릿 액세스 키를 자동으로 추출한다. 이를 통해 사용자는 키를 직접 복사하여 붙여넣는 번거로움 없이 쉽게 초기 설정을 완료할 수 있게 되었다. 3. (BE) Gemini 키 풀 분리 및 폴백 설정 ( backend/app/core/config.py ) Gemini API 키를 용도별로 분리하고, 키 풀을 활용하여 429 에러 발생 시 단계적으로 대응하도록 변경했다. # backend/app/core/config.py # ... (중략) ... class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore") database_url: str # ── Gemini 키 # 답변·임베딩(사용자가 기다리는 경로)은 여러 개를 돌려 쓴다. 콤마로 구분. gemini_api_keys: str = "" # 질문 요약(백그라운드로 DB 에 쌓는 것) 전용. 답변 쿼터를 갉아먹지 않게 분리. gemini_summary_key: str = "" # 하위호환 — 키가 하나뿐이던 시절의 이름. 위 두 개가 비면 이걸 쓴다. gemini_api_key: str = "" # ... (중략) ... @property def answer_keys(self) -> list[str]: """답변·임베딩용 키 목록""" return _split(self.gemini_api_keys) or _split(self.gemini_api_key) @property def summary_keys(self) -> list[str]: """요약 전용 키. 따로 안 주면 답변 키를 같이 쓴다(개발 환경).""" return _split(self.gemini_summary_key) or self.answer_keys # ... (중략) ... gemini_api_keys 는 사용자에게 즉시 응답해야 하는 답변 및 임베딩에 사용되며 여러 키를 로테이션한다. gemini_summary_key 는 백그라운드에서 실행되는 질문 요약 전용으로, 사용자 대기 경로의 쿼터에 영향을 주지 않도록 분리했다. 결과: 비용 절감과 안정적인 배포 환경 이러한 변경으로 인프라 월 비용은 94달러에서 24달러로 대폭 절감되었다. 배포 과정은 단일 스크립트 실행으로 간소화되었고, Windows PowerShell 환경에서도 원활하게 작동하게 되었다. 프로젝트 전용 IAM 정책으로 기존 프로젝트와의 간섭 가능성을 완전히 차단하고, CloudFront를 통해 HTTPS를 지원하게 되어 서비스의 안정성과 보안이 강화되었다. Gemini API 키 관리 및 폴백 강화 문제: 단일 Gemini 키의 한계와 429 에러 대응 부재 이전에는 하나의 Gemini API 키로 모든 LLM 작업을 처리했다. 이는 사용자가 기다리는 답변 생성, 임베딩, 그리고 백그라운드에서 이루어지는 질문 요약까지 모두 같은 쿼터를 소모하게 만들어, 429(Rate Limit Exceeded) 에러 발생 시 서비스 전체에 영향을 줄 수 있었다. 429 에러에 대한 적절한 폴백(fallback) 전략이 없어 사용자 경험을 해칠 우려도 있었다. 판단: 키 풀 분리와 단계적 폴백 구현 사용자 대기 경로의 안정성을 최우선으로 고려하여, Gemini API 키를 두 묶음으로 분리하고 429 에러 발생 시 단계적으로 대응하는 전략을 수립했다. 키 풀 분리: GEMINI_API_KEYS : 사용자에게 직접 응답하는 답변 생성 및 임베딩에 사용되는 키 묶음. 여러 키를 등록하여 로테이션하며 사용한다. GEMINI_SUMMARY_KEY : 백그라운드에서 질문을 요약하여 DB에 저장하는 용도 전용 키. 답변 쿼터와 분리하여 운영 안정성을 높였다. 429 단계적 폴백: 1단계 (키 로테이션): 429 에러 발생 시 해당 키를 잠시 쉬게 하고 즉시 다음 키로 재시도한다. 쉬는 상태는 Redis에 공유하여 여러 ECS 태스크가 같은 죽은 키를 반복해서 호출하는 것을 방지한다. 2단계 (대체 답변): 모든 키가 소진되었지만 근거 데이터는 확보된 경우, LLM 없이 근거와 출처를 그대로 보여주는 대체 답변을 제공한다. 치료 영역은 지식 청크의 K-DST 영역에서 추출하여 기관 추천까지 이어지도록 했다. 3단계 (오류 응답): 검색(임베딩)조차 불가능한 경우, 503 LLM_RATE_LIMITED 에러와 함께 재시도 가능 시간을 안내한다. 4단계 (요약 키 소진): 질문 요약 키가 소진된 경우 조용히 넘어가 question_summary 만 비워둔다. (사용자 대기 경로에 영향 없음) 키 풀 상태 모니터링: GET /health 엔드포인트에 키 풀 상태를 노출하여 운영자가 쉽게 확인할 수 있도록 했다 (키는 해시 앞 8자만 표시). 결과: 안정적인 LLM 서비스 운영 Gemini API 키 풀 분리와 단계적 폴백 구현을 통해 429 에러 발생 시 사용자 경험에 미치는 영향을 최소화하고, LLM 서비스의 안정성을 크게 높였다. 백그라운드 작업이 사용자 대기 경로의 쿼터를 소모하지 않게 되어 전체적인 서비스 지연 위험도 줄었다. 데이터 보강으로 기관 정보 정확도 향상 문제: 낮은 좌표 매칭률과 운영시간·홈페이지 정보 부재 공공데이터 기반의 전국 기관 2,866곳 중 좌표 매칭률이 65.6%에 불과했고, 카카오맵과 공공데이터 간 기관명 불일치로 인해 이름 매칭이 실패하는 경우가 많았다. 특히 운영시간과 기관 자체 홈페이지 정보는 거의 전무한 상태였다. 판단: 카카오 API를 활용한 데이터 보강 기관 정보의 정확도와 풍부함을 높이기 위해 카카오 API를 적극적으로 활용하여 데이터를 보강했다. 카카오 주소검색을 통한 좌표 보강: 기존의 기관명 키워드 검색 방식에서 벗어나, 주소 검색을 우선적으로 사용했다. 도로명 주소 정리 및 시군구 보정 로직을 개선하여 이름이 다르더라도 주소가 맞으면 매칭되도록 했다. 이로써 좌표 매칭률을 65.6%에서 95.0%로 크게 끌어올렸다. 카카오 플레이스 API를 통한 운영시간·홈페이지 수집: 카카오 공식 REST API에는 없는 운영시간과 홈페이지 정보를 카카오맵 웹이 내부적으로 사용하는 비공개 place-api.map.kakao.com 엔드포인트를 통해 수집했다. 비공개 API 사용에 따른 위험을 인지하고, 기본 1.5건/초의 속도 제한과 8건 연속 실패 시 자동 중단 기능을 구현하여 안정성을 확보했다. 수집된 운영시간은 '월 금 10:00 19:30, 토 09:30~18:30'과 같이 보기 좋게 정리하고, 휴무일도 표시하도록 파싱 로직을 개선했다. 이름 매칭 완화: '파랑새감각통합언어발달'과 '파랑새감각통합언어발달센터'처럼 접미사만 다른 경우를 놓치지 않도록 정규화 후 포함 관계를 인정하되, 오탐 방지를 위해 6자 이상일 때만 적용했다. 결과: 풍부하고 정확해진 기관 정보 데이터 보강 작업을 통해 기관 좌표 매칭률은 95%에 달했고, 전화번호 매칭률도 90.4%로 개선되었다. 운영시간은 0%에서 30.7%로, 카카오맵 링크는 71.5%로 대폭 증가하여 사용자에게 더욱 풍부하고 정확한 정보를 제공할 수 있게 되었다. 마무리 이번 한 주간 "안아수달" 프로젝트 백엔드는 인프라 비용 절감, 배포 과정 자동화, Gemini API 안정성 강화, 그리고 기관 정보 데이터 보강이라는 네 가지 주요 목표를 성공적으로 달성했다. 이를 통해 서비스의 운영 효율성과 안정성을 높이고, 사용자에게 더 나은 경험을 제공할 수 있는 견고한 기반을 마련했다. 앞으로도 지속적인 개선을 통해 더 나은 서비스를 만들어 나갈 것이다.
What RADAR observed and classified to build this opportunity. It is what the source published, not a verification that the offer is still active.
안아수달 백엔드: 월 94달러 인프라를 24달러로, 안전하고 쉬운 배포 구축기. 이번 한 주간 "안아수달" 프로젝트 백엔드(anasudal_BE)와 프론트엔드(anasudal_FE)는 서비스의 핵심 기능 개발과 인프라 안정화에 집중했다. 특히 백엔드는 초기 인프라 비용을 대폭 절감하고, 배포 과정을 간소화하며, Gemini API의 안정성을 높이는 데 주력했다. 안아수달 서비스 소개 안아수달은 아이 발달이 걱정될 때, 검증된 공공 자료로만 확인해볼 영역을 찾아주고 가까운 발달재활 기관을 추천하는 서비스다. 부모가 아이의 발달 상황을 입력하면, 시스템은 1,473개의 지식 조각에서 월령에 맞는 근거를 찾아 Gemini가 답변을 생성하고, 그 근거 안에서 확인해볼 치료 영역을 뽑아 전국 2,866곳 중 해당…
Open source