[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가 조율합니다:
AggregatorCollectionDomainService.fetch(descriptor)→ raw 공고 +sourceCompanyName집합DiscoveredCompanyDomainService.resolveOrCreate(names)(company 컨텍스트, BE-06) →Map<name, companyId>. 미존재 회사는DISCOVERED로 생성, 기존은 그대로(멱등)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종 전부 자동 수집합니다. 규약 준수는 각 어댑터의
SourceCompliancePolicy와SourceRequestExecutor가 강제하므로 오케스트레이션은 소스별 분기가 없습니다. - 소스 가드·고장 판정·시간 예산 — 회사 종속형과 동일하게 비정상 회차 가드(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종 일괄 수집된다