서비스 아키텍쳐 모듈화 - 100-hours-a-week/KTB4-1st-wiki GitHub Wiki
graph TD
subgraph GroupA["등록 파이프라인"]
F1["기능 1<br/>이미지 검열·식별·초안생성"]
REG["등록 검증 오케스트레이션<br/>(기능2→기능3 순차 호출)"]
F2["기능 2<br/>텍스트 검열<br/>(규칙 기반 필터링→LLM 2단 캐스케이드)"]
F3["기능 3<br/>시세조사·가치산정"]
REJECT["반려 처리"]
DONE["등록 완료"]
F1 --> REG
REG -->|① 호출| F2
F2 -->|검열 실패| REJECT
F2 -->|검열 통과| REG
REG -->|② 호출| F3
F3 --> REG
REG --> DONE
end
subgraph GroupB["독립 기능"]
F4["기능 4<br/>정책 챗봇"]
F5["기능 5<br/>스마트 검색<br/>(실시간 임베딩+코사인유사도)"]
F6["기능 6<br/>채팅 다듬기"]
end
subgraph ModelServing["서빙 백엔드"]
CLAUDE["Claude API"]
EMBED["임베딩 모델<br/>(dragonkue/multilingual-e5-small-ko-v2)"]
MODAL["Modal 서버리스 + vLLM<br/>(Qwen2.5-7B-Instruct, AWQ INT4,<br/>prefix caching)"]
end
subgraph Infra["인프라"]
VECTOR[("벡터스토어<br/>(ChromaDB, 정책문서 인덱스)")]
end
F1 --> CLAUDE
F2 --> CLAUDE
F3 --> CLAUDE
F4 --> EMBED
F4 --> CLAUDE
F4 --> VECTOR
F5 --> EMBED
F6 --> MODAL
classDef claudeapi fill:#e6f1fb,stroke:#185fa5,color:#0c447c;
classDef embed fill:#e1f5ee,stroke:#0f6e56,color:#085041;
classDef selfhost fill:#eeedfe,stroke:#534ab7,color:#3c3489;
classDef success fill:#eaf3de,stroke:#3b6d11,color:#27500a;
classDef fail fill:#fcebeb,stroke:#a32d2d,color:#791f1f;
classDef infra fill:#faeeda,stroke:#854f0b,color:#412402;
classDef orchestration fill:#fff4e0,stroke:#b36b00,color:#5c3600;
class F1,F2,F3,F4,CLAUDE claudeapi;
class F5,EMBED embed;
class F6,MODAL selfhost;
class DONE success;
class REJECT fail;
class VECTOR infra;
class REG orchestration;
- AI Server는 Claude API 호출이든 로컬 경량모델 실행이든 요청을 받아 결과를 돌려주는 한 덩어리의 모듈이다. 내부적으로 어떤 방식을 쓰는지는 구현 디테일이며, Backend는 그 차이를 알 필요없다.(모듈화)
- "무상태"는 요청 간 세션을 유지하지 않는다는 뜻이지, AI 서버가 아무 데이터도 갖지 않는다는 뜻은 아니다. 물건 소유권·거래 상태 같은 원본 비즈니스 데이터는 오직 Backend DB에만 있고 AI 서버는 이에 접근하지 않는다. 반면 벡터DB(정책문서 인덱스)처럼 AI 서버가 자신의 추론을 위해 스스로 만들어내는 파생 데이터는 AI 서버 자신의 작업 영역 안에 둘 수 있다.
- 원본 데이터는 Backend만 접근한다. AI 서버는 원본 데이터에 손대지 않는다.
| 모듈 | 담당 | 분리한 이유 |
|---|---|---|
| AI Server (FastAPI) | Claude API 호출 전담(프롬프트 구성·호출·응답 매핑) + 로컬 경량모델 실행(임베딩) + Modal 서버리스(vLLM) 호출. 요청 간 세션 상태를 유지하지 않아 여러 인스턴스로 자유롭게 확장 가능하며, Backend의 원본 비즈니스 데이터는 저장·접근하지 않는다. | 모델·프롬프트를 교체·튜닝할 때 Backend를 건드릴 필요가 없도록, "AI 관련 모든 것"을 하나의 경계 안에 캡슐화 |
캡슐화: AI 서버가 실제로 무엇을 관리하는지(Anthropic API 키, 모델 파일 등)는 그 내부에만 존재하는 구현 디테일이다. Backend·Frontend 입장에서는 "AI 서버에 요청을 보내면 결과가 온다"는 동일한 인터페이스만 보이며, 그 이상은 알 필요도 없다.
거시적으로는 Frontend·Backend·AI Server를 나눴지만, AI Server 하나의 프로세스 안에서도 기능별로 폴더를 분리해 같은 수준의 모듈화를 한 번 더 적용한다. 라우터도 별도 최상위 폴더로 빼지 않고, 각 기능 폴더 안에 함께 둔다 — 기능 하나를 수정할 때 폴더 하나만 열면 끝나도록 하기 위해서다.
등록 흐름(기능2→기능3)처럼 여러 기능이 순서대로 실행되어야 하는 경우는 features/registration/이라는 오케스트레이션 폴더를 별도로 둬서 조율한다. 개별 기능 폴더(moderation, price)는 이 오케스트레이션의 존재를 모른 채 예전처럼 독립적으로 작업 가능하다. moderation·price의 router.py(기존 엔드포인트 존치 여부)는 팀 논의 후 결정할 사항이라 이 문서에서는 확정하지 않는다.
ai-server/
├── features/ # 기능별 독립 모듈 (기본은 router/service/schemas/prompts 4개 파일 구성, 일부 예외는 주석 참고)
│ ├── vision/ # 기능1: 사진 등록 (검열+식별+초안작성)
│ │ ├── __init__.py
│ │ ├── router.py # 엔드포인트 정의
│ │ ├── service.py # 실제 처리 로직
│ │ ├── schemas.py # 요청/응답 스키마
│ │ └── prompts.py # 시스템 프롬프트
│ ├── moderation/ # 기능2: 텍스트검열 (규칙 기반 필터링→LLM 2단 캐스케이드)
│ │ ├── __init__.py
│ │ ├── router.py # (보류) 기존 엔드포인트 유지 여부는 팀 논의 후 결정
│ │ ├── service.py # 캐스케이드 로직(rules.py 호출 → 통과 시 LLM 호출)
│ │ ├── schemas.py # 요청/응답 스키마
│ │ ├── prompts.py # LLM 단계(사기성문구·물건혼재·가격언급) 시스템 프롬프트
│ │ └── rules.py # 정규식 패턴 + 금칙어 템플릿(연락처·스팸·욕설혐오·금지품목) — moderation만의 예외 파일
│ ├── price/ # 기능3: 가격검색 (웹서치+LLM, 구성 동일)
│ │ ├── __init__.py
│ │ ├── router.py # (보류) 기존 엔드포인트 유지 여부는 팀 논의 후 결정
│ │ ├── service.py
│ │ ├── schemas.py
│ │ └── prompts.py
│ ├── registration/ # 신규: 기능2→기능3 순차 오케스트레이션
│ │ ├── __init__.py
│ │ ├── router.py # 등록하기 흐름의 진입점 (구체 엔드포인트 스펙은 API 설계 문서에서 별도 확정)
│ │ ├── service.py # moderation.service·price.service를 함수 호출(같은 프로세스 내 import)로 순서대로 실행. HTTP self-call 아님
│ │ └── schemas.py # 요청/응답 스키마(검증+가격산정 결과를 합친 형태) — LLM을 직접 호출하지 않아 prompts.py 없음
│ ├── chatbot/ # 기능4: RAG 챗봇 (임베딩은 shared/embedding 사용, 구성 동일)
│ ├── search/ # 기능5: 스마트검색 (LLM 완전 교체 → 임베딩만 사용, prompts.py 없음)
│ └── message_refine/ # 기능6: 채팅 다듬기 (구성 동일)
│
├── shared/ # 2개 이상 기능이 실제로 겹쳐 쓰는 것만
│ ├── llm/ # 텍스트 생성 provider 모음 (기능별로 원하는 걸 골라 씀)
│ │ ├── base_provider.py # "텍스트 생성" 공통 규칙(인터페이스)만 정의
│ │ ├── claude_provider.py # Claude API 호출 (기능1,2,3,4가 사용)
│ │ └── modal_provider.py # Modal 서버리스+vLLM 호출 (기능6이 사용)
│ ├── embedding/ # 임베딩 provider (기능4,5가 공유)
│ │ └── embedding_provider.py # dragonkue/multilingual-e5-small-ko-v2 로드·추론, query/passage 프리픽스 처리
│ └── errors/ # 예외 → 표준 JSON 형식 변환 로직 (전 기능 공통 사용)
│
├── core/ # FastAPI 앱이라면 AI와 무관하게 다 있어야 하는 뼈대
│ └── config.py # 환경설정
│
└── main.py # 각 기능 라우터를 앱에 등록
도메인별 역할
-
features/{기능}/router.py: 이 기능의 엔드포인트 정의. 각 설계 문서의 "1번 엔드포인트 목록"에 대응. 기능별로 파일이 나뉘어 있어, 여러 기능을 동시에 작업해도 같은 파일에서 충돌하지 않는다. -
features/{기능}/service.py: 실제 처리 로직(Claude 호출, 캐스케이드, 임베딩 계산 등). API 명세서의 "내부 처리"에 대응. -
features/{기능}/schemas.py: 요청/응답 스키마. 이 파일의 내용이 곧 Backend와의 인터페이스 계약이다. -
features/{기능}/prompts.py: 시스템 프롬프트만 따로 분리. 프롬프트 튜닝과 로직 수정이 서로 다른 파일이 되어, 프롬프트만 고칠 때 로직 코드를 열 필요가 없다. LLM을 직접 호출하는 기능에만 존재하며,search·registration처럼 LLM을 호출하지 않는 폴더에는 없다. -
features/moderation/rules.py: 정규식 패턴과 금칙어 템플릿(연락처·스팸·욕설/혐오·금지품목)을 모아둔 파일.prompts.py를 분리한 것과 같은 이유로, 규칙만 추가·수정할 때service.py의 캐스케이드 로직을 열지 않아도 되게 한다. moderation만 이 파일이 추가되어 5개 파일 구성이 되는 예외다. -
features/registration/service.py: 기능2(moderation.service)와 기능3(price.service)을 같은 프로세스 안에서 함수 호출로 순서대로 실행하는 오케스트레이션 로직. 텍스트 검증을 통과해야만 가격 산정으로 넘어가며, 두 모듈의 내부 구현에는 관여하지 않고 오직 호출 순서만 조율한다. -
shared/llm: 여러 provider(Claude, Modal 등)를 같은 서랍에 늘어놓고, 각 기능이 자기가 쓸 걸 골라 부른다. provider끼리 서로 독립적이라 하나를 교체해도 나머지 기능엔 영향이 없다. 임베딩(기능4·5)은 "텍스트 생성"과 입출력 형태 자체가 달라 여기 포함하지 않고,shared/embedding/으로 별도 분리한다. -
shared/embedding: 기능4(챗봇 RAG)·기능5(스마트검색)가 공통으로 쓰는 임베딩 모델(dragonkue/multilingual-e5-small-ko-v2)을 한 곳에 둔다. e5 계열 모델은 질의(query)와 문서(passage)에 서로 다른 프리픽스를 붙여야 유사도 품질이 나오므로, 이 프리픽스 처리를 공통 함수로 여기에 넣어 두 기능이 실수 없이 재사용하게 한다. -
shared/errors:{"error": "...", "message": "..."}로 감싸는 형식 변환 로직만 여기 있다. 실제 에러 종류(item_not_found,empty_text등)는 각 기능이 자기 폴더 안에서 정의하고 예외를 던지면, 이 변환기가 표준 형식으로 바꿔준다. -
core/config.py:.env에 있는 API 키·설정값을 코드에서 읽어오는 창구. AI나 우리 도메인과 무관하게 어떤 서비스든 필요한 뼈대라 비즈니스 로직은 없다. -
main.py: 각 기능의router.py를 모아 앱에 등록하는 조립 지점.
이렇게 나누면, 기능5(스마트검색)를 임베딩으로 교체하는 작업은 features/search/와 shared/embedding/ 정도만 건드리면 끝나고 features/price/나 features/moderation/은 건드릴 필요가 없다 — 5번 섹션의 근거를 코드 구조 레벨에서도 그대로 실증하는 셈이다.
(기존에 쓴 내용 그대로 유지)
- 통신 방식: REST API (HTTP/JSON), 동기 호출
- 엔드포인트 네이밍:
/api/{도메인}/{동작}- 응답 포맷: 성공 시 필드 감싸지 않고 반환 / 실패 시
{"error": "...", "message": "..."}
AI 서버 내부의 개별 기능 모듈(
vision,moderation,price,chatbot,search,message_refine)은 서로를 직접 호출하지 않는다. 각 모듈은 자체적으로 요청을 처리·응답하며, 모듈 간 의존 관계가 존재하지 않는다.다만 등록 흐름처럼 여러 기능이 순서대로 실행되어야 하는 경우,
registration모듈이 별도로 이 순서를 조율(오케스트레이션)한다.registration은moderation,price의service.py함수를 같은 프로세스 내에서 직접 호출(import)하며, 이는 HTTP로 자기 자신을 다시 호출하는 방식(self-call)이 아니다. 이 의존은registration → moderation,registration → price방향으로만 존재하고, 반대 방향(개별 기능이 오케스트레이션 모듈을 아는 것)은 없다.대신 각 모듈 내부는
router → service → schemas3계층으로 구성되어, 하나의 모듈 안에서 데이터 입력(router)·모델 추론 및 후처리(service)·응답 스키마 검증(schemas)의 흐름이 분리된다. 여러 모듈이 공통으로 필요로 하는 기능(Claude/임베딩 클라이언트 래퍼, 예외→JSON 변환)은shared/에 위치시켜 각 모듈이 단방향으로 참조하며, 이는 모듈 간 통신이 아니라 공통 유틸리티에 대한 의존이다.
개별 기능 모듈 간에 호출 관계가 없다는 것은 결합도가 0에 가깝다는 의미이며, 이는 과제가 요구하는 "모델 교체/업데이트 시 서비스 전체 수정 불필요"와 "여러 팀원의 병렬 작업"을 가장 강하게 보장하는 형태다.
예를 들어
message_refine모듈의 내부 구현을 Claude Haiku 호출에서 Modal 서버리스 위 자체 서빙(vLLM)으로 교체하더라도, 이 변경은message_refine/service.py내부에만 국한되며vision,chatbot,search등 다른 모듈이나 Backend와의 계약에는 영향을 주지 않는다. 이는 모듈 간 호출이 아예 없기 때문에 가능한 구조이며, 만약 모듈 간 호출 체인이 있었다면 하나의 모듈 변경이 연쇄적으로 다른 모듈의 재검증을 요구했을 것이다.유일한 예외는
registration이다. 등록 흐름은 텍스트 검증(기능2)과 가격 산정(기능3)이 항상 한 세트로 함께 실행되어야 하므로(제목·내용이 바뀌면 가격도 다시 산정되어야 함), 이 둘을 순서대로 묶어 조율하는 오케스트레이션 모듈을 별도로 둔다. 결합은registration → moderation,registration → price방향으로만 존재하며,moderation과price자신은 서로를 모르고registration의 존재도 모른다. 따라서moderation이나price의 함수 시그니처(입출력 스키마)가 바뀌는 경우에만registration도 함께 수정이 필요하고, 나머지 모듈(vision,chatbot,search,message_refine)에는 영향이 없다 — 결합도 증가가registration한 곳에만 국한된다.
- 독립적 교체: AI 서버 내부 구현을 Claude API 호출에서 로컬 경량모델로 바꾸더라도 Backend·Frontend의 코드나 배포는 건드릴 필요가 없다 — 인터페이스 계약만 유지되면 된다.
- 독립적 스케일링: AI 관련 작업은 트래픽 패턴과 자원 사용량(API 비용, 로컬 모델의 CPU/메모리)이 일반 CRUD 요청과 크게 달라, AI 서버만 별도로 인스턴스를 늘리거나 줄일 수 있다.
- 장애 격리: Claude API 쪽에 문제가 생겨도 Backend의 로그인·DB 기능은 영향받지 않는다.
- 병렬 개발: 인터페이스 계약만 먼저 합의되면, Backend 담당과 AI 서버 담당이 서로의 구현을 기다리지 않고 동시에 개발할 수 있다.
사례 — 기능6(채팅 다듬기): 현재 외부 LLM API를 호출하는 방식에서 추후 vLLM을 활용해 Modal 환경에 자체 서빙하는 방식으로 변경하더라도, 수정 범위는 AI 서버 내부의 모델 호출 로직으로 제한된다. Backend가 보내는 요청 필드(original_text, my_profile, opponent_profile)와 AI 서버가 반환하는 응답 형식은 그대로 유지할 수 있으므로, Backend·Frontend 코드를 수정하지 않고 AI 서버의 모델 실행 방식만 교체하여 배포할 수 있다.