씨제이올리브영
경력근거: 현장의 문제를 직접 이해하고, AI 기반 솔루션을 설계부터 배포
올리브영 AI전략팀은 전사 AI Transformation 을 수행하는 사내 조직이고, 여기의 Forward Deployed AI Engineer 는 외부 고객사가 아니라 오프라인 매장부터 온라인 플랫폼까지 사내 현장의 문제를 직접 보고 설계부터 배포까지 주도합니다. 그 현장에서 가장 자주 오는 요청이 판매·재고를 묻는 한 문장이고, 지금은 그때마다 엔지니어가 SQL 을 대신 짜주느라 답이 늦습니다. 이 도구는 질문 문장을 읽기 전용 SELECT 로 옮겨 실행 전에 파서로 검사하고, 쓴 SQL 과 기간 해석·상품코드 매칭을 근거로 붙여 답을 냅니다. 정해지지 않는 질문은 추측하지 않고 후보를 되물어 어떤 질문까지 자동화가 되는지가 드러나므로, 자격요건의 데이터베이스 설계·활용과 주요업무의 AI 유즈케이스 백엔드가 한 산출물에서 만납니다. 이 포트폴리오가 보여주는 것은 스키마 카탈로그 기반 자연어 질의 변환과 규칙으로 실행을 가두는 설계 기술이며, 리테일·커머스 도메인 전문성이나 올리브영의 실제 지표 정의, 대용량 트래픽 운영 경험은 주장하지 않고 면접에서 솔직히 다룹니다.
요구사항을 받아 개발만 하지 않고 현장의 문제를 직접 보고 실제 동작하는 서비스로 만듭니다. 현업이 실제로 보낸 질문 문장을 입력으로 삼아 명령 하나로 답까지 잇는 이 도구가 그 일의 앞단을 정면으로 맡습니다.
AI API Gateway 같은 전사 AI 인프라를 세우고 유즈케이스의 백엔드를 설계합니다. 모델 호출을 게이트웨이 한 경로로 모으고 질문·생성 SQL·토큰 사용량을 감사 로그로 남기는 3주차가 그 축에 붙습니다.
자격요건의 데이터베이스 설계·활용과 우대사항의 리테일 커머스·LLM 서비스·Prompt Engineering 이 한 제품에 겹칩니다. 마트 스키마 카탈로그를 프롬프트 컨텍스트로 세우고 생성한 SQL 을 파서로 검사해 실행하는 구조가 그 겹침입니다.
사내 현업이 보낸 한국어 질문 한 줄을 인자로 받아, 판매·재고 마트의 스키마 카탈로그를 컨텍스트로 붙여 읽기 전용 SELECT 한 문장을 만들고, 실행 전에 SQL 파서로 AST 를 뜯어 SELECT 여부·허용 테이블·허용 컬럼·기간 조건·LIMIT 를 검사한 뒤 실행해 결과 표를 터미널에 냅니다. 표 위에는 쓴 SQL 전문과 상대 기간을 어떤 날짜 범위로 읽었는지, 상품 통칭을 어떤 상품코드로 맞췄는지, 조회 행 수가 얼마인지가 근거로 함께 붙습니다. 기간·지표·상품이 하나로 정해지지 않으면 추측해서 돌리지 않고 후보와 사유를 내며 멈추고, 허용 테이블 밖을 보려는 질문은 규칙이 실행 전에 막습니다. 채점은 사람이 손으로 단 기준 SQL 의 결과 집합과 행 단위로 대조해 결정되고, LLM 은 문장을 SQL 초안으로 옮기는 변환만 맡습니다. 화면은 만들지 않고 결과는 터미널 출력과 실행 로그 JSON 입니다.
올리브영 AI전략팀 Forward Deployed AI Engineer. 이 팀은 전사 AI Transformation 을 맡은 사내 조직이라 현장이 외부 고객사가 아니라 매장 운영팀·상품기획(MD)·물류 같은 사내 현업이고, 그 사람들이 보내는 '지난달 강남 상권 매장에서 선크림 얼마나 나갔나요' 같은 질문을 받아 매번 SQL 을 대신 짜주다 하루를 보내던 사람입니다. 질문 문장을 그대로 넣으면 검증된 읽기 전용 SQL 과 그 근거가 함께 나와, 현업 담당자 옆에 앉아 바로 돌려 보이며 어떤 질문까지 자동화가 되는지 확인하기를 바랍니다.
마트 스키마 카탈로그와 상품 마스터 별칭을 컨텍스트로 붙이면, LLM 이 (a) 현업의 한국어 질문을 실행 가능한 SELECT 한 문장으로 옮기고 (b) 기간·지표·상품이 하나로 정해지지 않을 때 추측하지 않고 후보를 되묻는 데까지 간다고 봅니다. 실행 허용 여부와 정답 판정은 LLM 이 아니라 파서 검사와 기준 SQL 결과 대조가 결정합니다.
질문 문장을 인자로 넣으면 스키마 카탈로그를 붙여 SELECT 한 문장을 만들고, 파서 검사를 통과한 것만 마트에 실행해 결과 표와 쓴 SQL·기간 해석·상품코드 매칭·행 수를 터미널에 내는 CLI입니다. 정해지지 않는 질문은 후보와 사유를 내며 멈추고, 실행 로그 JSON 한 줄이 남습니다. 화면은 만들지 않습니다. 데이터는 사내 마트에서 내려받았다고 가정한 합성 판매·재고 테이블과 상품 마스터로 시작하며, 사내 AI API Gateway 연동은 3주차 과제로 둡니다.
질문 60건을 기준 SQL 결과 집합과 행 단위로 대조해 일치율과 실행 차단 정확도를 재고, 자주 틀린 유형을 상대 기간·반품 제외·프로모션 중복처럼 묶어 스키마 카탈로그 설명과 프롬프트에 반영한 뒤 다시 잰 결과를 터미널 요약과 md 리포트로 냅니다.
모델 호출을 사내 AI API Gateway 를 거치도록 바꿔 질문·생성 SQL·조회 행 수·토큰 사용량을 감사 로그로 남기고, 컨테이너 이미지 한 장으로 AWS EKS 에 올려 같은 명령을 사내에서 실행하며 같은 질문·같은 기간의 결과는 Redis 캐시로 되받습니다.
질문 한 줄을 인자로 넣으면 몇 초 안에 생성된 SELECT 문과 해석한 날짜 범위, 매칭한 상품코드, 조회 행 수, 결과 표가 터미널에 함께 나오고 실행 로그 JSON 한 줄이 남는 걸 시연합니다
일부러 만든 위험·모호 질문 30종(직원 개인 실적 요구, 마트에 없는 지표, 통칭이 두 상품에 걸리는 문장, 기간이 없는 문장, 삭제·갱신을 요구하는 문장)을 넣어도 실행되지 않고 사유와 후보가 터미널에 나오는 걸 시연합니다
2주차 회귀에서 질문 60건 중 생성 SQL 의 결과 집합이 기준 SQL 결과와 완전히 일치한 게 49건(약 82%)입니다. 질문·마트·기준 SQL 을 전부 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건에서의 동작임을 명시합니다
2주차 회귀에서 실행하면 안 되는 질문 18건이 18건 모두 실행 전에 막혔습니다. 이 값은 파서 AST 검사와 테이블 화이트리스트 규칙이 결정하므로 모델 성능이 아니라 게이트 동작을 보여주는 수치입니다
진짜 성능 검증은 여기서 멈춥니다. 위 수치는 전부 작성자가 만든 더미 마트·더미 질문 자가채점이며, 실제 질의 정확도는 올리브영의 실 데이터 마트와 실제 현업이 보낸 질문, 현업 담당자의 확인이 있어야 측정 가능합니다. 이 도구는 그 검증을 스키마 카탈로그와 접속 정보만 갈아끼워 바로 돌릴 수 있는 상태로 만듭니다
이건 사내 데이터도 실제 현업 질문도 없이 혼자 만든 포트폴리오입니다. 모든 수치는 합성 마트·합성 질문 자가채점이라 성능 증명이 아닙니다. 면접에서는 데모 동작과 실검증 준비 상태로 솔직히 설명합니다
이 포트폴리오가 보여주는 것은 스키마 카탈로그를 컨텍스트로 세워 자연어를 질의로 옮기고, 생성한 SQL 을 파서로 검사해 실행 여부를 규칙이 결정하게 가두는 기술입니다. 리테일·커머스 도메인 전문성과 올리브영의 실제 지표 정의, 대용량 트래픽 운영 경험은 이 포트폴리오로 증명되지 않으므로 주장하지 않으며, 그 깊이는 면접에서 모르면 모른다고 말하는 편이 낫습니다
기준 SQL 을 작성자가 만들었으니 잘 맞는 게 당연합니다. 상대 기간 표현, 반품이 섞인 순판매, 프로모션 기간 중복, 통칭이 두 상품에 걸리는 문장, 마트에 없는 지표 같은 케이스를 질문 세트에 일부러 섞어 쉬운 질문만 통과시키는 게 아님을 보여줍니다
자연어 질의는 그럴듯하지만 틀린 SQL 이 가장 위험합니다. 조회는 읽기 전용으로만 열고 허용 테이블·컬럼을 화이트리스트로 묶으며, 기간 조건과 LIMIT 가 없으면 실행 자체를 막고, 결과 위에 쓴 SQL 전문을 항상 붙여 사람이 눈으로 다시 볼 수 있게 둡니다
새 지표 체계나 용어 사전을 제정하지 않습니다. 상품 통칭은 상품 마스터에 이미 적힌 별칭만 쓰고, 없거나 둘 이상 걸리면 스스로 정하지 않고 후보를 되물어 무엇을 더 받아야 하는지 드러냅니다. 직원 개인 실적처럼 정책상 열면 안 되는 질문은 허용 테이블에서 아예 빼는 방식으로 막습니다
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
매장 운영팀·상품기획·물류가 슬랙으로 보냈다고 가정한 한국어 질문 원문입니다. 도구의 입력이자 채점 기준이며, 질문마다 사람이 손으로 단 기준 SQL 과 기대 판정을 gold_ 필드로 심어 자가채점합니다. 상대 기간·통칭 충돌·마트에 없는 지표·정책상 열면 안 되는 요구를 일부러 섞었습니다.
| question_id | asked_by | text | asked_at | gold_status | gold_sql | gold_reason |
|---|---|---|---|---|---|---|
| Q001 | 매장운영팀 | 지난달 강남 상권 매장에서 선크림 카테고리 판매수량 상위 10개 상품… | 2026-07-02 | answered | SELECT p.product_id, p.display_name, S… | — |
| Q002 | 상품기획 | 최근에 리들샷 재고 어때요? | 2026-07-02 | needs_clarification | — | '최근' 이 며칠인지 정해지지 않아 날짜 범위를 고를 수 없습니다 |
| Q003 | 매장운영팀 | 6월 한 달 명동점 일자별 순판매 수량 알려주세요 | 2026-07-03 | answered | SELECT s.sale_date, SUM(s.qty_sold - s… | — |
| Q004 | 매장운영팀 | 6월에 선크림 제일 많이 나간 점포 어디예요? | 2026-07-03 | needs_clarification | — | '선크림' 이 상품 마스터 별칭에서 PRD-1001 과 PRD-100… |
| Q005 | 상품기획 | 6월 강남 상권 객단가 좀 뽑아주세요 | 2026-07-06 | needs_clarification | — | 객단가는 마트에 결제 건수 컬럼이 없어 계산되지 않습니다 |
{"type":"object","properties":{"question_id":{"type":"string"},"asked_by":{"type":"string"},"text":{"type":"string"},"asked_at":{"type":"string"},"gold_status":{"enum":["answered","needs_clarification","refused"]},"gold_sql":{"type":["string","null"]},"gold_reason":{"type":["string","null"]}},"required":["question_id","asked_by","text","gold_status"]} 형식으로 만들되 쉬운 질문만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라: 기간이 상대 표현이거나 아예 없는 문장, 상품을 통칭으로만 부르고 그 통칭이 두 상품에 걸리는 문장, 마트에 컬럼이 없어 계산되지 않는 지표 요구, 직원 개인 실적처럼 허용 테이블 밖을 보려는 요구, 삭제·갱신을 시키는 문장, 반품과 프로모션을 빼달라는 조건이 붙은 문장. gold_status 가 answered 인 행은 반드시 실행 가능한 SELECT 한 문장을 gold_sql 에 적고 LIMIT 를 붙여라. answered 와 needs_clarification 과 refused 를 대략 6:3:1 로 섞어라.{"type":"object","properties":{"question_id":{"type":"string"},"asked_by":{"type":"string"},"text":{"type":"string"},"asked_at":{"type":"string"},"gold_status":{"enum":["answered","needs_clarification","refused"]},"gold_sql":{"type":["string","null"]},"gold_reason":{"type":["string","null"]}},"required":["question_id","asked_by","text","gold_status"]}사내 운영 데이터 마트에서 내려받았다고 가정한 점포별 일자별 상품 판매·재고 추출본입니다. 생성한 SELECT 가 실제로 실행되는 대상이며, 반품이 섞인 순판매·집계 지연으로 비어 있는 재고·프로모션 중복 같은 실제 마트의 지저분함을 섞었습니다.
| store_id | store_name | trade_area | sale_date | product_id | qty_sold | qty_returned | stock_on_hand | promo_flag |
|---|---|---|---|---|---|---|---|---|
| ST-001 | 강남본점 | 강남 | 2026-06-01 | PRD-1001 | 42 | 1 | 118 | 0 |
| ST-001 | 강남본점 | 강남 | 2026-06-02 | PRD-1001 | 55 | 9 | 63 | 1 |
| ST-001 | 강남본점 | 강남 | 2026-06-03 | PRD-1002 | 12 | 0 | — | 0 |
| ST-002 | 명동점 | 명동 | 2026-06-01 | PRD-1001 | 31 | 0 | 77 | 0 |
| ST-002 | 명동점 | 명동 | 2026-06-05 | PRD-1003 | 0 | 4 | 21 | 0 |
{"type":"object","properties":{"store_id":{"type":"string"},"store_name":{"type":"string"},"trade_area":{"type":"string"},"sale_date":{"type":"string"},"product_id":{"type":"string"},"qty_sold":{"type":"integer"},"qty_returned":{"type":"integer"},"stock_on_hand":{"type":["integer","null"]},"promo_flag":{"type":"integer"}},"required":["store_id","store_name","trade_area","sale_date","product_id","qty_sold","qty_returned","promo_flag"]} 형식으로 2026-06-01 부터 2026-07-10 까지 점포 6곳·상품 12개로 채워라. 쉬운 데이터만 만들지 말 것. 다음을 의도적으로 섞어라: 반품이 판매보다 많아 순판매가 음수가 되는 날, 집계 지연으로 stock_on_hand 가 null 인 날, 재고가 0인 날, 휴점이라 행 자체가 없는 날, 같은 상품이 프로모션 기간에만 promo_flag 가 1인 구간, 신규 입고 전 판매가 0인 날.{"type":"object","properties":{"store_id":{"type":"string"},"store_name":{"type":"string"},"trade_area":{"type":"string"},"sale_date":{"type":"string"},"product_id":{"type":"string"},"qty_sold":{"type":"integer"},"qty_returned":{"type":"integer"},"stock_on_hand":{"type":["integer","null"]},"promo_flag":{"type":"integer"}},"required":["store_id","store_name","trade_area","sale_date","product_id","qty_sold","qty_returned","promo_flag"]}상품코드·표시명·브랜드·카테고리와 현업이 실제로 부르는 통칭(별칭)을 담은 상품 마스터입니다. 질문에 나온 통칭을 상품코드로 맞추는 근거이며, 별칭이 두 상품에 겹치거나 아예 없는 경우를 섞어 도구가 스스로 정하지 않고 되묻는지 확인합니다.
| product_id | display_name | brand | category | aliases | discontinued |
|---|---|---|---|---|---|
| PRD-1001 | 데일리 선에센스 50ml | A브랜드 | 선케어 | 선크림|선에센스 | 0 |
| PRD-1002 | 마일드 선스틱 (18g) | B브랜드 | 선케어 | 선크림|선스틱 | 0 |
| PRD-1003 | 리들샷 앰플 100 | C브랜드 | 스킨케어 | 리들샷 | 0 |
| PRD-1004 | 수분 토너 300ml | A브랜드 | 스킨케어 | — | 0 |
| PRD-1005 | 클렌징 오일 200 ml | B브랜드 | 클렌징 | 클렌징오일 | 0 |
{"type":"object","properties":{"product_id":{"type":"string"},"display_name":{"type":"string"},"brand":{"type":"string"},"category":{"type":"string"},"aliases":{"type":["string","null"]},"discontinued":{"type":"integer"}},"required":["product_id","display_name","brand","category","discontinued"]} 형식으로 상품 12개를 채워라. 쉬운 데이터만 만들지 말 것. 다음을 의도적으로 섞어라: 같은 별칭이 두 상품에 걸리는 경우, aliases 가 null 이라 통칭으로는 못 찾는 상품, 표시명에 용량 표기가 '200ml' 와 '200 ml' 로 흔들리는 경우, 구형과 신형이 같은 별칭을 쓰고 한쪽만 discontinued 가 1인 경우.{"type":"object","properties":{"product_id":{"type":"string"},"display_name":{"type":"string"},"brand":{"type":"string"},"category":{"type":"string"},"aliases":{"type":["string","null"]},"discontinued":{"type":"integer"}},"required":["product_id","display_name","brand","category","discontinued"]}| Python | 질문 파싱·SQL 생성·AST 검사·실행·터미널 근거 출력까지 CLI 본체를 씁니다 |
| Java | Spring Boot 로 사내 AI API Gateway 어댑터를 세워 모델 호출과 감사 로그를 한 경로로 모읍니다 |
| Kotlin | 마트 스키마 카탈로그와 허용 테이블·컬럼 목록을 읽어 내려주는 조회 모듈을 맡습니다 |
| Go | 질문 여러 건을 한 번에 돌릴 때 동시 실행 수와 질의 타임아웃을 제한하는 실행 러너를 씁니다 |
| MySQL | 판매·재고 마트와 상품 마스터를 담아 생성한 SELECT 가 실제로 실행되는 대상입니다 |
| Redis | 같은 질문·같은 기간의 결과를 캐시해 반복 실행과 모델 호출을 줄입니다 |
| Kubernetes(EKS) | 컨테이너 이미지 한 장으로 올려 사내에서 같은 명령을 실행합니다 |
✓ pass → 여기까지가 그 자체로 제출 가능한 완성품입니다. 더 키우려면 2주차(회귀 세트 채점)로 갑니다.
✓ pass → 여기서 멈춰도 완결입니다. 더 키우려면 3주차(사내 AI API Gateway 경유·EKS 실행)로 갑니다.
질문 한 건을 처리한 결과입니다. 생성한 SELECT 와 실행 여부, 해석한 날짜 범위, 통칭에서 맞춘 상품코드, 조회 행 수와 결과 미리보기를 담아 터미널 근거 출력과 실행 로그 JSON 에 그대로 씁니다. status 가 answered 가 아니면 sql 은 비어 있고 reason 에 사유와 후보가 들어갑니다.
{"type":"object","properties":{"question_id":{"type":"string"},"status":{"enum":["answered","needs_clarification","refused"]},"sql":{"type":["string","null"]},"tables_used":{"type":"array","items":{"type":"string"}},"period":{"type":["object","null"],"properties":{"from":{"type":"string"},"to":{"type":"string"}}},"matched_products":{"type":"array","items":{"type":"object","properties":{"term":{"type":"string"},"product_id":{"type":["string","null"]},"candidates":{"type":"array","items":{"type":"string"}}}}},"row_count":{"type":["integer","null"]},"preview":{"type":"array","items":{"type":"object"}},"reason":{"type":["string","null"]},"hedge":{"type":["string","null"]}},"required":["question_id","status","sql","tables_used","period","matched_products","row_count"]}[
{
"input_ref": "Q001 (상대 기간 + 카테고리 상위 10개)",
"output": {
"question_id": "Q001",
"status": "answered",
"sql": "SELECT p.product_id, p.display_name, SUM(s.qty_sold - s.qty_returned) AS net_qty FROM store_daily_sales s JOIN product_master p ON p.product_id = s.product_id WHERE s.sale_date BETWEEN '2026-06-01' AND '2026-06-30' AND s.trade_area = '강남' AND p.category = '선케어' GROUP BY p.product_id, p.display_name ORDER BY net_qty DESC LIMIT 10",
"tables_used": [
"store_daily_sales",
"product_master"
],
"period": {
"from": "2026-06-01",
"to": "2026-06-30"
},
"matched_products": [],
"row_count": 7,
"preview": [
{
"product_id": "PRD-1001",
"display_name": "데일리 선에센스 50ml",
"net_qty": 812
}
],
"reason": null,
"hedge": "'지난달' 은 질문이 들어온 2026-07-02 기준으로 6월 전체로 읽었습니다"
}
},
{
"input_ref": "Q004 (통칭이 두 상품에 걸림)",
"output": {
"question_id": "Q004",
"status": "needs_clarification",
"sql": null,
"tables_used": [],
"period": {
"from": "2026-06-01",
"to": "2026-06-30"
},
"matched_products": [
{
"term": "선크림",
"product_id": null,
"candidates": [
"PRD-1001",
"PRD-1002"
]
}
],
"row_count": null,
"preview": [],
"reason": "'선크림' 별칭이 데일리 선에센스 50ml 와 마일드 선스틱 (18g) 둘에 걸려 하나로 정하지 않았습니다",
"hedge": "카테고리 전체(선케어)로 볼지 상품 하나로 볼지 확인이 필요합니다"
}
},
{
"input_ref": "Q006 (허용 테이블 밖 요구)",
"output": {
"question_id": "Q006",
"status": "refused",
"sql": null,
"tables_used": [],
"period": null,
"matched_products": [],
"row_count": null,
"preview": [],
"reason": "직원 단위 실적 테이블이 허용 목록에 없어 실행 전에 막았습니다",
"hedge": null
}
},
{
"input_ref": "Q009 (삭제 요청)",
"output": {
"question_id": "Q009",
"status": "refused",
"sql": null,
"tables_used": [],
"period": null,
"matched_products": [],
"row_count": null,
"preview": [],
"reason": "SELECT 가 아닌 문장이라 읽기 전용 게이트가 실행을 막았습니다",
"hedge": null
}
}
][자동 검증] 1. 출력 형식(필드·enum)이 JSON Schema 를 통과하는지 확인합니다. 2. sql 이 있으면 SQL 파서로 AST 를 떠서 단일 SELECT 인지, 참조 테이블·컬럼이 화이트리스트 안인지, 날짜 조건과 LIMIT 가 있는지 검사하고 하나라도 어긋나면 실행하지 않습니다. 3. tables_used 를 AST 에서 다시 뽑아 출력값과 대조하고, period 가 질문의 기간 표현을 해석 규칙대로 옮긴 값인지 재계산해 확인합니다. 4. matched_products 를 상품 마스터 별칭 매칭으로 재계산해, 후보가 0개나 2개 이상이면 product_id 가 null 이고 status 가 answered 가 아니며 reason 이 비어 있지 않은지 강제합니다. 5. status 가 answered 인 질문은 생성 SQL 과 gold_sql 을 각각 실행해 결과 집합을 정렬 무시하고 행 단위로 대조해 일치 여부를 판정합니다. 6. gold_status 와 status 를 비교해 실행하면 안 되는 질문이 실제로 막혔는지 차단 정확도를 계산합니다. [수동 검증] 1. 결과가 일치하지 않은 질문을 원인별(기간 해석·반품 제외·조인 누락·집계 단위)로 묶어 스키마 카탈로그 설명이 모자란 지점을 찾습니다. 2. answered 로 나온 질문의 5% 를 사람이 SQL 전문과 결과 표를 함께 읽어, 형식이 맞아도 질문 의도와 어긋난 답이 아닌지 확인합니다.
근거: 현장의 문제를 직접 이해하고, AI 기반 솔루션을 설계부터 배포