@startuml distribution-다중채널배포 !theme mono title Distribution Service - 다중 채널 배포 (UFR-DIST-010) actor Client participant "Event Service" as EventSvc participant "Distribution\nREST API" as API participant "Distribution\nController" as Controller participant "Distribution\nService" as Service participant "Circuit Breaker\nManager" as CB participant "Channel\nDistributor" as Distributor participant "Retry Handler" as Retry database "Event DB" as DB queue "Kafka" as Kafka participant "우리동네TV API" as WooridongneTV participant "링고비즈 API" as RingoBiz participant "지니TV API" as GenieTV participant "SNS APIs" as SNS participant "Redis Cache" as Cache == REST API 동기 호출 수신 == EventSvc -> API: POST /api/distribution/distribute\n{eventId, channels[], contentUrls} activate API API -> Controller: distributeToChannels(request) activate Controller Controller -> Service: executeDistribution(distributionRequest) activate Service Service -> DB: 배포 이력 초기화\nINSERT distribution_logs\n{eventId, status: PENDING} DB --> Service: 배포 이력 ID note over Service: 배포 시작 상태로 변경 Service -> DB: UPDATE distribution_logs\nSET status = 'IN_PROGRESS' == Circuit Breaker 및 Bulkhead 초기화 == Service -> CB: 채널별 Circuit Breaker 상태 확인 CB --> Service: 모든 Circuit Breaker 상태\n(CLOSED/OPEN/HALF_OPEN) note over Service: Bulkhead 패턴 적용\n채널별 독립 스레드 풀 == 다중 채널 병렬 배포 (Parallel) == par 우리동네TV 배포 alt 채널 선택됨 Service -> Distributor: distributeToWooridongneTV\n(eventId, contentUrls) activate Distributor Distributor -> CB: checkCircuitBreaker("WooridongneTV") alt Circuit Breaker OPEN CB --> Distributor: 서킷 오픈 상태\n(즉시 실패) Distributor -> DB: 배포 채널 로그 저장\n{channel: WooridongneTV, status: FAILED, reason: Circuit Open} Distributor --> Service: 실패 (Circuit Open) else Circuit Breaker CLOSED 또는 HALF_OPEN CB --> Distributor: 요청 허용 Distributor -> Retry: executeWithRetry(() -> callWooridongneTV()) activate Retry loop Retry 최대 3회 (지수 백오프: 1초, 2초, 4초) Retry -> WooridongneTV: POST /api/upload-video\n{eventId, videoUrl, region, schedule} activate WooridongneTV alt 성공 WooridongneTV --> Retry: 200 OK\n{distributionId, estimatedViews} deactivate WooridongneTV Retry --> Distributor: 배포 성공 Distributor -> CB: recordSuccess("WooridongneTV") Distributor -> DB: 배포 채널 로그 저장\n{channel: WooridongneTV, status: SUCCESS, distributionId} Distributor --> Service: 성공\n{channel, distributionId, estimatedViews} else 실패 (일시적 오류) WooridongneTV --> Retry: 500 Internal Server Error deactivate WooridongneTV note over Retry: 지수 백오프 대기\n(1초 → 2초 → 4초) end end alt 3회 모두 실패 Retry --> Distributor: 배포 실패 (Retry 소진) deactivate Retry Distributor -> CB: recordFailure("WooridongneTV") note over CB: 실패율 50% 초과 시\nCircuit Open (30초) Distributor -> DB: 배포 채널 로그 저장\n{channel: WooridongneTV, status: FAILED, retries: 3} Distributor --> Service: 실패 (Retry 소진) end end deactivate Distributor end alt 채널 선택됨 Service -> Distributor: distributeToRingoBiz\n(eventId, phoneNumber, audioUrl) activate Distributor Distributor -> CB: checkCircuitBreaker("RingoBiz") alt Circuit Breaker CLOSED 또는 HALF_OPEN CB --> Distributor: 요청 허용 Distributor -> Retry: executeWithRetry(() -> callRingoBiz()) activate Retry loop Retry 최대 3회 Retry -> RingoBiz: POST /api/update-ringtone\n{phoneNumber, audioUrl} activate RingoBiz alt 성공 RingoBiz --> Retry: 200 OK\n{updateTimestamp} deactivate RingoBiz Retry --> Distributor: 배포 성공 deactivate Retry Distributor -> CB: recordSuccess("RingoBiz") Distributor -> DB: 배포 채널 로그 저장\n{channel: RingoBiz, status: SUCCESS} Distributor --> Service: 성공\n{channel, updateTimestamp} else 실패 RingoBiz --> Retry: 500 Error deactivate RingoBiz end end alt 3회 모두 실패 Retry --> Distributor: 배포 실패 deactivate Retry Distributor -> CB: recordFailure("RingoBiz") Distributor -> DB: 배포 채널 로그 저장\n{channel: RingoBiz, status: FAILED} Distributor --> Service: 실패 end else Circuit Breaker OPEN CB --> Distributor: 서킷 오픈 상태 Distributor -> DB: 배포 채널 로그 저장\n{channel: RingoBiz, status: FAILED, reason: Circuit Open} Distributor --> Service: 실패 (Circuit Open) end deactivate Distributor end alt 채널 선택됨 Service -> Distributor: distributeToGenieTV\n(eventId, region, schedule, budget) activate Distributor Distributor -> CB: checkCircuitBreaker("GenieTV") alt Circuit Breaker CLOSED 또는 HALF_OPEN CB --> Distributor: 요청 허용 Distributor -> Retry: executeWithRetry(() -> callGenieTV()) activate Retry loop Retry 최대 3회 Retry -> GenieTV: POST /api/register-ad\n{eventId, contentUrl, region, schedule, budget} activate GenieTV alt 성공 GenieTV --> Retry: 200 OK\n{adId, impressionSchedule} deactivate GenieTV Retry --> Distributor: 배포 성공 deactivate Retry Distributor -> CB: recordSuccess("GenieTV") Distributor -> DB: 배포 채널 로그 저장\n{channel: GenieTV, status: SUCCESS, adId} Distributor --> Service: 성공\n{channel, adId, schedule} else 실패 GenieTV --> Retry: 500 Error deactivate GenieTV end end alt 3회 모두 실패 Retry --> Distributor: 배포 실패 deactivate Retry Distributor -> CB: recordFailure("GenieTV") Distributor -> DB: 배포 채널 로그 저장\n{channel: GenieTV, status: FAILED} Distributor --> Service: 실패 end else Circuit Breaker OPEN CB --> Distributor: 서킷 오픈 상태 Distributor -> DB: 배포 채널 로그 저장\n{channel: GenieTV, status: FAILED, reason: Circuit Open} Distributor --> Service: 실패 (Circuit Open) end deactivate Distributor end alt Instagram 선택됨 Service -> Distributor: distributeToInstagram\n(eventId, imageUrl, caption, hashtags) activate Distributor Distributor -> CB: checkCircuitBreaker("Instagram") alt Circuit Breaker CLOSED 또는 HALF_OPEN CB --> Distributor: 요청 허용 Distributor -> Retry: executeWithRetry(() -> callInstagram()) activate Retry loop Retry 최대 3회 Retry -> SNS: POST /instagram/api/posts\n{imageUrl, caption, hashtags} activate SNS alt 성공 SNS --> Retry: 200 OK\n{postUrl, postId} deactivate SNS Retry --> Distributor: 배포 성공 deactivate Retry Distributor -> CB: recordSuccess("Instagram") Distributor -> DB: 배포 채널 로그 저장\n{channel: Instagram, status: SUCCESS, postUrl} Distributor --> Service: 성공\n{channel, postUrl} else 실패 SNS --> Retry: 500 Error deactivate SNS end end alt 3회 모두 실패 Retry --> Distributor: 배포 실패 deactivate Retry Distributor -> CB: recordFailure("Instagram") Distributor -> DB: 배포 채널 로그 저장\n{channel: Instagram, status: FAILED} Distributor --> Service: 실패 end else Circuit Breaker OPEN CB --> Distributor: 서킷 오픈 상태 Distributor -> DB: 배포 채널 로그 저장\n{channel: Instagram, status: FAILED, reason: Circuit Open} Distributor --> Service: 실패 (Circuit Open) end deactivate Distributor end alt Naver Blog 선택됨 Service -> Distributor: distributeToNaverBlog\n(eventId, imageUrl, content) activate Distributor note over Distributor: Naver Blog 배포 로직\n(Instagram과 동일 패턴) Distributor --> Service: 성공 또는 실패 deactivate Distributor end alt Kakao Channel 선택됨 Service -> Distributor: distributeToKakaoChannel\n(eventId, imageUrl, message) activate Distributor note over Distributor: Kakao Channel 배포 로직\n(Instagram과 동일 패턴) Distributor --> Service: 성공 또는 실패 deactivate Distributor end end note over Service: 모든 채널 배포 완료\n(1분 이내) == 배포 결과 집계 및 저장 == Service -> Service: 채널별 배포 결과 집계\n성공: [list], 실패: [list] alt 모든 채널 성공 Service -> DB: UPDATE distribution_logs\nSET status = 'COMPLETED', completed_at = NOW() else 일부 채널 실패 Service -> DB: UPDATE distribution_logs\nSET status = 'PARTIAL_FAILURE', completed_at = NOW() note over Service: 실패한 채널 정보 저장 else 모든 채널 실패 Service -> DB: UPDATE distribution_logs\nSET status = 'FAILED', completed_at = NOW() end == Kafka 이벤트 발행 == Service -> Kafka: Publish to event-topic\nDistributionCompleted\n{eventId, channels[], results[], completedAt} note over Kafka: Analytics Service 구독\n실시간 통계 업데이트 Service -> Cache: 배포 상태 캐싱\nkey: distribution:{eventId}\nvalue: {status, results[]}\nTTL: 1시간 == REST API 동기 응답 == Service --> Controller: 배포 완료 응답\n{status, successChannels[], failedChannels[]} deactivate Service Controller --> API: DistributionResponse\n{eventId, status, results[]} deactivate Controller API --> EventSvc: 200 OK\n{distributionId, status, results[]} deactivate API note over EventSvc: 배포 완료 응답 수신\n이벤트 상태 업데이트\nAPPROVED → ACTIVE == 배포 실패 처리 (비동기) == note over Service: 실패한 채널은\n- 수동 재시도 가능\n- 알림 발송 (추후 구현)\n- Circuit Open 시 30초 후\n 자동 Half-Open 전환 @enduml