[BE-27] 리멤버 커리어 애그리게이터 어댑터 (규약 회색지대 · 증분 수집)
작업 내용 (설계 의도)
근거 TDD: 20260722-타깃-공고-알림-및-지원-히스토리-tdd.md — “애그리게이터 어댑터 설계 — 규약 회색지대 4종”
근거 조사: 20260722-채용소스-조사-브리프.md “리멤버 커리어” (실측: POST career-api.rememberapp.co.kr/job_postings/search body {"search":{...},"page":N,"per":50} JSON 무인증, id 안정 ID, min_updated_at 증분 필터 지원, ends_at+explicit_due로 상시채용, per 상한 50, 백엔드 258건. 이용약관 제13조 1항 24호가 “스크래퍼”를 이름으로 지목해 금지 — 검색엔진 인덱싱만 예외)
변경 사항
리멤버 JSON API를 검색 조건으로 수집하는 애그리게이터 어댑터입니다. 조사에서 확인된 두 특성 — min_updated_at 증분 필터(이 소스만의 강점)와 약관의 명시적 스크래퍼 금지(회색지대 리스크) — 가 설계 핵심입니다.
능력 선언(SourceCapabilities):
| 항목 | 값 | 근거 |
|---|---|---|
sourceType | AGGREGATOR | — |
changeSignalKind | FIELD_HASH | 응답 아이템에 건별 updated_at 없음 |
providesDeadline | true | ends_at + explicit_due |
supportsIncrementalSince | true | min_updated_at 증분 |
| 규약 정책 | UA 명시·요청 지연·per 상한 50·허용 경로 /job_postings/search·1일 1회·재배포 금지 | 약관·robots |
핵심 설계 의도:
- 증분 수집(
min_updated_at) —CollectionDescriptor.Aggregator.lastSuccessfulCollectionAt(BE-25가 주입)을min_updated_at으로 전달해 마지막 성공 수집 이후 갱신분만 조회합니다. 전량 재수집 대비 요청량·부하를 크게 줄입니다. 단 응답 아이템에 건별updated_at이 없어 증분으로 좁힌 뒤에도 변경 판정은FieldHash로 합니다(브리프 사실). 최초 수집(lastSuccessfulCollectionAt=null)이면 전량. per상한 50 (초과 시 400) —SourceCompliancePolicy.pageSizeCap=50.SourceRequestExecutor가 초과 요청을 실행 전 거부해 어댑터가 실수로 51 이상을 보내는 것을 구조적으로 차단합니다. 258건 = 6페이지.- 마감일 정규화(FR-67) —
explicit_due=falseANDends_at=null이면 상시채용(null).explicit_due=true면ends_at파싱. - 회색지대 규약 — 약관이 스크래퍼를 금지하므로 사용자가 리스크를 인지하고 승인했습니다. 재배포·상업 이용 금지(개인 열람 전용, FR-69-6). 1일 1회는 DB 유니크가 강제. 약관 변경 모니터링(Operations) 대상.
id→sourceJobId, 회사명은 목록에서 확보 →sourceCompanyName.- robots·약관 확인 결과·회색지대 판정 근거를 KDoc에 기록.
supportedCategories()선언.
롤백: disabled_at으로 소프트 비활성화(약관 변경 시 즉시).
의존
- BE-02 (SPI·
supportsIncrementalSince·lastSuccessfulCollectionAt·SourceRequestExecutor)
다이어그램
처리 흐름
sequenceDiagram participant U as CollectAggregatorPostingsUseCase participant A as RememberAggregatorAdapter participant E as SourceRequestExecutor participant M as career-api.rememberapp U->>A: fetchList(Aggregator descriptor + lastSuccessfulCollectionAt) A->>A: min_updated_at = lastSuccessfulCollectionAt (null이면 전량) loop 페이지 (per≤50 · 지연 · UA) A->>E: post(/job_postings/search, {min_updated_at, page, per:50}) E->>M: POST (per>50이면 실행 전 거부) M-->>E: JSON (id·company·ends_at·explicit_due) end A->>A: 상시채용 정규화 · 필드 해시 · 회사명 추출 A-->>U: Fetched(RawJobPosting[])
클래스 의존
flowchart LR subgraph Domain["domain/posting"] Raw[RawJobPosting] Sig[ChangeSignature.FieldHash] Cap[SourceCapabilities] Desc[CollectionDescriptor.Aggregator] end subgraph Adapter["infrastructure/posting/collector/aggregator/remember"] Ad[RememberAggregatorAdapter] Dto[RememberJobPostingResponse] Hash[FieldHashCalculator] Deadline[RememberDeadlineNormalizer] end subgraph Common["collector/common"] Exec[SourceRequestExecutor] end Ad --> Exec Ad --> Dto Ad --> Hash Ad --> Deadline Hash --> Sig Ad --> Cap Ad --> Desc
테스트 케이스
lastSuccessfulCollectionAt이 있으면min_updated_at으로 전달되어 증분 조회한다lastSuccessfulCollectionAt이 null(최초)이면 전량 조회한다per를 51 이상으로 요청하면SourceRequestExecutor가 실행 전 거부한다per=50으로 258건을 6페이지에 걸쳐 수집한다explicit_due=false·ends_at=null이면deadlineAt=null(상시채용)로 정규화된다explicit_due=true면ends_at이 마감일로 파싱된다- 주요 필드가 동일하면 같은
FieldHash로 변경 없음으로 판정된다(건별 updated_at 미사용) id가sourceJobId, 회사명이sourceCompanyName으로 매핑된다- 허용 경로 외 호출은 Executor가 차단한다
- 재배포·상업 이용 관련 데이터 노출 경로가 없다(개인 열람 전용)
supports(REMEMBER)가 true,supportsIncrementalSince가 true를 반환한다supportedCategories()가 코드↔라벨 목록을 반환한다