[BE-25] 애그리게이터 수집 오케스트레이션 · 발견 회사 자동 등록

작업 내용 (설계 의도)

근거 TDD: 20260722-타깃-공고-알림-및-지원-히스토리-tdd.md — “발견 회사 자동 등록 흐름”, “방안 10·11”, “Sequence Diagram 00:10 애그리게이터 수집”

변경 사항

애그리게이터 소스(사람인·점핏)를 00:10에 수집하고, 미등록 회사를 DISCOVERED로 자동 등록합니다(FR-60·61·66). 회사 종속형 수집(BE-10)과 델타·시딩·마감·본문 저장·이벤트 발행 코어를 공유하되, “회사가 소스로 확정돼 있지 않다”는 차이만 이 티켓이 처리합니다.

핵심 설계 의도:

  • 상위 오케스트레이션으로 두 컨텍스트를 순차 조합합니다 — 애그리게이터는 회사가 미지정이라 공고 저장 전에 companyId가 필요합니다(동기 선행). posting 도메인이 company를 import하지 않도록 UseCase가 조율합니다:
    1. AggregatorCollectionDomainService.fetch(descriptor) → raw 공고 + sourceCompanyName 집합
    2. DiscoveredCompanyDomainService.resolveOrCreate(names)(company 컨텍스트, BE-06) → Map<name, companyId>. 미존재 회사는 DISCOVERED로 생성, 기존은 그대로(멱등)
    3. AggregatorCollectionDomainService.persist(raw, companyIdByName) → 델타·dedupKey·이벤트
  • companyId 없어 공고 저장 불가한 사고 방지 — 회사 자동 등록이 반드시 선행되고, 그 결과 매핑으로만 공고를 저장합니다.
  • 매칭 실패 공고까지 전부 저장(FR-66) — 수집 단계 필터링 없음. 회사 종속형과 동일 원칙.
  • 시딩 — 애그리게이터 소스의 최초 수집도 notificationEligible=0으로 저장(FR-10). 안 그러면 사람인 2,594건이 첫날 전부 신규 알림이 됩니다.
  • 회사 자동 등록은 관심 회사와 구분됩니다(DISCOVERED) — 발송 배치(BE-16)가 이 축으로 개별/일일 요약을 가릅니다.
  • 피처 플래그 aggregator.collection이 false면 애그리게이터 수집 자체를 건너뜁니다(규약 리스크·볼륨 폭증 시 즉시 중단).
  • search_keyword''(빈 문자열)로 다룹니다 — 소스 등록(BE-17)에서 이미 ''로 저장되므로 descriptor 구성 시 그대로 전달합니다. NULL로 바꾸지 않습니다(유니크 성립 조건, BE-01 규칙 1-b). idempotency_key(성공 시에만 채우는 NULL)와는 반대 규칙이니 혼동 금지.
  • 증분 수집 지원 (리멤버, FR-60)CollectionDescriptor.Aggregator.lastSuccessfulCollectionAt를 채워 넘깁니다. 해당 소스의 마지막 SUCCESS 회차 시각(job_posting_collection_runs)을 조회해 주입하고, supportsIncrementalSince=true인 어댑터(리멤버)만 이를 min_updated_at 필터로 활용합니다. 나머지 어댑터는 무시합니다. 최초 수집(이력 없음)이면 null(전량). 증분이라도 변경 판정은 여전히 FieldHash입니다(건별 updated_at 부재).
  • 회색지대 4종(원티드·리멤버·잡코리아·서핏) 포함 — 이 오케스트레이션은 애그리게이터 일반을 다루므로 어댑터 유형(청정/회색)과 무관하게 6종 전부 자동 수집합니다. 규약 준수는 각 어댑터의 SourceCompliancePolicySourceRequestExecutor가 강제하므로 오케스트레이션은 소스별 분기가 없습니다.
  • 소스 가드·고장 판정·시간 예산 — 회사 종속형과 동일하게 비정상 회차 가드(FR-15)·3일 연속 고장(FR-19)이 적용됩니다. 사람인 2,594건도 요청 지연 포함 소스당 최대 30분(NFR-2), 09:00 이전 완료(방안 11).

새 파일만 추가합니다application/posting/CollectAggregatorPostingsUseCase, domain/posting/AggregatorCollectionDomainService, presentation/posting/scheduler/AggregatorCollectionScheduler(cron 0 10 0 * * *). BE-06의 DiscoveredCompanyDomainService를 주입해 사용합니다. BE-10과 파일이 겹치지 않습니다.

롤백: 피처 플래그 aggregator.collection=false로 수집 즉시 중단. 발견 회사·공고는 보존.

의존

  • BE-02 (SPI·수집 계약), BE-06 (DiscoveredCompanyDomainService), BE-22 (사람인), BE-23 (점핏), BE-26 (원티드), BE-27 (리멤버), BE-28 (잡코리아), BE-29 (서핏)

다이어그램

처리 흐름

sequenceDiagram
    participant S as AggregatorScheduler
    participant U as CollectAggregatorPostingsUseCase
    participant F as FeatureFlagGateway
    participant A as AggregatorCollectionDomainService
    participant C as DiscoveredCompanyDomainService
    participant R as JobPostingRepository
    S->>U: execute()
    U->>F: isEnabled("aggregator.collection")
    alt 플래그 OFF
        F-->>U: false → 종료
    else 플래그 ON
        loop 애그리게이터 소스별
            U->>A: fetch(descriptor)
            A-->>U: raw + sourceCompanyName 집합
            U->>C: resolveOrCreate(회사명 집합)
            C-->>U: Map<name, companyId> (미존재 DISCOVERED 생성)
            U->>A: persist(raw, companyIdByName)
            A->>R: saveAll(dedupKey·델타·시딩·이벤트)
        end
    end

클래스 의존

flowchart LR
    subgraph Presentation["presentation/posting"]
        Sched[AggregatorCollectionScheduler]
    end
    subgraph Application["application/posting"]
        UC[CollectAggregatorPostingsUseCase]
    end
    subgraph DomainP["domain/posting"]
        A[AggregatorCollectionDomainService]
        GW[JobSourceGateway]
        Repo[JobPostingRepository]
        Flag[FeatureFlagGateway]
    end
    subgraph DomainC["domain/company"]
        C[DiscoveredCompanyDomainService]
    end
    Sched --> UC
    UC --> A
    UC --> C
    A --> GW
    A --> Repo
    A --> Flag

테스트 케이스

  • 애그리게이터 소스를 수집하면 raw 공고의 회사명 집합이 추출된다
  • 미등록 회사는 DISCOVERED로 자동 생성되고 그 companyId로 공고가 저장된다
  • 이미 등록된(관심이든 발견이든) 회사는 재생성되지 않고 기존 companyId가 재사용된다
  • 애그리게이터 첫 수집(시딩)은 notificationEligible=0으로 저장되고 알림 대상이 아니다
  • 매칭 실패 공고도 전부 저장된다(수집 단계 필터링 없음)
  • 저장 시 dedup_key가 회사명 기반으로 채워진다
  • 비정상 회차(실패·0건)는 소스 가드로 마감 판정을 건너뛴다
  • 3일 연속 비정상이면 소스 고장 상태로 전이한다
  • 피처 플래그 aggregator.collection=false면 수집이 0건이다
  • 회사 자동 등록이 실패해도(예외) 다른 소스 수집은 계속된다(소스 단위 격리)
  • 점핏처럼 회사가 상세에만 있는 소스도 어댑터가 채운 sourceCompanyName으로 자동 등록된다
  • 리멤버 소스는 마지막 성공 수집 시각이 lastSuccessfulCollectionAt로 전달되고, 최초 수집이면 null이다
  • 회색지대 4종(원티드·리멤버·잡코리아·서핏)도 소스별 분기 없이 6종 일괄 수집된다