본문 바로가기
MegazoneCloud
← 전체 글
AWS

[AWS]“클럽은 AI가 골라드립니다, 퍼팅은 직접 하세요“ — Bedrock AgentCore 멀티 에이전트로 만든 AI 골프 피팅

한눈에 보기

골프 커머스 고객사 온라인몰에 붙는 AI 피팅 서비스를 103일(2026-02-09 ~ 2026-05-22) 동안 구축해 운영에 올렸습니다. 회원이 키·몸무게를 넣으면 스크린 골프 경기 데이터나 8문항 설문을 바탕으로 스윙을 분석하고, 드라이버·아이언·골프공을 추천하는 서비스입니다.

이 글은 그 과정에서 내린 결정과, 운영에 올리고 나서야 알게 된 지점들을 정리한 기록입니다.

항목값
기간2026-02-09 ~ 2026-05-22 (103일)
AgentCore Runtime2개 (피팅 coordinator · 골프공 recommender)
에이전트피팅 Coordinator 1 + Sub-agent 15 · 골프공 Coordinator 1 + Sub-agent 3
도구MCP Gateway 4개 → Lambda 4개 (python3.11 · 512MB · timeout 30초)
ECS Fargate (운영)서비스 5개 · 태스크 10개
DB 프롬프트13건 (tool 호출 끔 9 · 켬 4)
코드 (인계 시점)AgentCore Python 5,846행 · BFF Python 2,516행 · 프론트 JS/JSX 9,090행

화면 한 번을 따라가 보면

화면은 5단계이고, 경기 데이터가 있는 회원과 없는 회원(또는 게스트)으로 두 갈래가 나뉩니다. 어느 갈래든 추천 순위와 점수는 코드가 정하고, LLM은 그 결과를 설명합니다.

[ 화면 흐름 · 경기 데이터 유무로 갈리는 5단계 ]

경기 데이터가 있는 회원은 분석 두 단계를 거치고, 없는 회원과 게스트는 설문 8문항으로 대신합니다. 두 갈래는 Step4에서 다시 만납니다.

화면 결과숫자·순위를 정하는 곳LLM이 하는 일
Step2 경기 분석고객사 경기 데이터 API 응답경기 분석 리포트 (tool 호출 끔)
Step3 골프 성향 유형경기 데이터의 성향 유형 코드유형 설명과 개선 조언 (tool 호출 끔)
Step4 드라이버·아이언 TOP 3고객사 피팅 API (TOP_1~3)추천 근거 (tool 호출 켬)
Step4 제품 스펙 카드DB 클럽 마스터요약 모드 설명 (tool 호출 끔)
Step5 브랜드 선택 추천BFF 매칭 엔진 (club_matching.py)3~4문장 설명 (tool 호출 끔)
Step5 골프공 추천golfball Lambda 점수표플레이 스타일 분석 500자 이내

1.고객사가 원한 것, 그리고 넘지 못한 선

고객사에는 이미 스크린 골프 경기 데이터와 클럽 피팅 엔진이 있었습니다. 이번 프로젝트의 몫은 그 데이터를 바탕으로 분석·추천 문장을 만들어 온라인몰 화면에 붙이는 일이었습니다.

요구결과
경기 데이터 기반 스윙·경기 분석✅ 화면 Step2(경기 분석) · Step3(골프 성향 유형)
경기 데이터가 없는 회원·게스트✅ 8문항 설문으로 대체 (성별·나이·스코어·드라이버 비거리·7번 아이언 비거리·탄도·구질 9방향·착지 7구역)
드라이버·아이언 추천 TOP 3✅ 고객사 기존 피팅 API 결과를 그대로 사용
브랜드를 고른 뒤 다시 추천✅ BFF 안의 매칭 엔진
골프공 추천✅ 별도 AgentCore Runtime, 고객사 엑셀 점수표 이식
제품 스펙·리뷰 요약·재고·가격✅ product Gateway를 쓰는 Sub-agent
검색결과·상품상세의 AI 피팅 안내✅ 별도 ECS 서비스. 로그인·피팅 보유·검색어 종류로 7가지 타입 분류
사용 횟수 제한✅ 회원당 월 10회, 세션당 브랜드 추천 5회(조정 가능)

바꿀 수 없던 전제

  • 로그인은 쇼핑몰 것을 그대로: 별도 가입 없이 쇼핑몰 JWT를 받아 통합 회원번호를 고객번호로 씁니다. 이 번호는 Coordinator 시스템 프롬프트에 세션마다 바인딩되고, 다른 회원 번호로는 조회하지 않습니다.
  • 피팅 엔진은 다시 만들지 않는다: 경기 데이터 조회와 클럽 TOP 3는 고객사 사내 API를 호출합니다. 우리가 정하는 건 그 결과를 어떻게 설명하느냐입니다.

2.서비스는 이렇게 쌓았다

요청 한 건은 ECS의 BFF를 거쳐 피팅 AgentCore Runtime으로 들어갑니다. Coordinator가 Sub-agent를 고르고, Sub-agent는 MCP Gateway를 통해 Lambda를 부릅니다. 골프공 질문만 A2A로 별도 Runtime에 넘깁니다. 전부 서울 리전 VPC 안에 있습니다.

[ 전체 구성도 · ECS → AgentCore Runtime 2개 → Gateway → Lambda → Aurora ]

BFF는 고객사 API를 직접 부르고, 골프공 질문만 Sub-agent가 A2A로 별도 Runtime에 넘깁니다.

계층구성하는 일
FrontendReact 19 + Vite 6, nginx:80화면 5단계, API 리버스 프록시
BFFStarlette:8080 (같은 ECS Task의 sidecar), 라우트 15개JWT 검증, 프롬프트 템플릿 치환, 사용 횟수 차감, 고객사 피팅 API 호출, 브랜드 매칭 엔진
AgentStrands Agents on AgentCore Runtime (Python 3.10, VPC 모드)Coordinator(Claude Sonnet 4.5) → Sub-agent 15개(Claude Haiku 4.5)
ToolMCP Gateway 3개(fitting · product · session) → Lambda 3개피팅·상품·세션 데이터 조회
골프공recommender Runtime + golfball Gateway → Lambda골프공 Coordinator + Sub-agent 3개, A2A로 호출
MemoryAgentCore Memory STM, 7일 보관세션 대화 맥락. 골프공 Runtime과 session_id 공유
DBAurora PostgreSQL 17.7, db.r8g.large피팅 원천 테이블 8개 + 프롬프트·모델 설정·사용량·대화 기록
별도 서비스ECS 서비스: 상품검색 · 골프공 API · Admin FE/BE검색결과 AI 피팅 안내, 골프공 API, 운영 관리 화면

정해두고 지킨 규칙 네 가지

① 숫자와 순위는 코드가, LLM은 문장만: 클럽 TOP 3, 브랜드 매칭, 골프공 점수는 전부 결정적 계산입니다. LLM은 그 결과를 받아 “왜 이 고객에게 맞는지”를 씁니다. 아래 “추천 숫자는 코드가 정한다”에서 자세히 다룹니다.

② Coordinator는 직접 답하지 않는다: Coordinator 프롬프트의 최우선 규칙은 “Sub-agent 호출 전에는 한 글자도 출력하지 않는다”입니다. Sub-agent 응답은 요약하거나 덧붙이지 않고 원문 그대로 내보냅니다. “분석하겠습니다” 같은 도입 멘트가 화면에 먼저 찍히는 걸 막으려고 넣었습니다.

③ 프론트와 BFF는 한 Task의 sidecar: ALB는 nginx 80만 보고, BFF는 localhost:8080으로만 붙습니다. 배포 단위는 Task Definition revision 하나입니다.

④ 운영이 바꿀 값은 DB에: 화면 단계별 프롬프트는 ai_prompt, 에이전트별 모델은 ai_agent_model_config, LLM 프로바이더(Bedrock/Gemini)는 ai_model_config에 둡니다. 프롬프트는 BFF와 AgentCore 양쪽에서 5분 캐시라, DB만 고치면 재배포 없이 반영됩니다.


3.추천 숫자는 코드가 정한다

클럽 TOP 3는 고객사 API가 주고, 브랜드 매칭과 골프공 추천은 기존 기준표를 옮겨 둔 계산식이 정합니다. LLM에게 순위를 묻는 곳은 없습니다.

브랜드 매칭: 더하지 않고 순서로 줄 세운다

브랜드를 고른 뒤의 추천은 BFF 안의 매칭 엔진이 맡습니다. 헤드스피드(m/s)로 최적 론치각과 드라이버 CPM을 찾는 기준표가 코드 상수로 들어 있고, 고객 값을 이 기준표에 대어 요인별 점수를 냅니다.

점수를 더하지 않고 정렬 키로 이어 붙인 게 핵심입니다. 요인별 점수를 3자리 숫자로 만들어 우선순위 순서대로 붙이고, 문자열 내림차순으로 정렬합니다.

이렇게 하면 성별·연령 적합도가 한 단계라도 높은 제품은, 뒤 요인 점수가 아무리 높은 제품보다 항상 앞에 섭니다. 뒤 요인은 앞 요인이 모두 같을 때만 순서를 가리고, 마지막 5자리는 원본 행 순서로 동점을 끊습니다. 제품 데이터는 정합성을 위해 캐시 없이 매 요청 DB에서 읽습니다.

골프공: 엑셀 수식을 옮기고 엑셀 결과와 대조했다

골프공 추천 기준은 고객사가 엑셀로 관리하던 볼 데이터 시트였습니다.

추천은 세 단계입니다.

  1. 필터: 커버 소재(우레탄 / 아이오노머)로 먼저 거르고, 가격대(우레탄 50,000원, 아이오노머 20,000원 기준)와 색상(화이트 / 컬러)으로 좁힙니다.
  2. 점수: 퍼포먼스·스핀·탄도·피스·컴프레션 답변마다 제품 속성과 대어 점수를 더하고, 브랜드·색상 가산점을 붙입니다.
  3. 정렬: 총점 내림차순, 동점이면 최신 출시연도. 브랜드를 고르지 않았으면 같은 브랜드는 한 개만 남깁니다.

퍼포먼스 질문 하나만 봐도 같은 답이라도 커버 소재에 따라 점수표가 다릅니다. 엑셀 수식에서 그대로 옮긴 값입니다.

답변우레탄 (드라이버 스핀 Low / Mid / High)아이오노머 (Low / Mid / High)
비거리20 / 10 / 230 / 10 / 5
방향성10 / 20 / 210 / 5 / 30
밸런스10 / 25 / 1010 / 30 / 10

옮긴 뒤에는 답변 조합별로 DB 결과와 엑셀 기대 결과를 함께 출력하는 시뮬레이션 스크립트로 대조했습니다. 대조 기준은 원본 엑셀의 수식과 매칭 로그 시트입니다.

추천 뒤 이어지는 질문(“A 모델이랑 B 모델 차이가 뭐야”)은 Q&A 지식베이스 100건(JSON)과 점수 추천 tool을 함께 씁니다. 질문을 데이터 직접 비교 / 데이터 + 해석 / 일반 지식 세 가지로 먼저 나누고, DB에 없는 모델은 추정하지 말고 “현재 데이터에서 확인이 어렵습니다”라고 답하게 했습니다.

에이전트 16개와 프롬프트 두 층

Coordinator 1개가 Sub-agent 15개를 tool로 들고 있는 Agents-as-Tools 구조입니다. Sub-agent는 @tool 데코레이터로 등록되고, Coordinator는 각 tool의 docstring을 보고 고릅니다.

GatewaySub-agent (Claude Haiku 4.5)동시 호출
fitting피팅·스윙 분석 · 드라이버 추천 · 아이언 추천 · 경기 분석 · 골프 성향 유형 · 제품 사양 · 종합 클럽 추천드라이버 + 아이언, 사양 + 리뷰
product리뷰 요약리뷰 + 사양
fitting + product브랜드 추천 · 재고 · 가격 · 프로모션재고 + 가격, 가격 + 프로모션
session일반 골프 질문—
없음위젯·차트 데이터 변환—
A2A골프공 추천 → recommender Runtime—

화면에서 나가는 호출은 고르지 않고 지정한다

채팅 입력은 Coordinator가 의도를 보고 고르지만, 화면 단계에서 자동으로 나가는 호출은 고를 필요가 없습니다. 프론트가 형태로 보내면 Coordinator는 의도 분석 없이 해당 Sub-agent를 바로 부릅니다. 이런 키가 10개이고, JSON 내용은 사용자에게 노출하지 않습니다.

프롬프트를 코드와 DB로 나눈 이유

층저장 위치담는 것바꾸는 방법
시스템 프롬프트코드 (agents/*.py)역할 · 보안 · tool 호출 순서 · 단위 정책AgentCore 재배포
DB 프롬프트ai_prompt 테이블, 13건입력 데이터 구조 · 출력 형식 · 변수DB UPDATE, 5분 캐시 후 반영

DB 프롬프트의 constraints(JSONB)는 BFF와 AgentCore가 읽어 템플릿 앞에 지시문을 붙입니다. 예를 들어 {“max_sentences”: 4, “tool_call”: false}는 [4문장 이내] [tool 호출 금지, 주어진 데이터만으로 답변]이 됩니다.

[ 프롬프트 치환 흐름 · tool_call 값에 따라 두 갈래 ]

true는 BFF가 문장을 완성해 넘기고, false는 AgentCore의 Sub-agent가 DB에서 직접 읽어 채웁니다.

tool_call: false는 BFF가 카드 API 결과를 변수로 직접 넣는 경우에 씁니다. 화면 카드와 설명 문장이 같은 데이터를 보게 하려는 장치이고, Sub-agent 프롬프트에는 “[tool 호출 금지] 접두어를 받으면 모든 tool을 멈추고 주어진 데이터만으로 답한다”는 탈출구를 넣었습니다.

골프공은 Runtime을 따로 뒀다

골프공 추천은 메인 Coordinator 안에 넣지 않고 별도 AgentCore Runtime(recommender)으로 뺐습니다. 메인 쪽 ball_recommendation_agent는 자체 LLM 처리 없이 invoke_remote_runtime으로 recommender를 부르기만 합니다.

  • 세션 공유: Coordinator의 session_id를 A2A 호출에 그대로 넘겨서 두 Runtime이 같은 STM을 씁니다.
  • 도메인 제약: 골프공 Coordinator는 골프공 질문만 받고, 클럽·피팅 질문은 정해진 문구로 거절합니다.
  • 프롬프트 캐싱: 골프공 쪽 프롬프트는 DB를 쓰지 않고 코드에 두는 대신 cachePoint로 Bedrock 프롬프트 캐싱을 걸었습니다.

모델은 DB 값 하나로 바꾼다

기본은 Coordinator에 Claude Sonnet 4.5, Sub-agent에 Claude Haiku 4.5입니다. ai_agent_model_config 테이블에서 에이전트별로 Sonnet 4.6, Opus 4.5/4.6, Gemini 2.5 Pro/Flash로 바꿀 수 있고, 첫 요청에 DB 설정을 전체 동기화한 뒤에는 변경이 감지될 때만 다시 읽습니다. Coordinator 인스턴스는 {session_id}:{provider} 키로 최대 100개까지 LRU 캐시합니다.


4.운영에 올리고 부딪힌 것들

운영 문서에 남긴 장애 유형은 대부분 모델이 아니라 배포 설정에서 나왔습니다. 그중 가장 위험한 건 에러 없이 “정상처럼” 뜨는 경우였습니다.

증상원인해결
채팅은 되는데 클럽 추천·피팅 데이터가 Mock이거나 비어 있음Gateway URL 환경변수 3개 중 하나라도 빠지면 AgentCore가 tools=[]로 기동배포 스크립트가 필수 환경변수 14개와 __FILL_ME__ 자리표시자를 검사해 배포 차단
Lambda에 tool을 추가했는데 Agent가 못 씀Lambda 재배포만으로는 Gateway toolSchema가 갱신되지 않음update-gateway-target으로 스키마 재등록. 배포 순서 고정 (아래)
배포 직후 전체 서비스 장애agentcore deploy 후 Runtime ARN이 바뀔 수 있고, BFF는 옛 ARN을 부름배포 후 –status로 ARN 확인, 바뀌면 프론트 env 갱신 후 재배포
수동 설정 변경 후 환경변수 전부 사라짐update-agent-runtime에 –environment-variables를 빼면 기존 값이 전부 삭제됨배포 스크립트가 lifecycle 설정(idle 1800초 · 최대 28800초)과 함께 환경변수 전체를 재적용
첫 호출에서 424 Runtime initialization time exceededcold start 중 OTEL 초기화ADOT 자동 계측 비활성화 + BFF가 이 오류일 때만 5초 간격 최대 5회 재시도
로컬 개발에서 첫 invoke 뒤 멈춤Gateway URL을 넣으면 MCP 세션이 시작됨로컬은 Gateway URL을 주석 처리하고 mock fallback 사용
골프공 Gateway 호출이 간헐적으로 실패MCP client 세션 만료, cold start 시 Gateway 미준비client TTL 30분 재생성, 실패 시 client 리셋 후 다음 호출에서 지연 초기화

배포 순서를 문서 맨 위에 박았다

신규 tool이 들어가는 배포는 순서가 하나라도 어긋나면 tools=[]로 뜹니다. 인계 문서 포털 첫 화면의 “필수 주의사항” 첫 줄이 이 순서입니다.

  1. Lambda 배포
  2. Gateway schema 등록 (update-gateway-target)
  3. AgentCore Runtime 배포 → ARN 변경 여부 확인
  4. BFF + 프론트 배포 (운영은 VITE_TEMP_LOGIN=false 확인)

배포 직후에는 CloudWatch에서 MCPClient 초기화 로그를 확인하는 걸 체크리스트로 넣었습니다. AgentCore에는 이전 버전 자동 롤백이 없어서, 롤백은 이전 커밋의 backend/src/를 체크아웃해 다시 배포하는 방식입니다.

같은 점수 로직이 두 벌 있었다

골프공 점수 로직은 golfball_tools/golfball_tools.py에 먼저 만들고, Lambda(lambda/golfball/lambda_handler.py)로 이식했습니다. 이식한 파일 머리에도 그렇게 적혀 있습니다.

2026-05-19 커밋에서 “브랜드 미선택 시 추천 4개 → 3개”를 고쳤는데, 수정은 golfball_tools.py 쪽에만 들어갔습니다. 인계 시점 Lambda 코드에는 브랜드 중복 제거 루프가 여전히 4개에서 멈춥니다. 같은 규칙을 두 곳에 두면 한쪽만 고쳐지는 일이 생깁니다.

DB 커넥션은 계산해서 정했다

위치드라이버min / max근거
BFFasyncpg2 / 12운영 최대 20 Task × 4 workers = 80 프로세스. 기동 시 160, 피크 960 연결 (Aurora max_connections 1,802의 53%)
AgentCoreasyncpg1 / 3microVM 1개 안에서 tool은 순차 호출이라 동시 점유 최대 1. max 3은 재시도 여유
Lambdapsycopg21 / 1warm 인스턴스당 연결 1개 유지

운영 리소스 (인계 시점)

항목값
AgentCore Runtimecoordinator · recommender, 둘 다 ACTIVE
MCP Gateway → Lambda4개 (fitting · product · session · golfball)
ECS Fargate서비스 5개 · 태스크 10개 (프론트+BFF 2048 CPU / 4096 MiB × 2)
Aurora PostgreSQL17.7, db.r8g.large, Multi-AZ 아님
배포 후 정상 기준Runtime ACTIVE, warmup 로그 warmup_agent_ok 15건 · skip 0건

103일 동안 있었던 일

날짜일어난 일
2025-11-11PoC v1.0.0. Supervisor + 4개 에이전트, 로컬 PostgreSQL, Streamlit
2025-11-17PoC v1.1.0. React 프론트 추가
2026-02-09구축 착수
2026-03-13골프공 데이터 V3. 통일 상품명·색상 가산점·출시연도 컬럼 추가
2026-04-12브랜드 설문 프롬프트 tool_call true → false
2026-04-28운영 AgentCore Runtime Role에 DB 시크릿 접근 권한 추가
2026-05-04배포 절차·로그 진단 문서 작성
2026-05-08골프공·상품검색 Backend 인수인계 문서 작성
2026-05-19골프공 Backend ECS 배포 수정 (응답 필드명, 추천 개수 4 → 3)
2026-05-22구축 종료

잘한 결정

  • 고객사 기준을 다시 만들지 않았다: 클럽 TOP 3는 고객사 피팅 API를 그대로 쓰고, 브랜드 매칭과 골프공 점수는 기존 기준표·엑셀 수식을 코드로 옮겼습니다. 추천 결과에 대한 설명 책임이 고객사 기준에 남습니다.
  • 엑셀과 대조하는 시뮬레이션을 만들었다: 수식을 옮긴 뒤 답변 조합별로 엑셀 기대 결과와 DB 결과를 같이 찍어봤습니다. 엑셀이 세 버전으로 바뀌는 동안 테이블 교체 스크립트로 따라갔습니다.
  • tool 호출을 줄였다: 화면 단계 프롬프트 13개 중 9개를 tool_call: false로 두고 BFF가 데이터를 넣었습니다. 카드와 설명이 같은 데이터를 봅니다.
  • 운영 값을 DB로 뺐다: 프롬프트는 5분 캐시 후 반영, 모델은 에이전트별로 재배포 없이 바꿉니다.
  • 배포 스크립트가 사람 실수를 막는다: 필수 환경변수 14개 검증, 자리표시자 차단, 환경변수 재적용을 스크립트에 넣었습니다.

아쉬운 결정

  • 측정을 남기지 않았다: 응답 지연과 추천 적합도를 회차별로 재서 남겼어야 했습니다. “인계 시점 상태”에 응답 지연과 적합도 숫자를 넣지 못한 이유입니다.
  • 같은 로직이 두 벌: 골프공 점수가 golfball_tools.py와 Lambda 두 곳에 있어서, 추천 개수 수정이 한쪽에만 들어갔습니다.
  • 프롬프트 관리가 두 방식: 메인 피팅은 DB 프롬프트(버전 컬럼, 5분 캐시), 골프공은 코드 하드코딩(재배포 필요)입니다. 운영자가 두 방식을 따로 알아야 합니다.

처음부터 다시 만든다면

처음에 한 것다시 한다면
점수 로직을 tool 모듈에 만들고 Lambda로 복사처음부터 Lambda 한 곳에만 두고 로컬은 같은 함수를 import
프롬프트를 시스템별로 다른 방식으로 관리골프공도 ai_prompt 테이블로 통일
배포 후 확인은 로그를 눈으로tools=[], ARN 변경, warmup 실패를 배포 스크립트가 직접 검사하고 실패 처리
품질 확인 기록을 따로 남기지 않음화면 단계별 고정 시나리오 세트로 지연·적합도를 회차마다 기록
Sub-agent를 기능 단위로 15개화면에서 실제로 호출되는 단위부터 만들고 채팅 전용은 뒤로

인계하며 남긴 숙제

  • Aurora Multi-AZ 전환 검토 (인계 시점 단일 인스턴스)

5.사용 도구

영역사용한 것
FrontendReact 19 · Vite 6 · Tailwind CSS · nginx
BFFPython · Starlette · asyncpg · httpx · boto3
AgentStrands Agents · Amazon Bedrock AgentCore Runtime (direct_code_deploy, Python 3.10, VPC) / Gateway(MCP) / Memory(STM)
ToolAWS Lambda (python3.11) × 4
LLMClaude Sonnet 4.5 (Coordinator) · Claude Haiku 4.5 (Sub-agent) · 대체 Gemini 2.5 Pro / Flash · 상품검색 Claude 3 Haiku
데이터Aurora PostgreSQL 17.7 · S3 · Secrets Manager
관측CloudWatch Logs · CloudTrail
개발 도구Kiro (spec: requirements · design · tasks) · Claude Code · Playwright MCP

MegazoneCloud 소속 AI Architect 2명이 투입되어 고객사 AI Fitting 구축 프로젝트를 수행하며 작성한 내용입니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다