본문으로 건너뛰기

아키텍처 개요

Lumie는 학원을 위한 멀티테넌트 SaaS 교육 플랫폼입니다. 이 제품은 Next.js 프론트엔드 웹 애플리케이션, 핵심 비즈니스 로직을 담당하는 단일 Spring Boot 모듈러 모놀리스, 그리고 별도의 런타임이나 확장 모델이 더 적합한 워크로드를 처리하는 소수의 독립 FastAPI 워커로 구성됩니다. 테넌트 격리는 테넌트별 별도 스키마가 아니라 공유 public schema에서 PostgreSQL Row Level Security(RLS)로 강제됩니다.

소스 앵커

경로무엇을 고정하는가
lumie-frontend/app/api/[...path]/route.tssame-origin /api/... 프록시, 업스트림 헤더 정리, localhost 쿠키 재작성
lumie-backend/app/src/main/java/com/lumie/app/LumieApplication.java백엔드 배포 단위의 단일 Spring Boot 진입점
lumie-backend/app/build.gradle.kts공유 라이브러리와 모든 비즈니스 모듈을 하나의 jar로 묶는 단일 backend app 모듈
lumie-backend/settings.gradle.kts체크인된 백엔드 모듈 인벤토리
lumie-backend/libs/common/src/main/java/com/lumie/common/domain/TenantScopedEntity.java테넌트 범위 엔티티 계약과 tenant_id 채움 방식
lumie-backend/app/src/main/java/com/lumie/app/config/RuntimeDbRoleGuard.javaRLS를 우회할 수 있는 PostgreSQL role로 실행을 거부하는 부팅 시 가드
lumie-backend/app/src/test/java/com/lumie/app/migration/MigrationsRlsIntegrationTest.java제한된 런타임 role에서 RLS 모델을 증명하는 교차 테넌트 격리 테스트
lumie-worker/services/{analysis,grading,report,chatbot}/pyproject.toml독립적인 FastAPI 워커 런타임과 서비스별 의존성 형태
.github/tilt-up.sh일상 개발에서 쓰는 로컬 프론트엔드 + 클러스터 기반 개발 분리 방식

시스템 구조

  • 프론트엔드는 직원, 학생, 온보딩, 마케팅 화면을 포함한 사용자 대상 웹 경험을 제공하는 단일 Next.js 애플리케이션입니다.
  • 백엔드는 lumie-backend/app에서 시작되는 하나의 Spring Boot 배포 단위입니다. lumie-backend/modules/* 아래의 모듈은 모놀리스 내부의 논리적 경계이며, 별도로 배포되는 서비스가 아닙니다.
  • 백엔드 모듈 간 호출은 lumie-backend/libs/internal-api의 인터페이스를 통해 라우팅되며, 이를 통해 애플리케이션을 네트워크 기반 마이크로서비스로 분리하지 않으면서도 모듈 경계를 명시적으로 유지합니다.
  • lumie-worker/services/* 아래의 워커는 별도로 배포되는 FastAPI 서비스입니다. 이들은 OMR 채점, 리포트 생성, 분석, 챗봇 오케스트레이션 같은 특화 워크로드를 처리합니다.
  • PostgreSQL, RabbitMQ, Redis, MinIO, 관측성 스택 같은 플랫폼 서비스는 제품 코드와 분리되어 lumie-infra에서 관리됩니다.
  • 테넌트 데이터는 공유 PostgreSQL public schema에 저장됩니다. 테넌트 범위 테이블은 tenant_id를 가지며, 런타임 데이터베이스 role은 의도적으로 RLS에 묶여 있으므로 Lumie는 schema-per-tenant 격리에 의존하지 않습니다.

런타임 요청 경로

  1. 브라우저가 Next.js 프론트엔드에서 페이지를 로드합니다.
  2. 브라우저 측 애플리케이션 호출은 same-origin /api/v1/... 경로를 사용하고, Next.js 프록시가 이를 구성된 백엔드 API origin으로 전달합니다.
  3. 백엔드는 사용자를 인증하고, 요청에서 테넌트 컨텍스트를 해석한 뒤, 모놀리스 내부의 적절한 모듈로 호출을 디스패치합니다.
  4. 활성 트랜잭션 내부에서 백엔드는 app.tenant_id를 바인딩하여 PostgreSQL RLS가 현재 테넌트의 row만 노출하도록 합니다.
  5. 모듈은 PostgreSQL 데이터를 읽거나 쓰고, 필요하면 파일 기반 워크플로를 위해 MinIO를 사용한 뒤, 프론트엔드를 거쳐 브라우저로 응답을 반환합니다.

로컬 개발에서는 이 경로가 의도적으로 분리됩니다. 일반적으로 Next.js 앱은 개발자 머신에서 실행되고, 백엔드, 워커, 상태 저장 서비스는 Tilt로 관리되는 dev 클러스터에서 실행됩니다.

데이터 및 이벤트 흐름

트랜잭션성 애플리케이션 데이터

대부분의 제품 상태는 모듈러 모놀리스 뒤에 존재합니다. 사용자 액션은 먼저 백엔드에 도달하고, 백엔드는 PostgreSQL 읽기와 쓰기에 대한 트랜잭션 경계를 소유합니다. 이렇게 하면 권한 부여, 테넌시, 검증, 비즈니스 규칙을 한곳에 둘 수 있습니다.

테넌트 격리

Lumie는 공유 public schema의 테넌트 범위 테이블에 PostgreSQL Row Level Security를 사용합니다. 활성 테넌트는 트랜잭션마다 app.tenant_id를 통해 바인딩되며, 백엔드 코드는 이 전제를 중심으로 작성됩니다. 즉, 테넌트 경계는 테넌트별 스키마 전환이 아니라 데이터베이스 계층에서 강제됩니다.

큐 기반 백그라운드 작업

무거운 비동기 워크플로는 RabbitMQ를 사용합니다. 채점과 리포트 생성의 경우, 백엔드는 같은 트랜잭션 안에서 job row와 event publication record를 함께 기록한 뒤 커밋 후에 event를 RabbitMQ로 전달합니다. 워커 서비스는 이 메시지를 소비해 백그라운드 작업을 수행하고, 완료 콜백을 보내며, 백엔드는 이를 소비해 애플리케이션 상태를 업데이트합니다.

직접 워커 호출

모든 워커가 queue 기반으로 동작하는 것은 아닙니다. AI 기능 표면은 상호작용 모델에 더 잘 맞는 경우 직접 HTTP 호출도 사용합니다. 예를 들어 백엔드는 채팅 트래픽을 chatbot-svc로 프록시하고, 워커는 테넌트 안전한 읽기, 쓰기, 대화 영속화가 필요할 때 내부 백엔드 엔드포인트를 호출합니다.

배포 경계

  • 백엔드는 많은 모듈로 개발되더라도 하나의 애플리케이션으로 배포됩니다. 모듈 경계는 코드 수준의 경계이지 배포 경계가 아닙니다.
  • 워커 서비스는 독립적으로 배포되며, queue depth나 워크로드 형태에 따라 백엔드와 별도로 확장할 수 있습니다.
  • 인프라는 K3s 위에 프로비저닝되고 lumie-infra의 GitOps를 통해 reconcile되며, ArgoCD가 원하는 클러스터 상태를 관리합니다.
  • PostgreSQL, RabbitMQ, Redis, MinIO, 시크릿 관리, 관측성 같은 공용 플랫폼 기능은 제품 저장소가 아니라 인프라 계층에 속합니다.

검증 및 성공 신호

이 확인 절차는 아키텍처 계약 수준에 머물며 런타임 상태를 변경하지 않습니다.

cd /Users/bluemayne/Projects/Lumie
rg -n "NEXT_PUBLIC_API_BASE|headers.delete\\('origin'\\)|set-cookie" \
'lumie-frontend/app/api/[...path]/route.ts'
rg -n "SpringApplication.run|project\\(\\\":modules:|spring-modulith|springdoc-openapi" \
lumie-backend/app/src/main/java/com/lumie/app/LumieApplication.java \
lumie-backend/app/build.gradle.kts \
lumie-backend/settings.gradle.kts
rg -n "tenant_id|NOBYPASSRLS|app\\.tenant_id|RLS isolation" \
lumie-backend/libs/common/src/main/java/com/lumie/common/domain/TenantScopedEntity.java \
lumie-backend/app/src/main/java/com/lumie/app/config/RuntimeDbRoleGuard.java \
lumie-backend/app/src/test/java/com/lumie/app/migration/MigrationsRlsIntegrationTest.java
rg -n "fastapi==|aio-pika==|httpx==|openai==" \
lumie-worker/services/analysis/pyproject.toml \
lumie-worker/services/grading/pyproject.toml \
lumie-worker/services/report/pyproject.toml \
lumie-worker/services/chatbot/pyproject.toml

성공 신호:

  • 프론트엔드 프록시는 여전히 브라우저 전용 헤더를 제거한 뒤 전달하고, localhost 개발을 위해 Set-Cookie를 재작성합니다.
  • 백엔드는 여전히 하나의 Spring Boot 진입점과, 모듈 그래프를 단일 배포 단위로 조립하는 하나의 app 모듈을 유지합니다.
  • RLS 계약은 여전히 두 겹으로 방어됩니다. 시작 시점에는 RuntimeDbRoleGuard, 통합 테스트에서는 MigrationsRlsIntegrationTest가 이를 검증합니다.
  • 워커 서비스는 백엔드 런타임에 흡수되지 않고 별도의 FastAPI 배포 단위로 유지됩니다.

다음에 볼 문서