Conversation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, grpc 설정 추가) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@RetryableTopic 전환 이후 DLT 발생 시 Inbox 상태가 업데이트되지 않던 문제를 해결합니다. 각 리스너에 @DltHandler를 추가하여 재시도 소진 후 DLT로 전환될 때 해당 Inbox 레코드를 DEAD_LETTERED로 마킹합니다. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@RetryableTopic 전환 이후 DLT 발생 시 Inbox 상태가 업데이트되지 않던 문제를 해결합니다. 각 리스너에 @DltHandler를 추가하여 재시도 소진 후 DLT로 전환될 때 해당 Inbox 레코드를 DEAD_LETTERED로 마킹합니다. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AuthEventKafkaListener: @RetryableTopic, @DltHandler 추가 및 UserInboxRepository 주입 - SseConnectedKafkaListener, SseDisconnectedKafkaListener: @RetryableTopic 추가 - KafkaConfiguration: @EnableKafkaRetryTopic, ThreadPoolTaskScheduler 빈 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AuthEventKafkaListener: @RetryableTopic, @DltHandler 추가 및 UserInboxRepository 주입 - SseConnectedKafkaListener, SseDisconnectedKafkaListener: @RetryableTopic 추가 - KafkaConfiguration: @EnableKafkaRetryTopic, ThreadPoolTaskScheduler 빈 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 토큰 검증(waiting gRPC)을 @transactional 이전에 호출하도록 분리 - increaseSoldQuantity gRPC 제거 → ReleaseSoldQuantityIncreaseEvent Outbox 비동기 발행으로 교체 - ReleaseGrpcAdapter, ReleaseIncreaseSoldQuantityPort, ReleaseServiceUnavailableException 삭제 - release-service gRPC 채널 설정 및 releaseService CircuitBreaker/TimeLimiter 설정 제거 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- order 서비스가 발행하는 release-sold-quantity-increase Outbox 이벤트 수신 - Inbox 패턴 인프라 추가: @idempotent AOP, ReleaseInboxEntity/Repository, IdempotentAspect - HandleReleaseSoldQuantityIncreaseService: 기존 IncreaseSoldQuantityService와 동일 도메인 로직 재사용 - build.gradle: spring-boot-starter-kafka, aspectj 의존성 추가 - application-dev/prod/local.yml: Kafka consumer 설정 및 inbox 설정 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AUTO_CONFIRM_DAYS 하드코딩 제거 → order.auto-confirm.days/chunk-size 설정 외부화 - findDeliveredBefore에 limit(Pageable) 추가로 전체 로드 방지 - 단일 트랜잭션 전체 배치 → AutoConfirmOrderChunkService.confirmOne()으로 분리 self-invocation 문제 해결, 한 건 실패해도 다른 건 롤백 없이 계속 처리 - Redisson 분산 락 적용(@SchedulerLock): 멀티 인스턴스 중복 실행 방지 - spring.data.redis 설정 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
f-lab-ted
left a comment
There was a problem hiding this comment.
Release Aggregate(상태 SCHEDULED → ON_SALE → SOLD_OUT/ENDED, 한정 수량/판매 수량/판매 예정 시각 관리)와 헥사고널 구성(REST/gRPC/Kafka/JPA/Scheduler), creator 서비스로의 gRPC 호출 + Resilience4j CircuitBreaker, 10분 단위 판매 시작 시각 강제 Validation, 자동 판매 시작 스케줄러, release-sold-quantity-increase Kafka Inbox 처리를 함께 검토했습니다. 또한 다른 서비스(auth/creator)의 Inbox DEAD_LETTERED 만료 처리/@RetryableTopic 전환, URL prefix /api/creator → /creator//admin/creator 변경 영향도 함께 살펴봤습니다.
잘한 점
DDD/헥사고널 경계가 명확하게 분리되어 있고(core ↔ port ↔ adapter), Release Aggregate에 상태 전이 규칙과 검증을 집중시킨 점, gRPC ↔ Resilience4j ↔ deadline 조합으로 외부 호출 안정성을 챙긴 점은 좋습니다.
보완할 점 (Critical/Major 위주)
- Critical
ReleaseGrpcService.toProtoRelease가 proto의created_at필드에scheduledAt값을 매핑하고 있어, 호출 측(creator 등)에서 의미가 달라진 데이터를 받게 됩니다. proto 정의를scheduled_at/status/limited_quantity/sold_quantity까지 포함하도록 보강하고 매핑을 일치시켜야 합니다. - Critical
StartSaleScheduler가 분산 락 없이 1분 주기로 돌고 있어 다중 인스턴스 환경에서 같은 SCHEDULED 릴리즈를 동시에 잡아OptimisticLockingFailureException또는 중복 startSale 시도가 발생합니다. 최근 order 서비스의 AutoConfirm 스케줄러에 적용된 분산 락 + 청크 처리 패턴을 동일하게 적용해 주세요. 또한 PR 설명의 "10분 주기"와 코드의fixedDelay = 60000(1분) 불일치도 정정 필요합니다. - Major
IncreaseSoldQuantityUseCase가 gRPC(ReleaseGrpcService#increaseSoldQuantity)와 Kafka(OrderEventKafkaListener) 두 경로 모두에서 호출됩니다. 동일 주문에 대해 양쪽이 모두 발화하면 이중 차감/oversell 위험이 있고, 책임 분리가 모호합니다. 비동기(Outbox→Inbox) 단일 경로로 통일하거나 gRPC 경로를 제거하는 것이 안전합니다. - Major
/admin/release/*가SecurityConfiguration의anyRequest().permitAll()정책 하에 인증/관리자 권한 검사 없이 노출됩니다. 게이트웨이에서 prefix-기반 보호가 있더라도 서비스 자체에 방어선이 필요합니다. - Major
RegisterReleaseService#registerRelease내부에서 creator gRPC 호출이@Transactional안에 포함되어 있어, 외부 호출(최대 2초 + 서킷브레이커 전환) 동안 DB 트랜잭션이 유지됩니다. gRPC 호출은 트랜잭션 밖에서 수행한 뒤 결과만 들고 트랜잭션을 시작하도록 분리해 주세요.
그 외 가이드에서 지적된 IdempotentAspect.isTooOld 중복 저장 위험, @DltHandler ↔ retry topic 이름 불일치, auth/creator 간 @RetryableTopic 비대칭, FRONTEND_URI→URL 환경변수 변경, creator gRPC 채널 일관성도 같은 맥락의 보완 대상입니다.
결론
Request Changes — 1번 proto 매핑(데이터 정합성), 2번 스케줄러 분산 락(다중 인스턴스 안전성), 3번 increaseSoldQuantity 이중 경로(이중 차감), 4번 admin 보호 부재 점에 대해서는 반드시 수정 바랍니다.
| .setThumbnailUrl(result.thumbnailUrl() != null ? result.thumbnailUrl() : "") | ||
| .setLevel(toProtoLevel(result.level())) | ||
| .setPrice(result.price()) | ||
| .setCreatedAt(Timestamp.newBuilder() |
There was a problem hiding this comment.
[Critical] proto created_at 필드에 scheduledAt 값을 매핑하고 있습니다.
.setCreatedAt(Timestamp.newBuilder()
.setSeconds(scheduledAt.getEpochSecond())
.setNanos(scheduledAt.getNano())
.build())- 호출 측(creator 서비스의
ReleaseQueryGrpcAdapter등)은 응답의created_at을 실제 생성 시각으로 신뢰합니다. 여기에 판매 예정 시각이 들어가면 정렬/노출 로직이 모두 어긋납니다. - 그리고
getReleasesByCreatorId응답에는status,scheduled_at,limited_quantity,sold_quantity같은 release 의 핵심 속성이 누락되어 있어 order/waiting 같은 호출 측에서 다시 확인 호출이 필요해질 가능성이 큽니다.
제안: proto 정의에 created_at/scheduled_at/status/limited_quantity/sold_quantity 를 추가하고, 매핑을 의미에 맞게 분리해 주세요. (특히 created_at은 result.createdAt()을 써야 합니다.)
|
|
||
| private final AutoStartSaleUseCase autoStartSaleUseCase; | ||
|
|
||
| @Scheduled(fixedDelay = 60000) |
There was a problem hiding this comment.
[Critical] 분산 락 없이 1분 주기로 동작하여 다중 인스턴스에서 충돌 위험이 큽니다.
AutoStartSaleService#autoStartScheduledReleases는findScheduledBefore(now)로 모든 대상 릴리즈를 한 트랜잭션에서 가져와release.startSale()+update()를 반복 호출합니다. 여러 pod 가 같은 SCHEDULED 행에 대해 동시에 진입하면@Version기반 낙관락 충돌로 일부 instance 가 예외와 함께 종료되고, 다음 주기까지 시작이 지연됩니다.- 최근 order 서비스의
AutoConfirm스케줄러에 적용한 "분산 락 + 청크 처리" 패턴을 동일하게 도입하는 것이 자연스럽습니다 (ShedLock / Redisson 등). - PR 설명에는 "10분 단위 자동 판매 시작"이라고 적혀있지만
fixedDelay = 60000(1분)으로 설정되어 있어 설명과 코드가 불일치합니다. 의도가 1분이라면 PR 문서를 수정하고, 10분이 의도라면fixedDelayString = "PT10M"또는cron으로 명시해 주세요.
@Scheduled(fixedDelay = 60000) // 1분
public void autoStartScheduledReleases() { ... }부가적으로, @ScheduledAtMultipleOfTenMinutes 로 등록 시각을 10분 단위로 강제했다면 스케줄러도 10분 주기(또는 cron 0 */10 * * * *)로 정렬하는 것이 의미 충돌이 적습니다.
| @Override | ||
| public void increaseSoldQuantity(IncreaseSoldQuantityRequest request, | ||
| StreamObserver<IncreaseSoldQuantityResponse> responseObserver) { | ||
| increaseSoldQuantityUseCase.increaseSoldQuantity(ReleaseId.of(request.getReleaseId())); |
There was a problem hiding this comment.
[Major] increaseSoldQuantity 동기 gRPC 경로 / Kafka Inbox 경로가 중복됩니다.
같은 도메인 행위(Release#increaseSoldQuantity)가 두 진입점에서 호출됩니다.
- 동기:
ReleaseGrpcService#increaseSoldQuantity(RPC) - 비동기:
OrderEventKafkaListener#handleReleaseSoldQuantityIncrease(Kafka Inbox)
위험 요소:
- 주문 서비스가 두 경로를 모두 사용하면 동일 주문에 대해 sold_quantity 가 2회 증가하여 oversell 됩니다.
- gRPC 경로에는 멱등성 보호(Inbox/eventId 검증)가 없어, 클라이언트가 재시도하면 그대로 또 증가합니다.
- 두 경로 모두
@Version낙관락에 의존하기 때문에 단순 충돌 시 예외만 던지고 재시도 정책이 없습니다.
제안: 주문→릴리즈 sold quantity 증가는 Outbox→Inbox(Kafka) 단일 경로로 통일하고 gRPC increaseSoldQuantity 엔드포인트는 제거하거나, 반대로 동기 차감을 유지하려면 Inbox 리스너를 제거하고 멱등키(eventId)에 대한 명시적 중복 방지를 gRPC에 도입해 주세요.
| .csrf(AbstractHttpConfigurer::disable) | ||
| .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) | ||
| .addFilterBefore(new UserInfoExtractFilter(trustedIpProperties), UsernamePasswordAuthenticationFilter.class) | ||
| .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) |
There was a problem hiding this comment.
[Major] /admin/release/* 가 인증/권한 없이 노출되어 있습니다.
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())AdminReleaseCommandController(start-sale / end-sale)는 운영자만 호출해야 하는데, 현재 설정에선 외부에서 누구나 호출 가능합니다. 게이트웨이가 보호한다 하더라도 서비스 자체적으로:
/admin/**경로는UserInfoExtractFilter가 주입한 ROLE_ADMIN 권한 보유자만 통과하도록requestMatchers("/admin/**").hasRole("ADMIN")추가- 또는 컨트롤러 메서드에
@PreAuthorize("hasRole('ADMIN')")적용
이 필요합니다. 외부 노출 위험이 큰 상태 전이(판매 시작/종료) 엔드포인트라 우선순위를 높여 봐주세요.
There was a problem hiding this comment.
이것은 AuthorizationPolicy를 통해 해결할 예정입니다
|
|
||
| @Override | ||
| @Transactional | ||
| public Long registerRelease(RegisterReleaseCommand command) { |
There was a problem hiding this comment.
[Major] gRPC 외부 호출이 @Transactional 경계 안에 포함되어 있습니다.
@Transactional
public Long registerRelease(RegisterReleaseCommand command) {
CreatorId creatorId = creatorIdQueryPort.getCreatorId(command.userId()); // gRPC + CircuitBreaker
Release release = Release.createBuilder()....build();
return releaseRepositoryPort.save(release);
}getCreatorId는 최대 2초 deadline + Circuit half-open 상황까지 고려하면 응답이 늦어질 수 있는데, 그 동안 DB 커넥션이 점유됩니다.- creator 서비스가 NOT_FOUND 가 아닌 다른 원인으로 실패하면
CreatorServiceUnavailableException까지 트랜잭션 안에서 처리됩니다(불필요한 트랜잭션 rollback 비용).
제안: creator 조회는 트랜잭션 밖에서 수행 후, 검증된 creatorId 만 들고 짧은 트랜잭션 안에서 저장하도록 분리하는 것을 권장합니다.
public Long registerRelease(RegisterReleaseCommand command) {
CreatorId creatorId = creatorIdQueryPort.getCreatorId(command.userId());
return saveNewRelease(creatorId, command); // 이 안에 @Transactional
}| @Override | ||
| @Transactional | ||
| public void autoStartScheduledReleases() { | ||
| List<Release> releases = releaseRepositoryPort.findScheduledBefore(Instant.now()); |
There was a problem hiding this comment.
[Major] 대상 릴리즈 전체를 한 트랜잭션에서 순차 처리합니다.
List<Release> releases = releaseRepositoryPort.findScheduledBefore(Instant.now());
releases.forEach(release -> {
release.startSale();
releaseRepositoryPort.update(release);
});- 누락된 시작 예정 건이 수십~수백 건 쌓이면 단일 트랜잭션이 길어지고 한 건 실패 시 모두 롤백됩니다.
release.startSale()은SCHEDULED가 아닐 때ReleaseStatusConflictException을 던지는데, 그 사이 다른 인스턴스/이벤트가 상태를 바꿔놓았다면 전체 작업이 무산됩니다.
제안: order 서비스의 AutoConfirm 청크 처리 패턴(분산 락 + 페이지 단위 + 개별 트랜잭션)을 적용해 부분 실패 격리를 확보해 주세요. 최소한 release.startSale() 을 try/catch 로 감싸 한 건 실패가 다른 건을 막지 않도록 해야 합니다.
| Long eventId = Long.parseLong(consumerRecord.key()); | ||
| InboxEvent event = (InboxEvent) consumerRecord.value(); | ||
| Long eventId = event.eventId(); | ||
| Instant occurredAt = event.occurredAt(); |
There was a problem hiding this comment.
[Major] 만료 메시지 DEAD_LETTERED 저장 시 existsByEventId 중복 체크가 누락되어 있습니다.
if (isTooOld(occurredAt)) {
transactionTemplate.executeWithoutResult(status ->
authInboxRepository.save(AuthInboxEntity.builder()
.eventId(eventId)
...
.status(AuthInboxStatus.DEAD_LETTERED)
.build())
);
return null;
}동일 eventId 가 (정상 처리된 후) 재전달되어 만료 판정을 받게 되면, 위 분기는 정상 처리 분기와 달리 중복 체크 없이 곧바로 save 를 호출하므로 event_id UNIQUE 제약 위반이 발생합니다. 또한 같은 메시지가 retry로 여러 번 들어와 모두 만료 처리되면 동일하게 위반됩니다.
제안:
if (isTooOld(occurredAt)) {
if (authInboxRepository.existsByEventId(eventId)) {
log.info("[Inbox] 이미 기록된 만료 이벤트. eventId={}", eventId);
return null;
}
// save DEAD_LETTERED ...
}creator 측 IdempotentAspect 도 동일 구조이므로 같이 보완해 주세요.
| @DltHandler | ||
| public void handleDlt(ConsumerRecord<String, ?> consumerRecord) { | ||
| log.error("[Inbox] DLT 전환. topic={}, partition={}, offset={}", consumerRecord.topic(), consumerRecord.partition(), consumerRecord.offset()); | ||
| creatorInboxRepository.findByTopicAndPartitionAndOffset(consumerRecord.topic(), consumerRecord.partition(), consumerRecord.offset()) |
There was a problem hiding this comment.
[Major] @DltHandler 안에서 consumerRecord.topic() 으로 Inbox 를 조회하면 원본 토픽이 아니라 retry 토픽을 받게 됩니다.
@RetryableTopic 사용 시 메시지는 outbox.event.user-withdrawn → outbox.event.user-withdrawn-retry-0 → ...-retry-1 ... 식으로 토픽이 바뀌며 흐릅니다. 결국 DLT 핸들러에서는 retry 토픽의 이름이 들어오는데, IdempotentAspect 가 Inbox 에 저장할 때는 원본 토픽 이름으로 저장합니다. 따라서:
creatorInboxRepository.findByTopicAndPartitionAndOffset(consumerRecord.topic(), ...)는 매칭되지 않아 markDeadLettered() 가 실행되지 않습니다.
제안: Inbox 저장 시 KafkaHeaders.ORIGINAL_TOPIC/ORIGINAL_PARTITION/ORIGINAL_OFFSET 헤더를 우선 사용하거나, eventId 기준으로 조회/마킹하도록 변경해 주세요. auth 서비스의 DLT 핸들러는 @RetryableTopic 없이 동작하므로 같은 이슈는 아니지만, 두 서비스의 정책(@EnableKafkaRetryTopic 적용 여부)이 비대칭이라 일관성 확인이 필요합니다.
|
|
||
| ManagedChannel channel = grpcChannelFactory.createChannel(ServiceHost.RELEASE.getHostName()); | ||
| ReleaseServiceGrpc.ReleaseServiceBlockingStub stub = ReleaseServiceGrpc.newBlockingStub(channel); | ||
| ReleaseServiceGrpc.ReleaseServiceBlockingStub stub = ReleaseServiceGrpc |
There was a problem hiding this comment.
[Minor] 같은 PR에 GrpcConfiguration 으로 releaseManagedChannel Bean 을 등록해두고도, 본 어댑터에서는 매 호출마다 grpcChannelFactory.createChannel(...) 로 새 채널을 만들고 있어 일관성이 깨졌습니다.
채널 생성은 비용이 큰 작업이고 keep-alive 등의 설정도 채널 단위로 관리되므로, 새로 추가된 releaseManagedChannel Bean 을 주입받아 재사용하도록 통일해 주세요. subscribeManagedChannel, reviewManagedChannel, userManagedChannel, shortformManagedChannel 사용처도 모두 동일 문제를 가집니다.
release 서비스의 CreatorGrpcAdapter 는 이미 creatorManagedChannel 을 주입받아 재사용하고 있어 좋은 레퍼런스가 됩니다.
|
|
||
| @RestController | ||
| @RequestMapping("/api/creator") | ||
| @RequestMapping("/creator") |
There was a problem hiding this comment.
[Minor] URL prefix 일괄 변경(/api/creator/* → /creator/*, /admin/creator/*)은 Breaking Change 입니다.
다음 영역을 PR 머지 전에 반드시 동기화 확인해 주세요.
- API Gateway 라우팅 규칙
- 타 마이크로서비스의 REST 호출 위치(있다면)
tools/curl-test.sh류 운영 스크립트- 프론트엔드 API base path
- e2e/통합 테스트 픽스처
특히 /admin/creator/* 는 새 prefix 이므로 게이트웨이에서 일반 사용자 토큰으로 접근하지 못하도록 별도 라우트/필터를 두는 것이 안전합니다.
…clude 추가
- IdempotentAspect: catch(Throwable→RuntimeException) wrap으로 인한 retry 정책 무력화 수정
- Error는 즉시 rethrow
- RuntimeException은 타입 보존하여 rethrow
- checked Exception만 IllegalStateException으로 wrap
- 전 서비스 KafkaListener: @RetryableTopic(exclude = {BusinessException.class}) 추가
- BusinessException(4xx)은 재시도 불필요하므로 즉시 DLT로 전송
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
# Conflicts: # services/creator/src/main/java/kr/magicbox/creator/adapter/in/kafka/UserEventKafkaListener.java # services/general-goods/src/main/java/kr/magicbox/generalgoods/adapter/in/kafka/aop/IdempotentAspect.java # services/order/src/main/resources/application-dev.yml # services/order/src/main/resources/application-local.yml # services/order/src/main/resources/application-prod.yml # services/subscribe/src/main/java/kr/magicbox/subscribe/adapter/in/kafka/aop/IdempotentAspect.java # services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/AuthEventKafkaListener.java # services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/SseConnectedKafkaListener.java # services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/SseDisconnectedKafkaListener.java
Order.confirmOrderLine()은 CONFIRMED 상태에서도 호출 가능(partial confirm 이후 재호출)하므로 이미 CONFIRMED인 상태에서 라인 확정 시 OrderConfirmedEvent가 중복 발행되는 버그 수정. PREPARING → CONFIRMED 전이 시에만 이벤트를 발행하도록 previousStatus 가드 추가. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CONFIRMED 상태에서 재확정을 도메인 레벨에서 차단하여 이벤트 중복 발행 방지. 서비스 레이어의 previousStatus 가드는 불필요해져 제거. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
기본값과 동일하나 운영 시 DLT 전략을 명확히 하기 위해 명시적으로 추가. - dltStrategy = FAIL_ON_ERROR: DLT 전송 실패 시 컨슈머 중단(메시지 유실 방지) - dltTopicSuffix = "-dlt": DLT 토픽명 명시 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
# Conflicts: # services/creator/src/main/java/kr/magicbox/creator/adapter/in/kafka/UserEventKafkaListener.java # services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/AuthEventKafkaListener.java # services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/SseConnectedKafkaListener.java # services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/SseDisconnectedKafkaListener.java
…에러 수정 pjp.proceed()가 throws Throwable로 선언되어 있어 catch(Exception)으로는 컴파일러가 Throwable 처리를 인정하지 않아 컴파일 에러 발생. 마지막 catch 블록을 Throwable로 변경하여 수정. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
# Conflicts: # services/auth/src/main/java/kr/magicbox/auth/adapter/in/kafka/UserEventKafkaListener.java # services/auth/src/main/resources/application-dev.yml # services/auth/src/main/resources/application-prod.yml # services/creator/src/main/java/kr/magicbox/creator/adapter/in/kafka/UserEventKafkaListener.java # services/creator/src/main/resources/application-prod.yml # services/general-goods/src/main/java/kr/magicbox/generalgoods/adapter/in/kafka/CreatorEventKafkaListener.java # services/general-goods/src/main/java/kr/magicbox/generalgoods/adapter/in/kafka/aop/IdempotentAspect.java # services/general-goods/src/main/java/kr/magicbox/generalgoods/adapter/out/communication/grpc/CreatorGrpcAdapter.java # services/general-goods/src/main/resources/application-prod.yml # services/order/build.gradle # services/order/src/main/resources/application-local.yml # services/release/build.gradle # services/release/src/main/resources/application-local.yml # services/shopping-cart/Dockerfile # services/subscribe/src/main/java/kr/magicbox/subscribe/adapter/in/kafka/CreatorEventKafkaListener.java # services/subscribe/src/main/java/kr/magicbox/subscribe/adapter/in/kafka/UserEventKafkaListener.java # services/subscribe/src/main/java/kr/magicbox/subscribe/adapter/in/kafka/aop/IdempotentAspect.java # services/subscribe/src/main/resources/application-prod.yml # services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/AuthEventKafkaListener.java # services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/SseConnectedKafkaListener.java # services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/SseDisconnectedKafkaListener.java # services/user/src/main/java/kr/magicbox/user/adapter/out/communication/grpc/ReviewQueryGrpcAdapter.java # services/user/src/main/resources/application-dev.yml # services/user/src/main/resources/application-prod.yml
- release.proto: ReleaseStatus enum 추가, Release 메시지에 scheduled_at/status/limited_quantity/sold_quantity 필드 추가 - ReleaseGrpcService: toProtoRelease()에서 createdAt을 scheduledAt으로 잘못 매핑하던 버그 수정, 누락 필드 매핑 추가 - creator/release.proto: release 서비스 proto와 동기화 (ReleaseStatus enum, 누락 필드 추가) - ReleaseResult: soldQuantity/status/scheduledAt/createdAt 필드 추가 - ReleaseStatus: 신규 enum 추가 (SCHEDULED/ON_SALE/SOLD_OUT/ENDED) - ReleaseQueryGrpcAdapter: 신규 필드 매핑 추가, import 정렬 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- StartSaleScheduler: fixedDelay=60000 → cron="0 */10 * * * *" (10분 주기, ScheduledAtMultipleOfTenMinutes와 정렬) - StartSaleScheduler: RedissonClient.tryLock()으로 분산 락 적용 — 락 획득 실패 시 즉시 반환 - AutoStartSaleService: 단일 트랜잭션 전체 처리 → 청크 루프(100건 단위) + AutoStartSaleChunkService에서 건별 트랜잭션 - ReleaseRepositoryPort/ReleaseJpaAdapter/ReleaseJpaRepository: findScheduledBefore에 limit 파라미터 추가 (Pageable 기반) - build.gradle: redisson-spring-boot-starter 의존성 추가 - application-*.yml: Redis 설정 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…로로 통일 sold_quantity 증가는 Outbox→Inbox(Kafka) 경로만 사용하므로 gRPC 중복 경로 제거. - release.proto: rpc IncreaseSoldQuantity 및 관련 메시지 제거 - ReleaseGrpcService: increaseSoldQuantity 메서드 및 IncreaseSoldQuantityUseCase 의존성 제거 - IncreaseSoldQuantityUseCase, IncreaseSoldQuantityService 삭제 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
creatorIdQueryPort.getCreatorId() (gRPC + CircuitBreaker)가 @transactional 경계 안에 포함되어 DB 커넥션을 불필요하게 점유하는 문제 수정. - RegisterReleaseService: @transactional 제거, gRPC 조회 후 RegisterReleaseTxService에 위임 - RegisterReleaseTxService: DB 저장만 담당하는 별도 서비스로 @transactional 격리 (self-invocation으로 AOP 프록시가 무시되는 문제를 피하기 위해 별도 클래스로 분리) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ReleaseResult: isOnSale() 파생 메서드 추가 — 판매 중 판단 로직을 DTO에 집중 - ReleaseGrpcService: isReleaseOnSale에서 enum 직접 비교 → result.isOnSale() 위임, 불필요한 ReleaseStatus import 제거 - ScheduledAtMultipleOfTenMinutesValidator: atZone() 중복 호출 제거(ZonedDateTime 한 번만), 현재 시각+10분 이후만 허용하는 최소 예약 시간 검증 추가 - ReleaseQueryGrpcAdapter: 매 호출마다 새 채널 생성 → GrpcConfiguration의 releaseManagedChannel Bean 주입으로 채널 재사용 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… 주입된 creatorManagedChannel 사용 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ng 교체 (Maven Central 미존재) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GetMyProfileUseCase / GetMyProfileService 추가 - GetMyProfileQuery, GetMyProfileResult DTO 추가 - GetMyProfileResponse 추가 (id, nickname, profile, role) - UserQueryController에 GET /user/me 엔드포인트 추가 - class-level @RequestMapping 제거, 각 메서드에 전체 경로 명시 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- outbox.event.order-prepare 토픽 전용 debeziumKafkaListenerContainerFactory 등록 (Debezium 메시지는 __TypeId__ 헤더가 없으므로 StringDeserializer로 수신) - OrderStateKafkaListener를 ConsumerRecord<String, String>으로 변경, ObjectMapper로 직접 OrderPrepareEventDto 파싱 - IdempotentAspect: value가 InboxEvent가 아닌 경우(String) consumerRecord.key()를 eventId로 폴백 처리 - DLT 발행 실패 방지: debeziumKafkaListenerContainerFactory에 ByteArraySerializer 기반 DLT ProducerFactory + DeadLetterPublishingRecoverer 등록 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- OrderOutboxAdapter: save() 후 entity id를 payload의 event_id 필드로 주입 - OrderOutboxEntity: updatePayload() 메서드 추가 - orchestrator IdempotentAspect가 event_id(NOT NULL)를 정상 읽어 order→orchestrator→payment 사가 흐름 완성 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|


📌 관련 이슈
📝 변경 사항 요약
작업 유형
(기능 단위 완료 후 PR)(버그 1개 = PR 1개)(작업 단위 완료 후 PR)(작업 단위 완료 후 PR)변경 내용
ReleaseAggregate (ReleaseStatus, ReleaseLevel, 판매 시작/종료, 판매 수량 관리), VO 3종, 예외 3종@ScheduledAtMultipleOfTenMinutes)변경 이유
✅ 테스트 체크리스트
📸 스크린샷 / 로그
펼쳐보기
curl-test.sh 기반 릴리즈 API 테스트 완료
🔄 동작 플로우 (Mermaid)
flowchart TD A[릴리즈 등록 요청] --> B[ReleaseCommandController] B --> C[RegisterReleaseService] C --> D[gRPC → creator 서비스 creatorId 조회] D -->|NOT_FOUND| E[CreatorNotFoundException 404] D -->|성공| F[Release Aggregate 생성 저장] F --> G[판매 시작 시간 도래] G --> H[StartSaleScheduler 10분 주기] H --> I[ReleaseStatus → ON_SALE] I --> J[판매 종료 요청] J --> K[ReleaseStatus → SALE_ENDED]💬 리뷰어에게
ReleaseGrpcService)가 order, waiting, creator 서비스에 릴리즈 정보를 제공합니다. proto 정의와 응답 매핑을 검토해주세요.@ScheduledAtMultipleOfTenMinutes커스텀 Validation이 10분 단위 판매 시작 시간을 강제합니다.