본문으로 건너뛰기

메시징

RabbitMQ는 Lumie 백엔드에서 기본 협업 방식이 아니라 선택적으로 쓰이는 경계입니다. 대부분의 프로세스 내부 후속 작업은 Spring Modulith의 JDBC outbox와 @ApplicationModuleListener에 머무르며, queue 기반 messaging은 주로 exam 모듈이 소유합니다.

이 페이지는 현재 async 계약을 다루는 reference 문서입니다.

소스 경로

Path역할
libs/messaging/src/main/java/com/lumie/messaging/config/RabbitMqConstants.java공용 AMQP 이름과 routing key
app/src/main/resources/application.yamlRabbitMQ 연결 및 Modulith retry 설정
modules/exam/src/main/java/com/lumie/exam/adapter/out/config/RabbitMqConfig.javaRabbitTemplate, JSON message converter, listener container 동작
modules/exam/src/main/java/com/lumie/exam/adapter/out/messaging/JobRequestForwarder.javaoutbox에서 RabbitMQ로 보내는 bridge
modules/exam/src/main/java/com/lumie/exam/adapter/in/messaging/*RabbitMQ callback consumer
modules/billing/src/main/java/com/lumie/billing/adapter/in/event/TenantCreatedListener.java프로세스 내부 Modulith event consumer 예시
modules/staff/src/main/java/com/lumie/staff/adapter/in/event/OwnerRegisteredListener.java프로세스 내부 Modulith event consumer 예시
modules/exam/src/main/java/com/lumie/exam/adapter/in/event/StudentRegisteredListener.java프로세스 내부 Modulith event consumer 예시

런타임 흐름

현재 사용 중인 큐 기반 계약

PurposeProducerMessage contractQueue or routingConsumer
OMR grading requestJobRequestForwarder.onOmrGradingRequested(...)OmrGradingImageMessageexchange lumie.commands, routing key grading.omr.request, queue grading.omr-requestgrading worker
OMR grading callbackgrading workerOmrGradingCallbackRequestqueue grading.omr-callbackOmrGradingCallbackListener
Report generation requestJobRequestForwarder.onReportGenerationRequested(...)ReportGenerationMessageexchange lumie.commands, routing key report.generation.request, queue report.generation-requestreport worker
Report generation callbackreport workerReportCallbackRequestqueue report.generation-callbackReportGenerationCallbackListener

예시 메시지 본문

이 예시는 OmrGradingImageMessage, ReportGenerationMessage, OmrGradingCallbackRequest, ReportCallbackRequest, lumie-worker/contracts/mq-schemas-v1.yaml에서 직접 가져왔습니다.

OMR 요청

{
"jobId": 341,
"examId": 15,
"tenantSlug": "acme",
"imageKey": "tmp/15/omr/20260614-01/page-1.png",
"imageIndex": 0,
"totalImages": 2,
"schemaVersion": 1
}

OMR 콜백

{
"jobId": 341,
"examId": 15,
"tenantSlug": "acme",
"imageKey": "tmp/15/omr/20260614-01/page-1.png",
"imageIndex": 0,
"totalImages": 2,
"success": true,
"error": null,
"phoneNumber": "01012345678",
"totalScore": 92,
"grade": 1,
"results": [
{
"questionNumber": 1,
"studentAnswer": "3",
"correctAnswer": "3",
"score": 5,
"earnedScore": 5,
"questionType": "MULTIPLE_CHOICE"
}
]
}

리포트 요청

{
"jobId": 812,
"examId": 15,
"studentId": 101,
"tenantSlug": "acme",
"reportIndex": 0,
"totalReports": 2,
"schemaVersion": 1
}

리포트 콜백

{
"jobId": 812,
"examId": 15,
"studentId": 101,
"tenantSlug": "acme",
"reportIndex": 0,
"totalReports": 2,
"success": true,
"error": null,
"reportBytes": "<base64-pdf>"
}

Exam 모듈이 Outbox를 사용하는 이유

OmrGradingRequestedEventReportGenerationRequestedEvent는 job row를 저장하는 같은 트랜잭션 안에서 발행됩니다. Spring Modulith가 이를 public.event_publication에 저장하고, 그 뒤 JobRequestForwarder가 commit 후 RabbitMQ로 발행합니다.

이로 인해 얻는 보장은 명확합니다.

  • 트랜잭션이 rollback되면 메시지는 전송되지 않음
  • 트랜잭션은 commit되었지만 broker 전송이 실패하면 publication은 미완료 상태로 남고 재시작 시 재시도됨

RabbitMQ와의 2-phase commit을 보장하는 것은 아닙니다. 보장은 “job은 저장되었고 메시지는 재시도된다”이지, “데이터베이스와 broker가 하나의 XA 트랜잭션으로 원자적으로 commit된다”가 아닙니다.

리스너 동작과 실패 처리

RabbitMqConfig는 다음을 설정합니다.

  • publisher와 listener 모두에 대한 JSON conversion
  • defaultRequeueRejected=false
  • lumie.rabbitmq.missing-queues-fatal로 제어 가능한 missingQueuesFatal

중요한 결과는 다음과 같습니다.

  • listener exception이 같은 consumer thread에서 무한 requeue loop를 만들지 않음
  • 백엔드는 broker 측 delivery-limit과 DLQ policy가 redelivery를 세고, 결국 실패를 lumie.dlx로 보내길 기대함

exam callback listener는 또한 다음을 수행합니다.

  • 비어 있거나 알 수 없는 tenantSlug를 가진 메시지 거부
  • TenantService를 통해 tenantSlug -> tenantId 해석
  • TenantContextHolder.withinContext(...)로 tenant context 복원
  • 성공과 실패에 대해 구조화된 background-job field를 로그

JVM 밖으로 나가지 않는 인프로세스 Modulith 이벤트

이들도 commit 후 비동기이지만 RabbitMQ 메시지는 아닙니다.

EventProducer moduleConsumer modulePurpose
TenantCreatedEventtenantbilling초기 trial subscription provision
OwnerRegisteredEventauthstaffOWNER staff row bootstrap
StudentRegisteredEventstudentexam이전에 매칭되지 않은 exam result backfill

이 event들도 queue 기반 exam event와 동일한 event_publication durability 메커니즘을 사용합니다.

동기 또는 HTTP 기반으로 남는 것

  • AI chat는 HTTP로 chatbot-svc에 요청하고, 이 worker는 /internal/chatbot/**로 callback합니다.
  • OmrServiceClient.gradeOmrImage(...)는 여전히 단일 이미지 채점 계약을 위한 직접 HTTP 경로입니다.
  • ReportServiceClient.generateReport(...)도 동기 report 생성을 위한 직접 HTTP 경로입니다.
  • billing webhook은 /internal/webhooks/toss/billing으로 HTTP로 들어옵니다.
  • 일반 모듈 간 협업은 RabbitMQ가 아니라 libs/internal-api를 사용합니다.

검토 중 발견된 계약 드리프트

  • RabbitMqConstants는 여전히 GRADING_OMR_COMPLETED_QUEUE = "grading.omr-completed"를 정의하지만, 현재 백엔드 listener와 exam internal controller는 grading.omr-callback을 사용합니다.
  • 이 페이지의 문서는 현재 활성 런타임 계약인 live consumer code (OmrGradingCallbackListener)를 따릅니다.

검증 명령어

cd /Users/bluemayne/Projects/Lumie/lumie-backend
./gradlew test
./gradlew :modules:exam:test
./gradlew :app:test

durability 경로에 대해 더 강한 확신이 필요하면 다음도 실행하세요.

./gradlew integrationTest

예상 성공 신호:

  • Gradle이 BUILD SUCCESSFUL로 종료되고, exam callback listener가 여전히 wrapper envelope 없이 문서화된 JSON body를 역직렬화합니다.
  • OmrGradingCallbackListener는 여전히 grading.omr-callback에, ReportGenerationCallbackListener는 여전히 report.generation-callback에 바인딩되며, 요청 publisher는 여전히 exchange lumie.commands를 사용합니다.

관련 페이지