배경 및 목표

프로그래머스와 일부 플로우에선 동기 방식으로 연동하고 있어, 외부 플랫폼 장애 발생 시 자사 서비스에 영향을 주었습니다. 또한 신규 채용 플랫폼을 추가할 때마다 기존 코드를 수정해야 했습니다.

또한 잡플래닛과의 연동이 단방향(자사에서 잡플래닛)이어서, 잡플래닛에서 자체적으로 공고 상태를 변경해도 감지할 수 없었습니다. 예를 들어 잡플래닛에서 공고를 마감 처리했는데 자사 시스템에서는 여전히 게시 중으로 표시되는 등, 양쪽 공고 상태가 불일치하는 상황이 지속될 위험이 있었습니다.

목표

  • 외부 플랫폼 장애가 자사 서비스로 전파되지 않도록 연동을 격리합니다.
  • 신규 플랫폼을 기존 코드 수정 없이 토픽 구독만으로 확장합니다.
  • 단방향 연동으로 감지 못하던 외부 데이터 변경을 주기적으로 반영합니다.
  • 정합성 오류를 조기에 발견해 빠르게 대응합니다.

해결 방법과 해결 후보군

후보군 비교 (아웃바운드 연동)

방식설명한계
동기 호출 유지외부 API 직접 호출외부 장애 전파, 신규 플랫폼 추가 시 코드 수정
동기 + 재시도 래핑try-catch 재시도장애 시 응답 지연 여전, 결합 유지
이벤트 드리븐 (채택)Kafka 이벤트 발행장애 격리, 토픽 구독만으로 플랫폼 확장

후보군 비교 (역방향 정합성)

방식설명한계
실시간 양방향 동기화변경 즉시 반영외부 협조 필수, 구현, 유지보수 비용 큼
조회 시마다 외부 확인조회 때 외부 대조매 조회 외부 호출로 지연, 부하
주기 배치 + 이벤트 동기화 (채택)배치가 회사별 이벤트 발행, 컨슈머가 대조, 업데이트준실시간이나 외부 결합 없이 자사만으로 구현 가능

1. 이벤트 드리븐 아웃바운드 연동 + 장애 격리

Kafka 이벤트 드리븐 아키텍처 전환

동기 호출을 제거하고, 자사 서비스는 Kafka 토픽에 이벤트만 발행하는 구조로 변경했습니다. 외부 플랫폼과의 직접 의존이 사라지므로, 외부 장애가 자사 서비스에 전파되지 않습니다.

// 자사 서비스: 이벤트만 발행
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun onEntityCreated(event: EntityCreatedEvent) {
    kafkaTemplate.send("event.domain.created", event.toMessage())
}

플랫폼별 컨슈머 그룹 분리

각 외부 플랫폼에 독립적인 컨슈머 그룹을 부여했습니다. 잡플래닛 컨슈머에 문제가 생겨도 다른 플랫폼 컨슈머는 정상 동작합니다. 신규 플랫폼 추가 시에는 새 컨슈머 그룹이 토픽을 구독하기만 하면 되므로, 기존 코드 수정이 불필요합니다.

실패 기록 + 재시도 API

네트워크 순단 등으로 일부 플랫폼 처리가 실패할 수 있으므로, 실패를 감지하면 실패 건을 별도로 기록하고, 미리 만들어 둔 재시도 API로 해당 건만 재처리합니다.

sequenceDiagram
    participant K as Kafka
    participant C as 플랫폼 컨슈머
    participant DB as 실패 기록
    participant Op as 운영자
    participant R as 재시도 API
    K->>C: 이벤트 전달
    C->>C: ❌ 처리 실패
    C->>DB: 실패 건 기록
    Op->>R: 재시도 API 호출
    R->>C: 해당 건 재처리

이 이벤트 드리븐 연동 패턴이 이후 ORP, 프로그래머스 등 모든 외부 연동의 표준이 되었습니다.

2. 역방향 정합성 배치

앞의 아웃바운드 연동은 자사에서 잡플래닛으로 향하는 단방향이므로, 잡플래닛이 자체적으로 변경한 상태는 역방향으로 감지해 정합성을 완결합니다.

주기적 정합성 배치 + 이벤트 기반 동기화 구축

배치 스케줄러가 주기적으로 SyncUp 배치를 실행하고, 배치는 회사별로 동기화 이벤트를 Kafka에 발행합니다. 컨슈머가 이벤트를 수신해 잡플래닛 API에서 전체 데이터를 조회하고, 자사 DB의 공고 상태와 비교하는 구조로 구축했습니다.

// 배치: 회사별 동기화 이벤트 발행
@Component
class SyncUpBatch(
    private val companyRepository: CompanyRepository,
    private val eventPublisher: SyncEventPublisher
) {
    fun run() {
        companyRepository.findAllLinked().forEach { company ->
            eventPublisher.publish(CompanySyncRequested(company.id))
        }
    }
}
 
// 컨슈머: 외부 조회 → 대조 → 업데이트 → 로깅
@Component
class CompanySyncConsumer(
    private val externalClient: ExternalPlatformClient,
    private val entityRepository: EntityRepository
) {
    fun consume(event: CompanySyncRequested) {
        val externalEntities = externalClient.fetchAll(event.companyId)
        val internalEntities = entityRepository.findAllLinked(event.companyId)
 
        externalEntities.forEach { external ->
            val internal = internalEntities.find { it.externalId == external.id }
            if (internal != null && internal.status != external.status) {
                internal.updateStatus(external.status)
                log.info("상태 불일치 감지: ${internal.title} (자사: ${internal.status} → 외부: ${external.status})")
            }
        }
    }
}

컨슈머가 잡플래닛 측의 공고 상태(게시 중, 마감, 삭제 등)와 자사 시스템의 공고 상태를 대조하여, 불일치가 발견되면 자사 DB를 잡플래닛 상태에 맞게 업데이트하고 변경 이력을 로깅합니다.

정합성 동기화 흐름

sequenceDiagram
    participant Sch as 배치 스케줄러
    participant Batch as SyncUp 배치
    participant K as Kafka 이벤트 큐
    participant C as 동기화 컨슈머
    participant Ext as 잡플래닛 API
    participant DB as 자사 DB
    Sch->>Batch: 주기적 실행
    loop 회사별 동기화
        Batch->>K: 회사별 동기화 이벤트 발행
        K->>C: 이벤트 수신
        C->>Ext: 공고 데이터 조회
        Ext-->>C: 공고 목록 + 상태
        C->>DB: 자사 공고 상태 조회
        C->>C: 양측 공고 상태 비교
        alt 상태 불일치 발견
            C->>DB: 자사 공고 상태 업데이트
            C->>C: 변경 이력 로깅
        end
    end

결과

지표결과
장애 격리100%
신규 플랫폼 확장토픽 구독만으로
부분 실패허용 + 재시도
응답 시간외부 API 대기에서 즉시로
테스트컨슈머별 독립 단위 테스트
상태 정합성양측 불일치 해소
사용자 영향잘못된 공고 상태 노출 방지

모니터링

  • 플랫폼별 컨슈머 그룹의 랙, 실패율과 재시도 API 호출 건수를 관측합니다.
  • 외부 장애 발생 시 자사 서비스 영향도(격리 여부)를 관측합니다.
  • 회사별 동기화 이벤트 처리 시 불일치 감지 건수와 업데이트 건수를 관측합니다.