최종 5인 팀 프로젝트
Mealplanning
식비 예산과 행동 데이터를 연결한 식생활 추천 서비스
LLM 비교 평가·모델 선정, Guardrail 로직 개선, 추천 노출·행동 이벤트 데이터 흐름 분석, AWS 서버리스 AI 기능 구성·검증을 맡았습니다. 아래에는 제가 직접 판단하고 확인한 과정을 근거와 함께 정리했습니다.
프로젝트 개요
누적 1,750건 평가 기록 · 품질·비용·응답 지연·Guardrail 4축
같은 275건으로 재측정 · 추가 LLM 호출 없음
조회한 44건이 전부 NULL · 전달 경로 추적
팀 환경과 개인 기여를 구분했습니다. EKS, Argo CD, Prometheus/Grafana 등은 팀 실행 환경이며 개인이 구축·운영한 범위가 아닙니다.
주요 실측 및 문제 해결 과정
프로젝트에서 담당한 작업과 주요 판단 과정을 실제 측정 결과와 함께 정리했습니다. 각 항목에서 문제를 확인한 과정, 선택 기준, 결과와 한계를 확인할 수 있습니다.
일부 스크립트·코드 작성과 반복 실행에는 AI 도구를 활용했으며, 비교 기준 설정·실험 조건 결정·결과 해석·채택과 되돌림 판단은 직접 수행했습니다.
A 구성과 아키텍처서비스가 어떤 문제를 다루는지와, 제가 맡은 기능을 어디까지 서버리스로 옮겼는지 3개 항목
A-1이 서비스는 어떤 문제를 해결하려 했나팀이 함께 만든 서비스 흐름과 그 안에서 제 기여 범위
문제 상황
식비를 관리하려면 «무엇을 먹을지»와 «얼마를 쓸지»를 따로 계산해야 했습니다. 레시피를 고르는 화면과 장을 보는 화면, 지출을 적는 화면이 분리돼 있으면 사용자가 같은 정보를 두 번 입력하게 됩니다.
서비스가 동작하는 방식
월 식비 예산을 먼저 설정하면 그 값이 레시피 추천·장바구니 예상 결제액·월별 잔액 계산의 공통 기준으로 쓰이도록 구성됐습니다. 영수증을 찍으면 품목과 금액이 추출돼 냉장고 재고와 지출 내역에 함께 반영되고, 보유 재료는 다시 추천과 장바구니 자동 제외 조건으로 이어집니다.
팀 공동 기능과 제 기여
- 팀 공동 구현 — 온보딩·예산 설정, 냉장고 재고, 레시피 검색·레시피북, 개인 레시피 작성. 서비스 흐름으로 설명하며 제 단독 구현으로 적지 않습니다.
- 제 기여가 뚜렷한 부분 — 영수증 OCR과 YouTube 영상 레시피 추출의 서비스 코드, 그리고 챗봇 응답 경로.
- 팀 실행 환경 — Kubernetes 기반 온프렘·AWS 환경, 데이터 저장소, Cache, CI/CD, Observability. 개발은 이 환경 위에서 했지만 구축·운영은 제 범위가 아닙니다.
A-2서버리스로 무엇을 옮기고 무엇을 남겼나검토 기준을 먼저 정하고, 기준에 걸린 기능은 옮기지 않았습니다
문제 상황
AI 기능이 상시 떠 있는 워크로드로 돌면 쓰지 않는 시간에도 자원을 붙잡습니다. 호출이 몰리지 않는 기능부터 이벤트 기반으로 옮길 수 있는지 검토했습니다.
확인 및 분석
«옮길 수 있는가»를 감으로 정하지 않으려고 검토 기준 5개를 먼저 정하고, 하나라도 걸리면 후보에서 빼는 방식으로 12종을 전부 확인했습니다.
가설 및 선택지
- 전부 옮긴다 — 관리 지점은 줄지만 걸리는 기능에서 되돌리기 비용이 커집니다.
- 기준을 세워 통과한 것만 옮긴다.
- 옮기지 않는다 — 상시 자원 점유가 계속됩니다.
수행 및 결정
두 번째를 택했습니다. 12종 중 10종을 채택하고 2종은 제외했습니다. Terraform(AWS)으로 Lambda·SQS·SNS·S3·EventBridge Scheduler 구성을 작성하고, 함수가 무엇에 깨어나고 실패하면 어디로 가는지를 트리거와 실패 경로로 먼저 정리했습니다.
결과
이벤트 기반으로 옮긴 기능은 호출이 있을 때만 실행되도록 구성됐고, 제외한 2종은 근거를 남긴 채 기존 방식으로 유지했습니다.
여기서 얻은 작은 교훈 — 테스트가 있다는 것과 실행된다는 것은 다릅니다
서버리스 테스트 파일이 저장소에 있는데도 CI 설정에는 해당 실행 경로가 들어 있지 않아 한 번도 돌지 않고 있었습니다. 파일 존재가 아니라 «파이프라인이 실제로 무슨 명령을 실행하는지»를 봐야 한다는 것을 확인하고 실행 경로에 편입했습니다. 같은 변경에서 제 스모크 테스트 자체의 오류도 발견해 고쳤습니다 — 잘못된 필드명을 보냈는데 200이 돌아와 통과처럼 보이던 경우였습니다.
CI/CD를 구축하거나 운영한 것이 아니라, 기존 검증 과정의 누락을 찾아 보완한 범위입니다.
A-3레시피 후기는 어떻게 모으고 붙였나후기 141,298건을 수집·분류하고 전 레시피에 요약을 연결한 과정
문제 상황
레시피마다 후기가 쌓여 있어도 사용자가 전부 읽지는 않습니다. 후기를 감정으로 분류하고 요약해 붙이려 했는데, 자유 요약은 근거를 다시 쓰는 작업과 성격이 달라 어떤 모델이 맞는지 미리 정할 수 없었습니다.
확인 및 분석
후보 5종을 같은 케이스로 재봤습니다. 판정을 가른 것은 문장 품질이 아니라 존댓말 지시를 지키는지였습니다. 지시를 무시하는 모델은 요약이 그럴듯해도 서비스 말투와 어긋났습니다.
스키마를 실제 DB에 적용하기 전에는 설계 전제가 실 DB와 어긋나지 않는지 먼저 점검했습니다. 그 과정에서 가격 이상탐지의 기준선이 아직 짧고, 두 소매 가격을 합치면 편차가 부풀어 탐지가 죽는다는 것을 확인했습니다.
수행 및 결정
부정 후기가 있는 레시피만 LLM으로 요약하고 나머지는 템플릿으로 처리했습니다. 가격 기준선은 소매별로 나누고, 이력이 짧은 구간의 급락은 기록만 하고 알리지 않도록 게이트를 뒀습니다.
결과
후기 141,298건을 수집하고, 이 중 141,282건을 감정 분류한 뒤 레시피 6,873개 전부에 요약 결과를 연결했습니다.
초기 비용 추정 36,291원을 기준으로 LLM 적용 대상과 입력량을 조정했고, 예상 비용을 약 2.1~2.2만원 수준으로 낮출 수 있는 구조를 확인했습니다.
B 결정어떤 기준으로 기술과 모델을 선택했는지 2개 항목
B-1어떤 기준으로 모델을 골랐나후보 15종 · 누적 1,750건 평가 기록 · 판단을 5번 바꿨습니다
문제 상황
관리형 모델로 옮길 수 있게 되면서 «코드 구조상 옮길 수 있는가»만 보고 이관 가능으로 적었습니다. 그런데 구조상 가능한 것과 품질이 유지되는 것은 다른 문제였습니다.
확인 및 분석
구조상 가능 여부만으로 판단하지 않고 동일 조건에서 실제 응답을 비교했습니다. 처음에는 응답 품질만 봤지만, 서비스에 올리려면 비용·응답 지연·Guardrail 통과 여부를 같이 봐야 한다는 것이 드러났습니다. 응답이 좋아도 검증 단계에서 막히는 모델이 있었기 때문입니다.
가설 및 선택지
- 값이 싼 모델로 교체한다.
- 기존 모델을 유지한다.
- 기능별로 나눠 배치한다.
수행 및 결정
저비용 모델이 기존 모델과 만점 동률로 나왔을 때, 모델이 좋아진 게 아니라 테스트가 쉬웠을 수 있다고 보고 더 어려운 케이스 세트를 새로 만들어 다시 쟀습니다. 그러자 갈렸습니다 — 환각 안전성은 모든 모델이 같았고 차이는 정보 완전성이었습니다. 저비용 모델은 답변에서 금액을 통째로 빠뜨렸습니다.
비용 43%·속도 2배 이점보다 식비 앱에서 금액이 안 보이는 손실이 더 크다고 판단해 즉시 교체를 보류했습니다. 이후 실패 원인을 프롬프트로 제거한 뒤, 실제 서비스에서 다듬는 경로에서 동률을 확인하고 조건이 충족된 기능만 옮겼습니다.
결과
후보 15종을 같은 기준으로 재측정해 누적 1,750건의 평가 기록을 남겼고, 새 측정이 나올 때마다 권고를 5번 바꿨습니다. 최종 결과만 남기지 않고 그때그때의 비교표와 판단 변경 시점을 함께 보존했습니다.
또한 입력 케이스풀이 이후 소실돼 완전히 동일한 재현은 불가능합니다. 채점 스크립트와 결과 문서는 남아 있습니다.
배운 점 / 다음 판단
평가 결과가 포화될 때는 모델 차이보다 평가 케이스의 난이도를 먼저 점검해야 한다는 점을 배웠습니다.
B-2영수증 OCR은 어떤 기준으로 골랐나실물 13장 · 5개 방식 · 성공 기준을 먼저 정의했습니다
문제 상황
영수증을 찍어 품목과 금액을 자동으로 넣으려면 감열지 인쇄와 구겨진 종이에서도 구조화된 결과가 나와야 했습니다.
확인 및 분석
실물 영수증 13장을 같은 조건에서 5개 방식에 통과시켰습니다. 다운스케일, 재시도, 타임아웃을 모두 고정했습니다.
가설 및 선택지
- 로컬 OCR 엔진 — 외부 API 호출 비용 없음, 오프라인 가능.
- 비전 모델 — 구조화 출력을 바로 받을 수 있음.
- 추론 강화 옵션을 켠 상위 모델 — 정확할 것이다.
수행 및 결정
성공을 무엇으로 셀지부터 정했습니다. 합계 정합 — 추출한 품목 금액의 합이 영수증 총액과 맞으면 성공입니다. 그리고 이 지표의 한계를 같이 적었습니다. 품목이 한두 개인 영수증은 쉽게 통과하고, 총액을 못 읽으면 품목이 맞아도 실패로 처리됩니다. 즉 절대 수치는 무르고 상대 순위만 견고합니다.
경량 비전 모델은 13장 중 12장에서 합계 정합을 확인했고, 실행 오류는 없었습니다. 상위 모델은 추론 옵션을 켜도 더 낫지 않으면서 비용이 수십 배였고, 로컬 엔진은 구조화 출력 자체가 나오지 않았습니다.
결과
경량 비전 모델을 채택했습니다. 사용자가 결과를 확인·수정하는 단계를 앞에 둬서 인식 오류를 사람이 보정할 수 있게 했습니다.
배운 점 / 다음 판단
지표를 먼저 정의하고 그 한계까지 적어 두면, 나중에 숫자를 과장해서 읽지 않게 됩니다.
C 검증과 개선바꾼 것이 정말 나아졌는지 같은 기준으로 다시 쟀습니다 4개 항목
C-1정상 응답이 왜 막혔고, 어떻게 줄였나같은 275건에서 통과 166건 → 247건 · 추가 호출 없음
문제 상황
응답 내용은 문제가 없는데 검증 단계에서 막히는 경우가 반복됐습니다. 막히면 템플릿 답변으로 대체되므로, 유료 호출을 하고 결과를 버리는 셈이었습니다.
확인 및 분석
실패한 케이스를 모아 다시 봤더니 상당수가 환각이 아니었습니다. 사용자가 질문에 쓴 재료와 금액을 그대로 되풀이한 것을 검증 로직이 «근거 없는 내용»으로 오판하고 있었습니다.
가설 및 선택지
- 모델을 바꾼다 — 비용과 재검증이 따라옵니다.
- 검증 기준을 느슨하게 한다 — 진짜 환각까지 통과합니다.
- 근거로 인정하는 범위를 고친다 — 사용자 질문도 근거에 포함합니다.
수행 및 결정
세 번째를 택했습니다. 근거 문자열에 사용자 질문 원문과 한글 수사를 숫자로 편 정규화 결과를 함께 넣도록 처리 방식을 바꿨습니다. 「8천원」 같은 표현이 8000과 이어지도록 한 것입니다.
결과
같은 275건 평가 세트를 다시 돌려 통과가 166건에서 247건으로 늘었습니다(약 60% → 90%). 문자열 구성만 바꾼 변경이라 LLM 호출은 한 건도 늘지 않았습니다.
배운 점 / 다음 판단
«모델이 나쁘다»로 결론 내기 전에 판정 로직부터 확인하게 됐습니다. 이 건은 모델을 교체하거나 LLM 호출을 추가하지 않고 판정 로직 수정으로 통과 건수를 개선했습니다.
C-2한 번 좋아진 결과를 왜 되돌렸나9회차 반복 검증 · 회차당 30건 · 총 270 호출
문제 상황
챗봇이 주제에서 벗어난 질문에 답해 버리거나, 반대로 답해야 할 질문을 거절하는 경우가 있었습니다. 판정 규칙을 고쳐 가며 반복해서 쟀습니다.
확인 및 분석
30건 스위트로 회차를 돌렸습니다. 초반 개선으로 점수가 올랐지만, 특정 회차에서 넣은 경계 매칭 규칙이 복합 요리명을 놓치게 만든다는 것을 확인했습니다. 「갈비찜」이 「매콤돼지갈비찜」 안에 들어 있는 경우를 못 잡는 식이었습니다.
가설 및 선택지
- 점수가 올랐으니 그대로 둔다.
- 되돌린다 — 오매칭이 다시 생길 수 있습니다.
- 예외 목록을 덧붙인다 — 규칙이 계속 늘어납니다.
수행 및 결정
되돌렸습니다. 복합 요리명을 놓치는 손실이 오매칭을 막는 이득보다 컸고, 원래 막으려던 주제 이탈은 앞단의 다른 장치가 이미 걸러 주고 있었기 때문입니다.
그리고 좋은 점수가 나온 뒤 바로 다음 단계로 가지 않고, 학습에 쓰지 않은 seed 3개로 같은 코드를 다시 돌려 특정 케이스에만 맞춘 개선이 아닌지 확인했습니다.
결과
되돌린 뒤에도 점수가 유지됐고, 서로 다른 seed에서도 비슷한 수준이 나왔습니다. 이후 생성 백엔드를 붙일 때는 «판정 결과가 같아야 한다»를 가설로 먼저 적고 확인했습니다.
배운 점 / 다음 판단
점수가 오른 변경도 조건이 한정되면 손해일 수 있다는 것, 그리고 좋은 결과일수록 다른 조건에서 먼저 확인해야 한다는 것을 기준으로 삼게 됐습니다.
C-32,276회 비교에서 무엇을 확인했나1,138건 × 2라운드 · 나빠진 항목도 함께 남겼습니다
문제 상황
소규모 스위트로는 잡히지 않는 문제가 있었습니다. 특히 재료가 많은 답변에서 품목이 잘리거나 금액이 빠지는 경우였습니다.
확인 및 분석
말투·난이도·품목 수를 조합해 1,138건을 만들고, 튜닝 전후로 두 번 돌려 2,276회를 비교했습니다. 생성은 seed를 고정해 결정적으로 맞췄습니다. 1차 계측에서 다품목 이름 잘림, 질문 문장을 요리 이름처럼 인용하는 동작, 그리고 채점기 자체의 오탐까지 드러났습니다.
가설 및 선택지
- 모델을 바꾼다.
- 프롬프트에서 실패 원인을 직접 금지한다.
- 후처리로 걸러 낸다.
수행 및 결정
프롬프트에서 실패 원인을 직접 막았습니다. 여러 줄 근거가 오면 모든 줄의 재료와 금액을 답변에 포함하도록 하고, 질문 문장을 요리 이름처럼 인용하는 것을 금지했습니다. 그리고 전체 평균이 아니라 실제 서비스에서 다듬는 경로를 기준으로 판단했습니다.
결과
실제 서비스 경로의 치명 결함이 88건에서 21건으로 줄었습니다(720건 기준 12.2% → 2.9%). 전체 1,138건 기준으로는 9.9% → 4.6%입니다.
배운 점 / 다음 판단
전체 평균은 실제 사용자가 겪는 것과 다를 수 있습니다. 그래서 어느 경로가 실제로 그 코드를 지나가는지 먼저 나눠서 보게 됐습니다.
C-4CRF 경량화는 어떻게 검증했나예측이 한 건도 달라지지 않는 것을 먼저 확인했습니다
문제 상황
재료 인식 모델을 서빙할 때 학습용 라이브러리가 통째로 따라왔습니다. 추론에는 쓰이지 않는데도 배포 패키지 대부분을 차지했습니다.
확인 및 분석
패키지 크기를 뜯어 보니 수치 연산·머신러닝 라이브러리 세 개가 대부분이었습니다. 모델을 학습 객체가 아니라 네이티브 형식으로 저장하면 추론 전용 경량 라이브러리만으로 열 수 있었습니다.
가설 및 선택지
- 그대로 둔다 — 배포 방식에 제약이 남습니다.
- 포맷을 바꾼다 — 결과가 달라질 위험이 있습니다.
수행 및 결정
경량화 이후에도 동일 모델로 볼 수 있는지를 확인하기 위해, 채택 기준을 크기 감소가 아니라 예측 결과의 완전 일치로 정했습니다.
사람이 검수한 정답 50건으로 두 경로의 예측을 전수 비교했고, 라벨과 추출 결과가 50건 모두 일치하는 것을 확인한 뒤에 반영했습니다. 학습은 기존 스택을 그대로 두고 서빙만 네이티브 포맷으로 바꿨습니다.
결과
배포 패키지가 크게 줄면서 성능 지표는 유지됐습니다. 부수적으로 알게 된 것도 있습니다 — 학습 객체를 그대로 불러오는 방식은 파일 내용에 따라 코드가 실행되는 경로라, 모델을 외부 저장소에서 받아 오는 구조에서는 그 자체가 위험 지점이 됩니다. 네이티브 포맷은 순수 데이터라 그 경로가 없습니다.
배운 점 / 다음 판단
«좋아 보이는 변경»일수록 채택 조건을 먼저 숫자로 못 박아 두면, 나중에 논쟁이 생기지 않습니다.
D 트러블슈팅과 신뢰성값을 직접 조회해 어느 구간에서 끊겼는지 확인한 기록 3개 항목
D-1학습 label이 안 만들어진 이유조회한 44건이 전부 비어 있었습니다
문제 상황
추천이 화면에 노출된 기록은 쌓이는데, 학습에 필요한 긍정 label이 한 건도 만들어지지 않았습니다.
확인 및 분석
모델을 다시 학습시키는 대신 학습 SQL의 조인 조건과 실제 저장된 이벤트를 대조했습니다. 노출과 행동을 잇는 키 값을 조회한 44건이 전부 비어 있었습니다.
가설 및 선택지
- 모델 문제 — 학습을 다시 돌린다.
- SQL 조인 조건 문제.
- 값이 애초에 전달되지 않는다.
수행 및 결정
세 번째로 좁히고 값을 따라 올라갔습니다. Frontend의 세션 생성 → API 요청 본문 → 이벤트 저장 → 학습 데이터 순서로 각 구간에서 값이 실려 있는지 확인했고, 요청에 필드가 실리지 않는 지점을 찾아 전달 경로를 고쳤습니다.
결과
끊겨 있던 노출과 행동의 연결이 복구돼, 이후 쌓이는 데이터부터는 label이 만들어질 수 있는 상태가 됐습니다.
규모를 주장하는 사례가 아닙니다. 조회한 44건이 전부 비어 있었다는 사실과 그 원인 경로만을 근거로 씁니다.
배운 점 / 다음 판단
«어느 기술이 고장났나»보다 «신호가 어느 구간에서 끊겼나»를 먼저 보게 됐습니다. AI 기능의 이상도 모델 내부 문제로 단정하면 원인을 놓칩니다.
D-2외부 모델 버전이 바뀌면 왜 문제가 생기나고칠 범위를 6개 컴포넌트로 넓힌 판단
문제 상황
모델을 «최신» 별칭으로 부르고 있었는데, 그 별칭이 상위 세대를 가리키게 되면서 기존에 쓰던 파라미터가 거부됐습니다. 해당 기능이 전량 실패했습니다.
확인 및 분석
실패한 기능은 하나였지만, 코드베이스를 훑어 보니 같은 방식으로 별칭을 쓰는 곳이 여러 군데였습니다. 즉 지금 조용한 곳도 다음 별칭 변경에서 같은 방식으로 깨질 수 있었습니다.
가설 및 선택지
- 실패한 기능만 고친다 — 가장 빠릅니다.
- 별칭을 쓰는 곳을 전부 찾아 고정한다.
- 예외 처리를 넣어 실패를 흡수한다 — 원인이 남습니다.
수행 및 결정
두 번째를 택했습니다. 실측으로 고른 모델과 런타임이 실제로 부르는 모델이 어긋날 수 있는 지점 전부를 범위로 잡고 6개 컴포넌트를 특정 버전으로 고정했습니다. 벤치마크로 고른 모델이 아닌 것이 돌면 그 벤치마크 결과 자체가 무효가 되기 때문입니다.
결과
운영 문서까지 포함해 12개 파일이 정렬됐고, 별칭 변경으로 계약이 바뀌는 경로가 제거됐습니다.
최초 장애 인지보다, 동일한 위험이 있는 호출 지점을 함께 확인하고 수정 범위를 정하는 작업을 맡았습니다.
배운 점 / 다음 판단
장애가 발생한 지점만 수정하지 않고, 동일한 실패 조건이 있는 경로까지 함께 확인하는 방식으로 범위를 넓혔습니다.
D-3Running인데 새 배포는 안 되던 이유프로젝트가 끝난 뒤 실행 환경을 점검하며 확인한 사례
문제 상황
워크로드는 정상으로 보이는데 새 배포만 실패했습니다. 겉으로는 아무 문제가 없어 보였습니다.
확인 및 분석
기존 Pod는 노드에 남아 있던 이미지로 계속 실행되고 있었습니다. 재시작 횟수도 0이었습니다. 반면 레지스트리 접근이 필요한 순간 — 즉 새 배포나 재스케줄링 — 에만 실패가 드러났습니다.
결과 / 배운 점
Pod가 Running 상태인 것과 새 배포가 가능한 상태는 다르다는 점을 확인했습니다. 현재 상태가 정상인 것과 다음 변경을 수행할 수 있는 것은 별개이고, 둘 다 운영 상태의 일부입니다. 주기적 점검이 실패 신호를 만들고 있었지만 그 실패 자체에는 알림이 걸려 있지 않아 아무도 모르고 있었습니다.