튜토리얼
Week 9: 서비스 아키텍처

Week 9. 서비스 아키텍처 및 성능 최적화

그래프 시각화, FastAPI 서버, Incremental Update, 성능 최적화


학습 목표

이 튜토리얼을 완료하면 다음을 할 수 있습니다:

  • 그래프 시각화 구현 (D3.js, Neovis.js, pyvis)
  • FastAPI 기반 API 서버 구축
  • Incremental Update 및 정합성 관리
  1. 프로덕션 성능 최적화

핵심 개념

핵심 개념

"레스토랑 서비스" 비유

지금까지 우리는 주방(Kitchen) 시스템을 만들었습니다.

  • 데이터: 식재료
  • 온톨로지: 레시피
  • Graph DB: 주방 설비
  • LLM: 셰프 (요리 및 추론)

서비스 아키텍처손님에게 음식을 대접하는 것입니다.

  • API (FastAPI): 웨이터. 주문(Query)을 받고 요리(Result)를 서빙합니다.
  • 시각화: 플레이팅. 데이터를 먹음직스럽게(이해하기 쉽게) 보여줍니다.
  • 캐싱(Caching): 미리 손질된 재료. 피크 타임에도 빠르게 서빙할 수 있게 돕습니다.

1. 서비스 아키텍처

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Frontend  │────▶│   FastAPI   │────▶│   Neo4j     │
│  (React)    │     │   Server    │     │   Graph DB  │
└─────────────┘     └─────────────┘     └─────────────┘
      │                    │                    │
      │             ┌──────┴──────┐            │
      │             ▼             ▼            │
      │      ┌──────────┐  ┌──────────┐       │
      └─────▶│  D3.js   │  │   LLM    │◀──────┘
             │  Viz     │  │  Service │
             └──────────┘  └──────────┘

2. 그래프 시각화

Neovis.js (Neo4j 직접 연결)

import NeoVis from 'neovis.js';
 
const config = {
    containerId: "viz",
    neo4j: {
        serverUrl: "bolt://localhost:7687",
        serverUser: "neo4j",
        serverPassword: "password",
    },
    labels: {
        Movie: { label: "title", size: "popularity" },
        Person: { label: "name" }
    },
    relationships: {
        DIRECTED: { thickness: "weight" },
        ACTED_IN: { thickness: "weight" }
    },
    initialCypher: `
        MATCH (d:Director)-[:DIRECTED]->(m:Movie)<-[:ACTED_IN]-(a:Actor)
        RETURN d, m, a LIMIT 100
    `
};
 
const viz = new NeoVis(config);
viz.render();

pyvis (Python 기반)

from pyvis.network import Network
 
def visualize_graph(neo4j_driver, query):
    net = Network(height="600px", width="100%", notebook=True)
 
    with neo4j_driver.session() as session:
        result = session.run(query)
        for record in result:
            for node in record["nodes"]:
                net.add_node(node.id, label=node["name"])
            for rel in record["rels"]:
                net.add_edge(rel.start_node.id, rel.end_node.id, title=rel.type)
 
    return net.show("graph.html")

3. FastAPI 서버

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from neo4j import GraphDatabase
 
app = FastAPI(title="Knowledge Graph API")
 
class QueryRequest(BaseModel):
    query: str
    params: dict = {}
 
class EntityRequest(BaseModel):
    name: str
    entity_type: str
    properties: dict = {}
 
@app.post("/query")
async def execute_query(request: QueryRequest):
    with driver.session() as session:
        result = session.run(request.query, request.params)
        return [dict(record) for record in result]
 
@app.get("/entity/{entity_type}/{name}")
async def get_entity(entity_type: str, name: str):
    query = f"""
        MATCH (e:{entity_type} {{name: $name}})
        OPTIONAL MATCH (e)-[r]-(connected)
        RETURN e, collect({{rel: type(r), node: connected}}) as connections
    """
    with driver.session() as session:
        result = session.run(query, name=name)
        record = result.single()
        if not record:
            raise HTTPException(404, "Entity not found")
        return dict(record)
 
@app.post("/entity")
async def create_entity(request: EntityRequest):
    query = f"""
        CREATE (e:{request.entity_type} {{name: $name}})
        SET e += $properties
        RETURN e
    """
    with driver.session() as session:
        result = session.run(query, name=request.name, properties=request.properties)
        return dict(result.single()["e"])

4. Incremental Update

class GraphUpdater:
    def __init__(self, driver):
        self.driver = driver
 
    def upsert_entity(self, entity_type, entity_id, properties):
        """개체 업서트 (있으면 업데이트, 없으면 생성)"""
        query = f"""
            MERGE (e:{entity_type} {{id: $id}})
            SET e += $properties
            SET e.updated_at = datetime()
            RETURN e
        """
        with self.driver.session() as session:
            return session.run(query, id=entity_id, properties=properties).single()
 
    def add_relationship(self, source_id, target_id, rel_type, properties=None):
        """관계 추가 (중복 방지)"""
        query = """
            MATCH (s {id: $source_id})
            MATCH (t {id: $target_id})
            MERGE (s)-[r:$rel_type]->(t)
            SET r += $properties
            RETURN r
        """
        with self.driver.session() as session:
            return session.run(query,
                source_id=source_id,
                target_id=target_id,
                rel_type=rel_type,
                properties=properties or {}
            ).single()
 
    def sync_from_source(self, source_data, last_sync_time):
        """외부 소스와 동기화"""
        new_records = [r for r in source_data if r["updated_at"] > last_sync_time]
        for record in new_records:
            self.upsert_entity(
                record["type"],
                record["id"],
                record["properties"]
            )

5. 성능 최적화

인덱싱 전략

// 조회 성능 향상
CREATE INDEX entity_id FOR (n:Entity) ON (n.id)
CREATE INDEX entity_name FOR (n:Entity) ON (n.name)
 
// 복합 인덱스
CREATE INDEX movie_year_title FOR (m:Movie) ON (m.year, m.title)
 
// 전문 검색
CREATE FULLTEXT INDEX entity_search FOR (n:Entity) ON EACH [n.name, n.description]

쿼리 최적화

// Bad: 전체 스캔
MATCH (m:Movie) WHERE m.title CONTAINS "Inception" RETURN m
 
// Good: 인덱스 활용
MATCH (m:Movie) WHERE m.title = "Inception" RETURN m
 
// Better: 파라미터 사용 (캐싱)
MATCH (m:Movie {title: $title}) RETURN m

면접 질문 맛보기

Q: 지식 그래프 서비스의 확장성을 어떻게 확보하나요?

  • 수평 확장: Neo4j Cluster (Causal Clustering)
  • 캐싱: Redis로 빈번한 쿼리 결과 캐싱
  • 읽기/쓰기 분리: Read Replicas 활용
  • 샤딩: 도메인별 그래프 분리

더 많은 면접 질문은 Premium Interviews (opens in a new tab)에서 확인하세요.


🎬 프로젝트: 영화 추천 지식그래프

🎉 프로젝트 완료!

Week주제프로젝트 마일스톤
1온톨로지 입문✅ 영화 도메인 설계 완료
2RDF & RDFS✅ 영화 10편 RDF 변환 완료
3OWL & 추론✅ 추론 규칙 적용 완료
4지식 추출✅ 영화 100편 자동 수집 완료
5Neo4j✅ 그래프 DB 구축 완료
6GraphRAG✅ 자연어 질의 시스템 완료
7온톨로지 에이전트✅ 자동 업데이트 에이전트 완료
8도메인 확장✅ 의료 도메인 확장 완료
9서비스 배포프로덕션 배포

Week 9 마일스톤: 영화 추천 서비스 배포

9주간 만든 영화 지식그래프를 실제 서비스로 배포합니다.

서비스 아키텍처:

┌─────────────────────────────────────────────┐
│                   Frontend                   │
│  (React Dashboard + Chat Interface)          │
└─────────────────┬───────────────────────────┘
                  │ REST API
┌─────────────────▼───────────────────────────┐
│                FastAPI Server                │
│  /recommend  /search  /chat  /update        │
└──────┬──────────────┬───────────────────────┘
       │              │
┌──────▼──────┐ ┌─────▼─────┐
│   Neo4j     │ │   LLM     │
│ (Graph DB)  │ │ (OpenAI)  │
└─────────────┘ └───────────┘

API 엔드포인트:

@app.get("/recommend/{movie_title}")
# 유사 영화 추천
 
@app.post("/chat")
# 자연어 질의 (GraphRAG)
 
@app.post("/update")
# 새 영화 추가 (에이전트 트리거)
 
@app.get("/graph/stats")
# 지식그래프 통계

배포 체크리스트:

  • Neo4j Aura 클라우드 설정
  • FastAPI 서버 Docker화
  • Vercel/Railway 배포
  • API 키 환경변수 설정
  • 모니터링 (Sentry)

축하합니다! 9주간의 프로젝트가 완성됩니다.

프로젝트 노트북에서는 다음을 직접 구현합니다:

  • FastAPI로 /recommend, /chat, /update API 구현
  • pyvis로 지식그래프 시각화 대시보드
  • Neo4j Aura 클라우드 연결
  • Docker Compose로 원클릭 배포

완성된 시스템: "놀란 감독 스타일의 SF 영화 추천해줘"라고 물으면, 지식그래프에서 감독-장르-평점 관계를 추론하고 자연어로 답변하는 AI 에이전트


실습 노트북

이론을 더 깊이 탐구하고 싶다면:

실습 노트북에서는 추가로 다음 내용을 다룹니다:

  • Kubernetes 배포 설정
  • 로드 테스트 (Locust) 및 성능 튜닝
  • Prometheus + Grafana 모니터링
  • CI/CD 파이프라인 구성

과정 완료!

축하합니다! 9주 과정을 모두 마쳤습니다.

배운 내용 요약

  • 온톨로지 공학: RDF, RDFS, OWL, 추론
  • 지식 추출: NER, RE, Entity Resolution
  • 그래프 DB: Neo4j, Cypher
  • LLM 통합: GraphRAG, 온톨로지 에이전트
  • 프로덕션: 시각화, API, 최적화

다음 단계