본문으로 건너뛰기

아키텍처

Lumie 백엔드 아키텍처는 하나의 Spring Boot 프로세스 내부에서 코드 수준의 모듈 경계로 정의됩니다. 핵심 설계 결정은 서비스 간 네트워크 분리가 아니라, 모놀리스 내부에서 의존성 방향을 엄격히 지키는 것입니다.

이 페이지는 경계 중심의 overview 문서입니다. 비즈니스 규칙이 어디에 살아 있는지, 모듈이 어떻게 협력해야 하는지, 어떤 공유 코드가 시스템에서 중요한 역할을 맡는지 이해할 때 사용하세요.

소스 경로

Path역할
lumie-backend/AGENTS.md표준 백엔드 모듈 레이아웃과 multi-tenancy 규칙
.codex/knowledge/backend/BACKEND_RULES.md백엔드 작업에 적용되는 경로 수준 아키텍처 규칙
lumie-backend/app/src/main/java/com/lumie/app/LumieApplication.java단일 @SpringBootApplication 엔트리포인트
lumie-backend/settings.gradle.kts현재 포함된 모듈 목록
lumie-backend/libs/common/src/main/java/com/lumie/common/tenant/*tenant context, request context, RLS binding
lumie-backend/libs/internal-api/src/main/java/**모듈 간에 공개된 프로세스 내부 계약
lumie-backend/modules/*/src/main/java/**모듈이 소유하는 domain, application, adapter 코드

경계 모델

표준 모듈 레이아웃

lumie-backend/AGENTS.mdBACKEND_RULES.md는 표준 형태를 다음과 같이 정의합니다.

modules/{module}/src/main/java/com/lumie/{package}/
├── domain/{entity,vo,exception}/
├── application/{service,port/out,dto/{request,response}}/
└── adapter/{in/web,in/messaging,in/internal,out/persistence,out/external}

현재 코드베이스에서 각 계층의 의미는 다음과 같습니다.

  • domain: persistence-backed aggregate, value object, 모듈별 error code를 담습니다. 이 모놀리스에서 aggregate에 JPA annotation을 두는 것은 의도된 설계입니다.
  • application/service: use case orchestration, transaction boundary, 여러 port 간 coordination을 담당합니다.
  • application/port/out: 모듈이 소유하는 outbound dependency를 정의합니다.
  • adapter/in/*: inbound transport 또는 integration entrypoint입니다.
  • adapter/out/*: persistence, storage, queue, cache, external-service 구현입니다.

의도적으로 없는 것:

  • application/port/in/*UseCase 계층은 없습니다. 컨트롤러는 application service를 직접 주입받습니다.
  • infrastructure/ 패키지는 없습니다.
  • 다른 모듈의 domain/entity 타입을 직접 import하지 않습니다.

공통 라이브러리와 그 경계

Library모듈이 사용할 수 있는 용도사용하면 안 되는 용도
libs/commontenant context, user context, base entity, exception, idempotency, logging, auth helper, pagination utility제품 도메인 계약을 공개하는 용도
libs/internal-api동기식 프로세스 내부 service interface와 모듈 간 event recordpersistence adapter나 JPA entity 공유
libs/messagingAMQP 기반 흐름에 쓰이는 queue, exchange, routing-key 상수queue topology나 비즈니스 orchestration 선언

허용되는 협업 패턴

동기식 모듈 간 호출

libs/internal-api 인터페이스와, 해당 모듈이 소유하는 adapter/in/internal/*Adapter 구현을 사용합니다.

현재 코드의 실제 예시:

  • modules/homepage/adapter/out/internal/TenantLookupAdaptercom.lumie.tenant.api.TenantService에 의존합니다
  • modules/staff/application/service/StaffCommandServiceAuthService, BillingService, ClassService, ContentService에 의존합니다
  • modules/exam/adapter/in/event/StudentRegisteredListenerExamService에 의존합니다

비동기 모듈 간 후속 처리

public.event_publication에 저장되는 Spring Modulith event를 사용하고, 이후 @ApplicationModuleListener가 이를 소비합니다.

현재 예시:

  • TenantCreatedEvent -> billing trial provisioning
  • OwnerRegisteredEvent -> owner staff bootstrap
  • StudentRegisteredEvent -> exam-result backfill

외부 프로세스 경계

worker와 third-party HTTP API는 outbound integration으로 취급합니다.

  • exam -> grading-svc, report-svc, RabbitMQ, MinIO
  • ai -> chatbot-svc
  • billing -> Toss Payments와 현재 stub 상태인 Popbill adapter

모듈은 서로를 /v1/** route를 통해 호출해서는 안 됩니다.

컨텍스트 전파는 부수적 요소가 아니라 아키텍처입니다

백엔드의 아키텍처 경계는 Java와 PostgreSQL 양쪽에서 함께 강제되므로, context 전파는 부수적인 요소가 아니라 핵심 구성입니다.

  • JwtAuthenticationFilterInternalHmacAuthFilter는 컨트롤러 코드가 실행되기 전에 tenant와 user context를 채웁니다.
  • RlsTenantContextAspect@Transactional 진입 시 app.tenant_id를 바인딩합니다.
  • TenantAwareTaskDecoratorConfig는 Spring이 관리하는 async 실행으로 tenant, user, MDC, security context를 복사합니다.
  • 이 경로를 우회하는 호출 지점은 반드시 TenantContextHolder.withinContext(...)로 context를 명시적으로 복원해야 합니다.

알아둘 만한 네이밍 및 패키징 드리프트

  • modules:class는 Java package로 com.lumie.classroom을 사용합니다.
  • 문서 route인 Staff Service는 여전히 modules/staff Gradle subproject를 가리킵니다.
  • modules/activity-log는 디스크에는 있지만 settings.gradle.kts에 포함되어 있지 않습니다.

이들은 별도 런타임 서비스가 아니라 문서나 패키징 측면의 특이사항입니다.

아키텍처 실패 모드

  • 다른 모듈의 aggregate type을 import하면 공개된 경계를 우회하면서 repository, transaction, tenancy 가정을 서로 결합하게 됩니다.
  • @Transactional 메서드 안에서 외부 HTTP를 호출하면 네트워크 왕복 동안 데이터베이스 작업이 열린 채로 유지되며, 백엔드 규칙은 이를 명시적으로 금지합니다.
  • 트랜잭션이어야 할 helper를 self-invocation으로 호출하면 Spring proxy를 우회하여 RLS binding이 생략될 수 있습니다. public homepage 경로에서 이 문제를 피하려고 HomepageQueryService.Tx가 별도로 존재합니다.
  • AI 모듈의 전용 virtual-thread executor처럼 Spring이 관리하지 않는 async 코드는 context를 수동으로 다시 설정해야 합니다.

검증 명령어

cd /Users/bluemayne/Projects/Lumie/lumie-backend
./gradlew test
./gradlew :libs:common:test
./gradlew :modules:homepage:test
./gradlew :modules:staff:test

유용한 경계 중심 테스트:

  • libs/common/src/test/java/com/lumie/common/tenant/RlsTenantContextAspectIntegrationTest.java
  • modules/homepage/src/test/java/com/lumie/homepage/application/service/HomepageQueryServiceTest.java
  • modules/staff/src/test/java/com/lumie/staff/application/service/StaffCommandServiceTest.java

관련 페이지