메시징
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.yaml | RabbitMQ 연결 및 Modulith retry 설정 |
modules/exam/src/main/java/com/lumie/exam/adapter/out/config/RabbitMqConfig.java | RabbitTemplate, JSON message converter, listener container 동작 |
modules/exam/src/main/java/com/lumie/exam/adapter/out/messaging/JobRequestForwarder.java | outbox에서 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 예시 |
런타임 흐름
현재 사용 중인 큐 기반 계약
| Purpose | Producer | Message contract | Queue or routing | Consumer |
|---|---|---|---|---|
| OMR grading request | JobRequestForwarder.onOmrGradingRequested(...) | OmrGradingImageMessage | exchange lumie.commands, routing key grading.omr.request, queue grading.omr-request | grading worker |
| OMR grading callback | grading worker | OmrGradingCallbackRequest | queue grading.omr-callback | OmrGradingCallbackListener |
| Report generation request | JobRequestForwarder.onReportGenerationRequested(...) | ReportGenerationMessage | exchange lumie.commands, routing key report.generation.request, queue report.generation-request | report worker |
| Report generation callback | report worker | ReportCallbackRequest | queue report.generation-callback | ReportGenerationCallbackListener |
예시 메시지 본문
이 예시는 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를 사용하는 이유
OmrGradingRequestedEvent와 ReportGenerationRequestedEvent는 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=falselumie.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를 로그