be.me.kangsiwoo.com
api:
GET /

강시우 / Kang Siwoo

Backend Engineer

데이터가 흐르는 길을 고민해 보는 주니어 백엔드 개발자

Kafka에서 들어온 메트릭을 FastAPI로 받아 SSE로 흘려보내고, 원격 LLM 호출을 큐·워커로 줄 세워 본 경험이 있습니다. 아직 배우는 중이지만, 말로 설명하기보다 이 사이트의 /api를 제 k3s 클러스터에 직접 띄워 두었습니다. 부족하더라도 한번 호출해 봐 주세요.

FastAPI · async · Kafka→SSE · queue/worker · React/TS

bash — livebe.me.kangsiwoo.com
$
~/be $GET /about

말보다 동작으로 보여드리고 싶었습니다

제 이력서 사이트(me.kangsiwoo.com)는 인프라·관찰가능성(SRE) 관점에서 저를 소개합니다. 이 사이트는 조금 다릅니다. ‘시스템을 운영하는 사람’보다는 ‘애플리케이션 백엔드를 만들어 보는 사람’으로서의 모습을 담아보고 싶었습니다.

그래서 말로만 설명하기보다, 실제로 동작하는 걸 보여드리는 쪽을 택했습니다. 화면(/)은 Next.js 정적 빌드로 MinIO에 올라가 있고, /api/* 는 제 k3s 클러스터에서 돌고 있는 FastAPI입니다. 아래 플레이그라운드의 버튼을 누르면 진짜 서버가 응답합니다. 실무·사이드에서 다뤄 본 패턴(실시간 스트리밍, 비동기 작업 큐)을 작게 옮겨 담아 봤습니다.

아직 주니어라 부족한 점이 많습니다. 대규모 트래픽을 받아본 적도, 분산 시스템의 깊은 함정을 겪어본 적도 없습니다. 다만 ‘왜 이렇게 만들었는지’ 정도는 스스로 고민하고 코드로 옮겨봤고, 그 과정을 솔직하게 보여드리고 싶었습니다.

~/be $GET /projects

제가 만들어 본 것들

GET /projects/flafla/consumer.py

FLA — 실시간 메트릭 파이프라인

데이터센터 인프라 상태를 실시간 수집·시각화하는 사내 모니터링 콘솔. 팀 프로젝트이고, 제가 맡은 부분은 메트릭 파이프라인과 시각화 라이브러리였습니다.

  • 메트릭이 들어와 화면에 닿기까지의 파이프라인 설계
  • AIOKafkaConsumer 기반 인프라별 독립 소비 구조
  • SSE 푸시 + 시각화 라이브러리(npm 배포)
data-flow
  external agents (per infra)
        │  publish  cpu-status-{id}
        ▼
   ┌──────────┐  topic: cpu-status-1
   │  Kafka   │  topic: cpu-status-2 ...
   └────┬─────┘
        │  AIOKafkaConsumer (group_id = fla-{id})   ← 인프라마다 독립
        ▼
   ┌──────────┐   SSE   ┌─────────────┐
   │ FastAPI  │ ──────▶ │ React 위젯   │  CPU/RAM/GPU 즉시 갱신
   └──────────┘         └─────────────┘
fla/consumer.py
# 인프라별 Kafka 토픽을 독립 group_id로 소비해 SSE로 흘린다
async def consume_infra(infra_id: str):
    consumer = AIOKafkaConsumer(
        f"cpu-status-{infra_id}",
        bootstrap_servers=KAFKA_BROKERS,
        group_id=f"fla-{infra_id}",        # 인프라마다 독립 → 수평 분리
        enable_auto_commit=True,
    )
    await consumer.start()
    try:
        async for msg in consumer:
            metric = json.loads(msg.value)
            latest[infra_id] = metric          # in-memory 최신값 스냅샷
            await hub.publish(infra_id, metric)  # 구독 중인 SSE로 팬아웃
    finally:
        await consumer.stop()

데이터가 흐르는 길은 이렇습니다. 외부 에이전트가 인프라별 Kafka 토픽(cpu-status-{id})에 메트릭을 발행 → FastAPI 백엔드가 AIOKafkaConsumer로 인프라마다 독립된 group_id로 소비 → SSE로 클라이언트에 푸시 → 프론트가 CPU/RAM/GPU 위젯을 즉시 갱신.

여기서 제가 의식한 설계 포인트는 ‘인프라가 늘어나도 구조가 안 무너지게’ 하는 것이었습니다. 토픽을 인프라 단위로 나누고 컨슈머도 독립 group_id로 분리해 두면, 인프라가 추가될 때 컨슈머·토픽이 자연스럽게 수평으로 갈라집니다. 한 인프라의 메트릭이 다른 인프라 처리에 끼어들지 않도록 격리한 것이 핵심 판단이었습니다.

Kafka를 고른 이유, polling 대신 SSE를 고른 이유(주기적 요청 없이 서버가 변화를 밀어주는 게 실시간 위젯에 맞다고 봤습니다)도 나름대로 따져보고 정했습니다. 거대한 처리량을 받아본 건 아니지만, ‘왜 이 도구를 골랐는지’ 정도는 스스로 설명해 보려 한 프로젝트입니다.

self-audit: known-issues 18건
WARN 보안 4 — 인증/권한 경계 미흡 항목 카탈로그화
WARN 기능 차단 5 — 엣지 케이스에서 막히는 흐름 기록
WARN 미구현·품질 9 — 다음에 개선할 항목으로 정리
→ 자기 코드의 약점을 직접 찾아 적어둔 결과물(성숙도 신호)
GET /projects/tinatina/worker.py

TINA-AI — 비동기 작업 큐와 LLM 파이프라인

카카오톡 대화와 두 사람의 MBTI를 받아 원격 LLM에 분석을 맡기고, 구조화된 궁합 분석 JSON을 돌려주는 FastAPI 서비스.

  • 정규식 파서 → 프롬프트 템플릿 → JSON 추출 파이프라인 설계
  • queue + 데몬 워커 + Future 기반 호출 직렬화
  • 추론은 원격 게이트웨이에 위임 → GPU 불필요
data-flow
  KakaoTalk export + 2×MBTI
        │
        ▼
  정규식 파서 ─▶ MBTI 프롬프트 템플릿 ─▶ 프롬프트 엔지니어링
                                          │
                                          ▼
                              원격 LLM (DeepSeek-R1 14B)
                              OpenWebUI 게이트웨이
                                          │  JSON만 안전 추출
                                          ▼
                                  구조화 분석 결과
tina/worker.py
# 원격 LLM 호출을 큐 + 데몬 워커로 직렬화한다
task_queue: "queue.Queue[tuple[Payload, Future]]" = queue.Queue()

def worker():                          # 데몬 스레드 하나가 순차 처리
    while True:
        payload, fut = task_queue.get()
        try:
            raw = call_remote_llm(payload)     # 무거운 외부 호출
            fut.set_result(extract_json(raw))  # 응답에서 JSON만 추출
        except Exception as e:
            fut.set_exception(e)
        finally:
            task_queue.task_done()

threading.Thread(target=worker, daemon=True).start()

@app.post("/analyze")
def analyze(payload: Payload):
    fut: Future = Future()
    task_queue.put((payload, fut))     # 요청을 막지 않고 줄 세움
    return fut.result(timeout=120)     # 완료되면 결과 반환

제가 설계한 파이프라인은 네 단계입니다 — 카카오톡 정규식 파서 → MBTI 프롬프트 템플릿 구성 → 프롬프트 엔지니어링 → 응답에서 JSON만 안전하게 추출.

가장 신경 쓴 부분은 동시성이었습니다. 원격 LLM 게이트웨이는 한 번에 많은 요청을 던지면 버티지 못합니다. 그래서 queue.Queue + 데몬 워커 + concurrent.futures.Future 조합으로 호출을 순차 처리하도록 직렬화했습니다. ‘무거운 외부 호출을 요청 스레드에서 떼어내 줄 세운다’는 백엔드 동시성의 기본기를 직접 손으로 구현해 본 경험입니다.

추론은 원격 게이트웨이에 위임했기 때문에 서비스 컨테이너에는 GPU가 필요 없고, python:3.11-slim 이미지에 docker compose up 한 줄로 뜹니다. 책임을 어디에 둘지(추론은 게이트웨이, 오케스트레이션은 내 서비스) 나눠본 경험이기도 합니다.

GET /projects/netmonnetmon/topology.tsx

Network-Monitor — 수집한 데이터를 화면까지

KCD 실무. SNMP·LLDP로 수집한 네트워크 데이터를 React(TypeScript) 커스텀 토폴로지 UI로 시각화. 온프레미스와 AWS를 한 화면에 통합.

  • Zabbix SNMP/LLDP 수집 → Python 전처리 → 토폴로지 그래프
  • React(TS) 커스텀 토폴로지 UI
  • Boto3·CloudQuery로 온프렘+AWS 단일 뷰 통합
data-flow
  네트워크 장비 ─SNMP/LLDP─▶ Zabbix
                                  │
                                  ▼
                         Python 전처리(토폴로지 그래프)
                                  │
        Boto3 · CloudQuery(AWS) ──┤
                                  ▼
                       React(TS) 커스텀 토폴로지 UI
                       (구조 진단 시간  3h → 1h)
netmon/topology.tsx
// 수집·전처리된 노드/링크를 받아 토폴로지로 그린다 (프론트 연결)
type Node = { id: string; kind: "switch" | "router" | "host"; load: number };
type Link = { from: string; to: string; traffic: number };

function Topology({ nodes, links }: { nodes: Node[]; links: Link[] }) {
  const positioned = useMemo(() => layout(nodes, links), [nodes, links]);
  return (
    <svg>
      {links.map((l) => (
        <Edge key={l.from + l.to} {...edgeFor(positioned, l)} />
      ))}
      {positioned.map((n) => (
        <NodeBadge key={n.id} node={n} warn={n.load > 0.8} />
      ))}
    </svg>
  );
}

백엔드가 수집·전처리한 데이터를 운영자가 한눈에 보도록 React(TypeScript)로 커스텀 토폴로지 UI를 만들었습니다. 구조와 트래픽을 함께 보이게 해서, 네트워크 구조를 파악해 원인을 짚는 시간을 3시간에서 1시간으로 줄였습니다.

이 성과 자체는 SRE 업무에 가깝지만, 이 사이트에서는 ‘백엔드가 내려준 데이터를 화면까지 책임지고 연결해 봤다’는 프론트엔드 근거로 인용합니다.

~/be $POST /playground

라이브 API 플레이그라운드 — 직접 호출해 보세요

이 사이트에서 가장 보여드리고 싶은 부분입니다. 아래 엔드포인트는 제 k3s 클러스터에서 돌고 있는 FastAPI 백엔드로 연결됩니다. 실무·사이드에서 써 본 패턴(SSE 실시간 · 비동기 작업 큐)을 작게 옮겨 라이브로 만들어 본 것입니다. 남용 방지를 위한 간단한 rate limit은 의도된 동작입니다.

POST /playgroundOpenAPI ↗
GET/api/info

서비스 메타데이터 — 워밍업 + liveness 확인

curl equivalent
curl -sN https://be.me.kangsiwoo.com/api/info
// response will appear here — press Send
~/be $GET /projects/frontend

백엔드가 만든 데이터를 화면까지 그려 봤습니다

백엔드만 만들고 데이터를 화면에서 어떻게 쓰는지 모르면 좋은 API를 만들기 어렵다고 느꼈습니다. 그래서 부족하나마 양쪽을 다 만져보려 했습니다.

FLA에서는 드래그·리사이즈가 되는 그리드 대시보드 위젯 라이브러리를 만들어 npm에 배포해 봤습니다(fla-dashboard). 위젯을 옮길 때 다른 위젯과 겹치면 자동으로 빈 자리를 찾아 비켜주는 격자 충돌 해소 알고리즘(findSpotPriority / findNearestSpot)을 직접 구현했고, Vite 라이브러리 모드로 ESM/UMD 번들과 타입 정의(.d.ts)까지 함께 빌드했습니다.

이 사이트도 React 19 / Next.js 15 / TypeScript / Tailwind로 직접 만들었습니다. 정적 빌드와 라이브 API를 한 도메인 아래 묶는 구성도 직접 짜봤습니다. 화려한 UI 전문가는 아니지만, 백엔드가 내려준 데이터를 화면까지 연결해 보려 했습니다.

fla-dashboard/grid.ts
// fla-dashboard — 위젯이 겹치면 빈 자리를 찾아 자동으로 비켜준다
// (실제 npm 배포: fla-dashboard@0.2.6, Vite lib 모드 ESM/UMD/.d.ts)
function findNearestSpot(grid: Grid, w: number, h: number): Cell {
  for (let y = 0; y < grid.rows; y++) {
    for (let x = 0; x + w <= grid.cols; x++) {
      if (canPlace(grid, x, y, w, h)) return { x, y };   // 첫 빈 자리
    }
  }
  return { x: 0, y: grid.rows };          // 못 찾으면 맨 아래 새 줄
}

function findSpotPriority(grid: Grid, moved: Widget): Cell {
  // 옮긴 위젯과 충돌하는 위젯들을 우선순위대로 재배치
  const collided = grid.widgets.filter((w) => overlaps(w, moved));
  return collided
    .sort(byDistanceTo(moved))
    .reduce((g, w) => place(g, w, findNearestSpot(g, w.w, w.h)), grid).cursor;
}
~/be $GET /architecture

이 사이트의 아키텍처 — 정적 화면 + 살아있는 백엔드

be.me.kangsiwoo.com 한 도메인 안에 두 개의 다른 성격을 한 묶음으로 운영합니다.

  • / (화면)Next.js 15 정적 빌드(out/). 빌드 산출물을 MinIO 오브젝트 스토리지 버킷에 올려 정적으로 서빙합니다. 바뀔 일 없는 콘텐츠라 캐시·배포가 단순합니다.
  • /api/* (백엔드)제 k3s 클러스터에서 실제로 돌고 있는 FastAPI 컨테이너. 위 플레이그라운드의 요청이 여기로 들어옵니다.
  • 라우팅같은 도메인에서 경로(/ 와 /api)로 정적 자산과 라이브 백엔드를 가르도록 구성. 프론트는 같은 출처로 호출하니 CORS 고민이 줄고, 백엔드는 클러스터 안에서 독립적으로 배포·재시작됩니다.

‘정적으로 둘 것과 살아있어야 할 것을 나눈다’ — 그 정도를 생각하며 구성해 봤습니다. 콘텐츠는 정적으로 싸게, 상호작용은 진짜 서버로. 제가 인프라(MinIO·k3s)를 조금 굴려볼 수 있어서 가능했던 구성이고, 백엔드를 만드는 일과 그걸 어디에 띄울지를 한 번에 해보려 한 결과물입니다.

be.me.kangsiwoo.com
  브라우저
    │  https://be.me.kangsiwoo.com
    ▼
 Cloudflare(DNS) ─▶ Traefik(k3s) ─▶ nginx ──┬─▶  /          MinIO 버킷
                                            │        (Next.js 정적 export)
                                            └─▶  /api/*     FastAPI (이 클러스터)
                                                     │ SQLite · asyncio queue · SSE
                                                     ▼
                                                  live backend
GET /architecture/deploy

배포까지 직접 해보려 했습니다

  • 코드는 DC GitLab(ksw.codes) 레포 → CI가 Next.js를 빌드해 MinIO 버킷으로 미러.
  • 백엔드는 컨테이너로 패키징(비루트 실행 + HEALTHCHECK)해 k3s에 배포.
  • liveness(/api/healthz, 프로세스만)와 readiness(/api/readyz, DB까지)를 나눠 프로브에 연결 — DB 한 번 끊겼다고 파드를 재시작하지 않게.
  • TLS는 cert-manager가 Let’s Encrypt(DNS-01)로 자동 발급/갱신.
~/be $GET /stack

기술 스택 — 과장 없이, 티어로

using = 실무/사이드에서 직접 다룸 · familiar = 다뤄봤고 더 배우는 중

Backendlang/runtime
  • Pythonusing
  • FastAPIusing
  • asynciousing
  • Pydanticusing
  • Kafka (AIOKafkaConsumer)using
  • SSEusing
  • SQLite / SQLfamiliar
  • REST API 설계using
Frontendui
  • TypeScriptusing
  • React 19using
  • Next.js 15using
  • Tailwindusing
  • Vite (lib mode)familiar
Infra / Opsdeploy
  • Dockerusing
  • k3s / Kubernetesusing
  • GitLab CIusing
  • MinIOusing
  • Traefik / nginxusing
  • cert-managerfamiliar
  • Terraformfamiliar
~/be $GET /honesty

제가 아는 것과 아직 모르는 것

주니어이기에 경계를 분명히 적습니다. 과장하지 않는 것이 더 믿을 만하다고 생각합니다.

자신 있게 다룰 수 있는 것
  • Python / FastAPI 기반 비동기 API 설계
  • Kafka(AIOKafkaConsumer) 소비
  • SSE 실시간 스트리밍
  • queue·워커·Future 동시성
  • 외부 LLM 호출 오케스트레이션 + 구조화 JSON 추출
  • Docker로 패키징
  • React/Next.js/TypeScript로 데이터를 화면까지 연결
아직 부족하고 배우는 중인 것
  • 대규모 트래픽·동시성을 실제로 받아본 경험
  • 데이터베이스 스키마 설계와 쿼리 튜닝의 깊이
  • 분산 트랜잭션·정확히 한 번 전달 같은 분산 시스템의 보장
  • 견고한 테스트 코드 습관

FLA에서는 제 코드를 다시 읽으며 보안·기능·품질에 걸친 한계 18건을 직접 known-issues로 카탈로그화했습니다. 자기 코드의 약점을 찾아 적어두는 것이 다음에 더 잘 만드는 출발점이라고 믿습니다. 모르는 걸 모른다고 말할 수 있는 주니어를 찾고 계신다면, 이야기 나누고 싶습니다.