JOBKOREA 겸용 플랫폼 표현 (SourceType 고정 해소) TDD

Background

근거 PRD: /Users/biuea/Desktop/dpdpdndn/프로젝트/공고알림앱/20260722-타깃-공고-알림-및-지원-히스토리-prd.md (FR-60 소스 유형 2종) 근거 티켓: tickets/BE-30-followup-jobkorea-dual-source-type.md 근거 조사: 20260722-채용소스-조사-브리프.md “우리은행 — 인크루트 + 잡코리아” + 본 문서 robots 실측(2026-07-27)

원 요구사항은 “우리은행 = 인크루트 + 잡코리아”(한 회사가 복수 소스, Company:JobSource = 1:N)입니다. 그런데 JobPlatform.JOBKOREASourceType.AGGREGATOR고정돼, 우리은행의 잡코리아 채용대행(jrs.jobkorea.co.kr/wooribank)을 회사 종속형(COMPANY_BOUND) 소스로 등록할 수 없습니다. BE-21 기동 검증 케이스 6이 이 이유로 400 실패했습니다.

Overview

  • 무엇을: JobPlatform이 소스 유형을 1개로 고정하던 것을 지원 집합(Set<SourceType>) 으로 완화해, JOBKOREA가 AGGREGATOR·COMPANY_BOUND겸용하게 합니다.
  • : 한 플랫폼(잡코리아)이 전역 검색형(애그리게이터, www.jobkorea.co.kr)과 회사 채용관형(회사 종속, jrs.jobkorea.co.kr/{slug}) 두 성격을 실제로 겸함. 현재 모델은 이를 표현 못 해 등록 자체가 막힙니다.
  • 어떻게: JobPlatform.supportedSourceTypes: Set<SourceType> + supports(sourceType) 도입, JobSource.forCompany/forAggregatorrequire(platform.sourceType == X)require(platform.supports(X))로 교체. 스키마 변경 없음(행 단위 source_type 컬럼·유니크 제약이 이미 겸용을 수용). 어댑터 계약(BE-02, 미구현)의 디스패치·능력 조회는 platform 단독 키에서 (platform, sourceType) 키로 일반화해 겸용을 지원.

Terminology

용어정의
SourceType채용 데이터 출처 유형 — COMPANY_BOUND(회사 slug 수집) / AGGREGATOR(검색 조건 수집), FR-60
JobPlatform어댑터 구현 단위 플랫폼(GREENHOUSE·INCRUIT·JOBKOREA 등). domain.common
겸용 플랫폼하나의 JobPlatform이 두 SourceType을 모두 지원하는 경우 (JOBKOREA)
회사 채용대행(JRS)잡코리아 Job Recruiting Service — jrs.jobkorea.co.kr/{slug}, 회사별 호스팅 채용관
애그리게이터 잡코리아www.jobkorea.co.kr 직무 카테고리 전역 검색 (BE-28)
SPI 디스패치JobSourceGatewayImpl이 요청을 담당 어댑터로 라우팅하는 방식

Define Problem

AS-IS

실제 코드 인용:

  • domain/common/JobPlatform.kt:12enum class JobPlatform(val sourceType: SourceType). 각 값이 단일 sourceType을 고정 보유: JOBKOREA(SourceType.AGGREGATOR) (JobPlatform.kt:20).
  • domain/company/JobSource.kt:70forCompanyrequire(platform.sourceType == SourceType.COMPANY_BOUND). JOBKOREAAGGREGATOR라 이 require가 실패 → IllegalArgumentException → GlobalExceptionHandler가 400.
  • domain/company/JobSource.kt:97forAggregatorrequire(platform.sourceType == SourceType.AGGREGATOR) (대칭).
  • domain/company/JobSource.kt:24JobSource는 이미 private val sourceType: SourceType행 단위로 보유. 즉 도메인 인스턴스 수준에서는 소스마다 유형이 다를 수 있으나, 팩토리가 platform의 고정 유형으로 강제.
  • infrastructure/company/persistence/JobSourceJpaEntity.kt:23source_type 컬럼이 이미 존재(var sourceType: String).
  • db/migration/V202607220000...sql:53source_type VARCHAR(20) NOT NULL 행 단위 컬럼. 유니크 제약 uk_job_sources_company_bound (platform, source_slug) / uk_job_sources_aggregator (platform, search_category_code, search_keyword)platform+행 속성 조합이라 같은 platform이 두 유형으로 공존해도 충돌하지 않음.

핵심 문제: 행 단위 sourceType은 이미 존재하나, JobPlatform enum의 1:1 고정 + 팩토리 require가 겸용을 봉쇄. 강직성은 도메인 enum 한 곳에 집중돼 있고, 스키마·영속 계층은 이미 겸용 준비 완료.

어댑터 계약 측(BE-02, 미구현 — 코드에 posting 도메인·SPI·어댑터 없음):

  • TDD 본문(20260722-...-tdd.md:394) SourcePlatformAdapter.supports(platform: JobPlatform): Boolean, capabilitiesOf(platform) (:377), 디스패치 adapters.first { it.supports(platform) } (:403) — 전부 platform 단독 키. 겸용 시 한 플랫폼에 어댑터가 2개(JRS용·www용)라 platform 단독으로는 어느 어댑터·어느 능력인지 결정 불가.

TO-BE

  • JobPlatformsupportedSourceTypes: Set<SourceType>를 보유. JOBKOREA = setOf(AGGREGATOR, COMPANY_BOUND), 나머지는 단일 원소 집합.
  • 팩토리는 platform.supports(요청유형)으로 검증 — 잘못된 조합(SARAMIN을 회사 종속형으로) 거부는 유지, 겸용(JOBKOREA)만 통과.
  • 어댑터 SPI(BE-02 구현 시점)는 (platform, sourceType) 키로 디스패치·능력 조회 — 겸용 플랫폼의 두 어댑터가 유형으로 구분됨.
  • 우리은행: forCompany(JOBKOREA, "wooribank") 성공 → BE-21 케이스 6 통과.

Architecture Benchmarking

제품/사례해결 방식참고할 패턴미참고 사유
Airbyte (오픈소스 ELT 커넥터) docs.airbyte.comSource 커넥터와 그 커넥터의 connection 설정(sync mode 등)을 분리 — 같은 커넥터가 여러 sync mode(full/incremental)를 “커넥터 고정”이 아니라 “connection 단위 속성”으로 지원소스 성격(유형)을 플랫폼 고정이 아닌 인스턴스(행) 속성으로 두는 원칙 = 본 설계의 supportedSourceTypes + 행 단위 source_type과 동일커넥터 레지스트리·프로토콜(Docker화된 소스)은 1인용 도구에 과함 — 우리는 enum + 팩토리 검증으로 충분
Singer / Meltano tap hub.meltano.comtap(소스)이 capability를 선언(discover·state·catalog)하고 오케스트레이터가 그 선언에 따라 분기어댑터가 자기 능력을 선언하고 게이트웨이가 선언 기반 디스패치 = BE-02 SourceCapabilities + (platform, sourceType) 디스패치tap 프로토콜(별도 프로세스·JSON 스트림)은 불필요 — in-process SPI로 충분
Spring @Qualifier / 다중 빈 라우팅 docs.spring.io같은 인터페이스의 여러 구현을 키(qualifier)로 선택한 플랫폼에 어댑터 2개일 때 (platform, sourceType) 복합 키로 선택 — when 분기 없이 supports() 위임 유지(OCP)프레임워크 qualifier 대신 도메인 값(SourceType)으로 라우팅 — 인프라 애노테이션을 도메인에 노출하지 않음

Possible Solutions

방안 비교

방안설명왜 채택 / 미채택
A. JobPlatform을 지원 집합으로 (채택)sourceType: SourceTypesupportedSourceTypes: Set<SourceType>. JOBKOREA만 2원소, 나머지 1원소. 팩토리 require(platform.supports(X)). 행 단위 source_type은 그대로 사용(이미 존재)채택. AS-IS에서 행 단위 source_type이 이미 존재해 이 방안이 가장 가벼움(원 티켓이 “폭 큼”으로 본 것은 오해 — 스키마·영속은 이미 겸용 준비 완료). enum 한 곳 + 팩토리 가드만 바꾸면 되고, 잘못된 조합 거부 불변식(SARAMIN을 회사 종속형으로 금지)을 유지. 스키마 마이그레이션 0건
B. JOBKOREA_COMPANY 별도 enum 값 + 별도 어댑터겸용을 표현하려 플랫폼 값을 쪼갬미채택. 한 실제 플랫폼에 enum 값 2개 = 오염. categoriesOf(platform)·base_url·중복 판정(대표 선정 sourceType tie-break, TDD:755)이 “잡코리아”를 두 값으로 흩뜨려 처리해야 함. 어댑터를 2개 두는 것(호스트·파서가 실제로 다름)은 정당하나 그건 (platform, sourceType) 디스패치로 해결되지 enum 분리가 필요치 않음
C. 요구사항 축소 (우리은행은 인크루트만)잡코리아 회사 종속형을 비목표 선언미채택. robots 실측(아래) 결과 jrs.jobkorea.co.kr가 전면 허용이라 규약상 무산 사유가 없음. 원 요구사항(“인크루트 + 잡코리아”)을 근거 없이 축소할 이유 없음. (robots가 차단이었다면 이 방안이 채택될 예정이었음 — 게이트 통과로 폐기)

robots.txt 재검증 (선행 게이트 — 실측 2026-07-27)

대상결과판정
https://jrs.jobkorea.co.kr/robots.txtUser-agent: * / Allow: / / Sitemap: .../sitemap.xml금지 경로 없음, Crawl-delay 없음허용 (게이트 통과)
https://jrs.jobkorea.co.kr/wooribank200, 서버 렌더 HTML. 채용 3건 + 마감일 노출(2026.05.26(화) - 06.08(월) 14:00까지). SPA 아님수집 가능(구조는 아래 오픈 이슈)
https://www.jobkorea.co.kr/robots.txt (대조)AI 크롤러(ClaudeBot·anthropic-ai·GPTBot) 대부분 경로 Disallow: /, 검색 경로 일반 크롤러도 차단애그리게이터 전용 제약 — 회사 종속형과 다른 호스트라 무관

결론: 배민(BE-08) 폐기는 비공식 내부 API + robots가 사유였으나, 잡코리아 회사 종속형은 별도 호스트(jrs.)가 전면 허용이라 무산 사유 없음. 방안 A 설계 진행. 단, JRS 채용관은 회사별 커스텀 마케팅 셸이라 안정 공고 ID 추출 방식이 미검증(아래 Open Questions) — 이는 모델링(본 티켓)이 아니라 어댑터 티켓의 첫 구현 단계 검증 대상.

Detail Design

시스템 역할 경계

단위역할소유 데이터/책임노출 인터페이스의존
JobPlatform (enum, domain.common)플랫폼별 지원 소스 유형 선언supportedSourceTypessupports(sourceType): BooleanSourceType
SourceType (enum, domain.common)소스 유형 2종없음
JobSource (Entity, domain.company)소스 인스턴스 + 유형 검증행 단위 sourceType·companyId·sourceSlug·검색조건forCompany·forAggregator·reconstituteJobPlatform
JobSourceJpaEntity (infra)job_sources 매핑source_type 컬럼(변경 없음)
SourcePlatformAdapter (SPI, infra.posting — BE-02 미구현, 계약 amend)플랫폼×유형별 수집 어댑터자기 SourceCapabilitiessupports(platform, sourceType)·fetchList(descriptor)·capabilitiesRawJobPosting
JobSourceGatewayImpl (registry, infra.posting — BE-02 미구현)(platform, sourceType)로 어댑터 라우팅collect(descriptor)·capabilitiesOf(platform, sourceType)·categoriesOf(platform)List
JobkoreaCompanyBoundAdapter (신규 어댑터 티켓)jrs.jobkorea.co.kr/{slug} 회사 채용관 수집JRS 파싱·마감일 정규화supports(JOBKOREA, COMPANY_BOUND)SourceRequestExecutor
JobkoreaAggregatorAdapter (BE-28)www.jobkorea.co.kr 전역 검색www 파싱·경로 화이트리스트supports(JOBKOREA, AGGREGATOR)SourceRequestExecutor

서버 토폴로지: 신규 서버 없음. 모델링 변경은 기존 API 서버 도메인 계층 내. 수집은 기존 스케줄러(BE-10 0 0 0 * * *)에 회사 종속형 소스로 자연 편입 — 워커·별도 서버 불필요(1일 1회, 소스 수십 건 규모). 근거: 처리량이 초당 요청이 아닌 일 1회 배치라 단일 서버로 충분.

인터페이스 시그니처

// domain/common/JobPlatform.kt (변경)
enum class JobPlatform(val supportedSourceTypes: Set<SourceType>) {
    GREENHOUSE(setOf(SourceType.COMPANY_BOUND)),
    WOOWAHAN(setOf(SourceType.COMPANY_BOUND)),
    INCRUIT(setOf(SourceType.COMPANY_BOUND)),
    SARAMIN(setOf(SourceType.AGGREGATOR)),
    JUMPIT(setOf(SourceType.AGGREGATOR)),
    WANTED(setOf(SourceType.AGGREGATOR)),
    REMEMBER(setOf(SourceType.AGGREGATOR)),
    JOBKOREA(setOf(SourceType.AGGREGATOR, SourceType.COMPANY_BOUND)), // 겸용
    SURFIT(setOf(SourceType.AGGREGATOR)),
    ;
    fun supports(sourceType: SourceType): Boolean = sourceType in supportedSourceTypes
}
 
// domain/company/JobSource.kt (변경 — require만)
require(platform.supports(SourceType.COMPANY_BOUND)) { "${platform}은 회사 종속형을 지원하지 않습니다" } // forCompany
require(platform.supports(SourceType.AGGREGATOR))    { "${platform}은 애그리게이터형을 지원하지 않습니다" } // forAggregator
 
// infra/posting SPI — BE-02 계약 amend (platform 단독 → (platform, sourceType))
interface SourcePlatformAdapter {
    fun supports(platform: JobPlatform, sourceType: SourceType): Boolean
    val capabilities: SourceCapabilities        // 어댑터 인스턴스가 자기 능력 보유
    fun fetchList(descriptor: CollectionDescriptor): SourceCollectionOutcome
    fun supportedCategories(): List<AggregatorCategory>
}
// JobSourceGatewayImpl: adapters.first { it.supports(descriptor.platform, descriptor.sourceType) }
// capabilitiesOf(platform, sourceType)로 변경 — 겸용 플랫폼의 두 능력을 구분

클래스 역할 정의

도메인 모델

클래스명역할핵심 책임
JobPlatform플랫폼 유형 능력 선언supports(sourceType)로 겸용/전용 판정
JobSource소스 인스턴스팩토리에서 platform.supports()로 조합 검증, 행 단위 sourceType 확정

서비스 클래스

클래스명역할입력 → 출력의존
JobSourceGatewayImpl(amend)어댑터 라우팅CollectionDescriptorSourceCollectionOutcomeList

실패 경로·동시성·멱등

관심사설계
잘못된 조합 등록forCompany(SARAMIN, ...)platform.supports(COMPANY_BOUND)=falseIllegalArgumentException → 400. 겸용 불변식은 유지
등록 멱등uk_job_sources_company_bound (platform, source_slug)(JOBKOREA, wooribank) 재등록은 DuplicateKey. 애그리게이터 JOBKOREA 행(source_slug=NULL)과 충돌 없음(MySQL NULL distinct)
JRS 수집 실패/0건BE-10 소스 가드(FR-15) — 실패·0건 회차는 마감 판정·미발견 카운터 전면 skip. 어댑터 미구현 상태로 소스만 등록돼 매 회차 0건이어도 잘못된 CLOSED가 발생하지 않음 → 모델링 먼저 머지해도 안전
JRS 파서 고장어댑터 Failed 반환 → 소스 가드. 3일 연속 비정상 시 JobSourceHealth 고장 알림(BE-19)
robots 정책 변경JobSource.disable() 소프트 비활성화 — 회사·기존 공고 보존, 수집만 중단
수집 멱등(향후)Layer 1 JobPostingEvent.eventId 기반(기존 BE-10 설계)
동시성등록은 단일 사용자 API, 겸용 변경은 배포성 코드 변경 — 동시 쓰기 없음. 락 불필요

상태 전이 표 (팩토리 검증 — platform × 요청 유형)

신규 상태 머신 없음(JobSource의 seeded/disabled 전이는 기존 유지). 팩토리 검증 규칙:

platform × 요청forCompany(COMPANY_BOUND)forAggregator(AGGREGATOR)거부 사유
INCRUIT / GREENHOUSE / WOOWAHAN허용거부회사 종속 전용
SARAMIN / JUMPIT / WANTED / REMEMBER / SURFIT거부허용애그리게이터 전용
JOBKOREA허용(신규)허용겸용 — 둘 다 통과

Component Diagram

flowchart LR
    subgraph Domain["domain"]
        Platform[JobPlatform supports]
        Source[JobSource forCompany/forAggregator]
        SType[SourceType]
    end
    subgraph Infra["infrastructure.posting (BE-02 amend)"]
        Gateway[JobSourceGatewayImpl]
        JrsAd[JobkoreaCompanyBoundAdapter]
        WwwAd[JobkoreaAggregatorAdapter]
    end
    Source --> Platform
    Platform --> SType
    Gateway --> JrsAd
    Gateway --> WwwAd
    JrsAd -->|supports JOBKOREA COMPANY_BOUND| Gateway
    WwwAd -->|supports JOBKOREA AGGREGATOR| Gateway

Sequence Diagram (등록 — BE-21 케이스 6)

sequenceDiagram
    participant U as 등록 UseCase
    participant DS as CompanyDomainService
    participant S as JobSource
    participant P as JobPlatform
    U->>DS: 우리은행 잡코리아 소스 등록
    DS->>S: forCompany(companyId, JOBKOREA, "wooribank", jrsUrl)
    S->>P: supports(COMPANY_BOUND)
    P-->>S: true (겸용)
    S-->>DS: JobSource(sourceType=COMPANY_BOUND)
    DS->>DS: repository.save (uk 멱등)

ERD

스키마 변경 없음. job_sources는 현행 유지.

erDiagram
    COMPANIES ||--o{ JOB_SOURCES : has
    JOB_SOURCES {
        bigint id PK
        bigint company_id "NULL이면 애그리게이터"
        varchar source_type "행 단위 — 변경 없음"
        varchar platform "JOBKOREA 겸용"
        varchar source_slug "회사종속: wooribank"
    }
  • uk_job_sources_company_bound (platform, source_slug)(JOBKOREA, wooribank) 수용.
  • uk_job_sources_aggregator (platform, search_category_code, search_keyword) — 애그리게이터 JOBKOREA 별도 수용. 두 행 공존 가능.

Testing Plan

레벨대상시나리오
domainJobPlatformJOBKOREA.supports(COMPANY_BOUND)=true·supports(AGGREGATOR)=true; INCRUIT.supports(AGGREGATOR)=false; SARAMIN.supports(COMPANY_BOUND)=false
domainJobSourceforCompany(JOBKOREA, "wooribank") 성공, sourceType=COMPANY_BOUND; forAggregator(JOBKOREA, ...) 성공; forCompany(SARAMIN)·forAggregator(INCRUIT) 여전히 예외(회귀)
infrastructureJobSourceRepositoryImplTestcontainers — 겸용 JOBKOREA 회사 종속형 저장·복원 왕복; (JOBKOREA, wooribank) 중복 저장 거부; 애그리게이터 JOBKOREA 행과 회사 종속형 JOBKOREA 행 공존
infrastructureSPI 디스패치(BE-02 amend 시)(JOBKOREA, COMPANY_BOUND) → JRS 어댑터, (JOBKOREA, AGGREGATOR) → www 어댑터로 라우팅
presentation/scenario등록 API우리은행 잡코리아 회사 종속형 등록이 200(BE-21 케이스 6 재현)

핵심 실패 경로: 잘못된 조합 400 유지(회귀), JRS 어댑터 부재 시 0건 회차가 소스 가드로 무해함.

Release Scenario — 무중단 배포

expand-contract·데이터 마이그레이션 불필요 — 스키마 변경 0건, 기존 데이터 무영향. 변경은 팩토리 검증 완화(이전에 거부되던 조합만 신규 허용)라 기존 흐름 동작이 바뀌지 않음.

단계내용롤백
1. 모델링 배포JobPlatform 지원 집합 + JobSource 가드 완화 배포. 기존 소스·API 무영향코드 revert (하위 호환이라 데이터 정리 불필요)
2. 소스 등록우리은행 잡코리아 회사 종속형 소스 1건 등록(disabled 아님). 어댑터 미구현이면 수집은 0건이나 소스 가드로 무해disable() 소프트 오프
3. 어댑터 배포(후속 티켓)JobkoreaCompanyBoundAdapter 배포 후 수집 시작. 시딩 회차는 알림 없이 저장(BE-10 FR-10)disable() — robots 정책 변경·파서 고장 시 즉시 중단
  • 피처 플래그 불필요: 검증 완화는 “새 조합 허용”일 뿐 런타임 분기가 아님. 수집 on/off는 기존 disabled_at + posting.auto-close 플래그가 담당.
  • 배포 순서: 코드 먼저(모델링) → 데이터(소스 등록) → 어댑터. 각 단계 독립 배포.

Open Questions

  1. JRS 안정 공고 ID (어댑터 티켓 검증 대상)jrs.jobkorea.co.kr/{slug}는 회사별 커스텀 마케팅 셸로, 실측(2026-07-27)상 목록에 data-gno·상세 링크가 노출되지 않음. 채용관이 표준 잡코리아 상세(/Recruit/GI_Read/{gno})로 링크아웃하는지, 내부 JSON 엔드포인트가 있는지 어댑터 구현 첫 단계에서 2~3개 실제 채용관으로 확정해야 함. 모델링(본 티켓)에는 영향 없음.
  2. 우리은행 잡코리아 실사용 필요성 — 사용자 확인 필요: 회사 종속형 잡코리아 소스를 지금 등록·수집까지 갈지, 모델링만 열어두고 어댑터는 보류할지.
  3. BE-02 구현 상태 — 현재 코드에 posting 도메인·SPI·어댑터 미구현. SPI (platform, sourceType) 디스패치는 BE-02 구현 시점에 반영(계약 amend). BE-02가 이미 착수됐다면 supports(platform)supports(platform, sourceType) 시그니처 수정 필요.

Document History

날짜변경 내용
2026-07-27최초 작성 — robots 게이트 통과(방안 A 채택), 스키마 무변경 확인, SPI (platform, sourceType) 디스패치 amend