[BE-52] recruitment DomainService·UseCase·인프라·환불 워커

작업 내용 (설계 의도)

변경 사항

근거 TDD: “서비스 클래스·실패 경로·동시성·멱등·Sequence Diagram 신청/취소”. BE-51 코어 위에 오케스트레이션·영속화·환불 경로를 구현한다.

포함 (전부 신규 파일):

  • domain service: RecruitmentDomainService — create/apply/confirmApplication/cancelPendingApplication/cancelApplication/cancelRecruitment/getRecruitment/findApplications/findApplicationsBy(applicantUserId).
    • findApplicationsBy(applicantUserId): 신청자 본인 기준 신청 목록 조회(FR-4 취소 FE 경로 지탱 — senior-pm 보강). ApplicationRepository.findByApplicantUserId(applicantUserId) 추가(도메인 interface + infra QueryDSL).
    • apply: 분산락 recruitment:$recruitmentId(booking spinLock 패턴 재사용) + findForUpdateById(비관락) + countActiveByRecruitmentId < capacity 위반 시 RecruitmentFullException. 정원 충족 시 Recruitment.closeWhenFull().
    • cancelApplication: CancellationPolicy.feeRateFor(deadline) → refundAmount = fee*(1-rate), Application.cancelByApplicant()ApplicationRefundRequestedEvent 적재(플랫폼 귀속, 개설자 정산 없음).
    • cancelRecruitment: 개설자 검증 → CONFIRMED 전원 전액환불 이벤트 + Recruitment CANCELLED.
    • Repository/Publisher/DistributedLock/CancellationPolicy/RecruitmentRefundGateway 주입(UseCase 직접주입 금지, DomainService만).
  • application UseCase (execute 10줄 이내, DomainService만 호출): CreateRecruitmentUseCase·ApplyRecruitmentUseCase(fee>0이면 createPending+initiatePg, fee0이면 confirmApplication 직접)·CancelApplicationUseCase·CancelRecruitmentUseCase·GetRecruitmentUseCase·ListRecruitmentsUseCase·ListApplicationsUseCase(개설자용)·ListMyApplicationsUseCase(신청자 본인용, findApplicationsBy) + Command/Response(ApplicationResponse: id·recruitmentId·status·paymentId·appliedAt).
  • infrastructure: RecruitmentRepositoryImpl·ApplicationRepositoryImpl(+JpaRepository, QueryDSL CustomImpl for findForUpdate/count — @Query 금지), RecruitmentRefundGatewayImpl(booking StubPaymentRefundGateway 패턴, @Profile("!prod")).
  • presentation: RecruitmentRefundEventWorker(@TransactionalEventListener(AFTER_COMMIT) → RecruitmentRefundGateway.requestRefund, 예외 catch·에러로그 — booking RefundEventWorker 동형).
  • ApplyRecruitmentUseCasePaymentDomainService.createPending/initiatePgOrderType.RECRUITMENT로 호출(CreateBookingUseCase 패턴). OrderType enum·when 분기는 BE-55 소관.

롤백

플래그 OFF(recruitment.enabled) + 역방향 DDL. 환불 실패는 재시도 로그·Operations 알람.

의존

  • BE-51 (recruitment 도메인 코어)

다이어그램

처리 흐름 (신청)

sequenceDiagram
    participant U as ApplyRecruitmentUseCase
    participant R as RecruitmentDomainService
    participant P as PaymentDomainService
    U->>R: apply(recruitmentId, userId)
    R-->>U: Application(PENDING)
    alt fee > 0
        U->>P: createPending(RECRUITMENT, applicationId, fee)
        U->>P: initiatePg(...)
    else fee == 0
        U->>R: confirmApplication(applicationId, null)
    end

클래스 의존

flowchart LR
    ARU[ApplyRecruitmentUseCase] --> RDS[RecruitmentDomainService]
    ARU --> PDS[PaymentDomainService]
    RDS --> Lock[DistributedLock]
    RDS --> Pol[CancellationPolicy]
    RDS --> Pub[DomainEventPublisher]
    Worker[RecruitmentRefundEventWorker] --> Gw[RecruitmentRefundGateway]

테스트 케이스

  • 정원 여유 시 신청이 PENDING으로 생성되고 결제 흐름이 시작된다
  • 참가비 0원 모집 신청은 PG 없이 즉시 CONFIRMED된다
  • 정원이 가득 찬 모집 신청은 RecruitmentFullException(409)이다
  • 동시 100건이 마지막 1자리를 경합해도 확정 신청이 정원을 초과하지 않는다(오버부킹 0)
  • 마감 5일 전 취소 시 환불액이 참가비의 95%다(수수료 5% 플랫폼 귀속)
  • 마감 2일 전 취소 시 환불액이 참가비의 90%다
  • 개설자 모집 취소 시 CONFIRMED 전원이 전액환불된다
  • 이미 취소된 신청 재취소는 중복 환불 없이 멱등하다
  • 환불 게이트가 예외를 던져도 원 취소 트랜잭션은 롤백되지 않고 에러 로그를 남긴다
  • findApplicationsBy(applicantUserId)가 해당 신청자의 신청만 반환하고 타인 신청은 제외한다
  • 신청 이력이 없는 사용자의 내 신청 조회는 빈 목록을 정상 반환한다