본문으로 건너뛰기

메시징

Lumie 백엔드는 RabbitMQ를 통한 비동기 메시징을 지원합니다. 현재는 OMR 배치 채점 워크플로우에서 사용합니다.

libs/messaging

libs/messaging에 Exchange, Queue, Routing Key 상수를 정의합니다. 메시징에 참여하는 모든 모듈이 이 상수를 공유하여 문자열 오타를 방지합니다.

// libs/messaging/src/main/java/com/lumie/messaging/config/RabbitMqConstants.java
public final class RabbitMqConstants {

// Exchange
public static final String LUMIE_COMMANDS_EXCHANGE = "lumie.commands";
public static final String LUMIE_DLX_EXCHANGE = "lumie.dlx";

// Queues — OMR 채점
public static final String GRADING_OMR_REQUEST_QUEUE = "grading.omr-request";
public static final String GRADING_OMR_REQUEST_DLQ = "grading.omr-request.dlq";
public static final String GRADING_OMR_COMPLETED_QUEUE = "grading.omr-completed";

// Routing Keys — OMR 채점
public static final String GRADING_OMR_REQUEST_ROUTING_KEY = "grading.omr.request";
public static final String GRADING_COMPLETED_ROUTING_KEY = "grading.completed";

private RabbitMqConstants() {}
}

OMR 배치 채점 흐름


OmrGradingJobMessage

// exam 모듈/application/dto/message/OmrGradingJobMessage.java
public record OmrGradingJobMessage(
Long jobId,
Long examId,
String tenantSlug, // RabbitListener 스레드에서 테넌트 컨텍스트 복원용
Long tenantId, // RLS 바인딩 핵심 — 누락 시 행이 보이지 않음
List<String> imageKeys
) implements Serializable {}

메시지에 tenantSlugtenantId를 모두 포함시키는 이유: @RabbitListener 스레드는 HTTP 요청 스레드가 아니므로 JwtAuthenticationFilter가 실행되지 않습니다. 리스너에서 TenantContextHolder.setTenant(slug) + TenantContextHolder.setTenantId(id) 를 모두 호출해야 RLS 정책이 정상 동작합니다.


OmrGradingJobListener

// exam 모듈/adapter/in/messaging/OmrGradingJobListener.java
@Slf4j
@Component
@RequiredArgsConstructor
public class OmrGradingJobListener {

private final OmrGradingJobProcessor processor;
private final OmrStoragePort storagePort;

@RabbitListener(queues = RabbitMqConstants.GRADING_OMR_REQUEST_QUEUE)
public void handleOmrGradingJob(OmrGradingJobMessage message) {
// 테넌트 컨텍스트 수동 복원 (HTTP 스레드가 아니므로)
TenantContextHolder.setTenant(message.tenantSlug());
TenantContextHolder.setTenantId(message.tenantId());
try {
processor.startProcessing(message.jobId());
for (String imageKey : message.imageKeys()) {
processor.processImageAndUpdateProgress(
message.jobId(), message.examId(), imageKey, message.tenantSlug());
}
processor.completeJob(message.jobId());
storagePort.deleteObjects(message.imageKeys());
} catch (Exception e) {
processor.failJob(message.jobId());
} finally {
TenantContextHolder.clear(); // ThreadLocal 반드시 정리
}
}
}

RabbitMQ 설정

app/src/main/resources/application.yaml:

spring:
rabbitmq:
host: ${RABBITMQ_HOST:localhost}
port: ${RABBITMQ_PORT:5672}
username: ${RABBITMQ_USERNAME:guest}
password: ${RABBITMQ_PASSWORD:guest}
virtual-host: /

운영 환경에서는 lumie-backend-rabbitmq-secrets Kubernetes Secret에서 자격증명을 주입받습니다. RabbitMQ 운영자(Topology Operator)가 lumie-event 네임스페이스에 User CR을 관리합니다.


메시징 패턴 요약

항목
Exchangelumie.commands (direct)
Dead Letter Exchangelumie.dlx
OMR 요청 Queuegrading.omr-request
OMR 요청 DLQgrading.omr-request.dlq
OMR 완료 Queuegrading.omr-completed
발행 모듈exam
수신 모듈exam (자체 수신 → grading-svc HTTP 위임)

관련 문서