[BE-05] 에어코리아 Gateway 구현 — 3단계 체인 + Redis 캐시 + graceful degrade

작업 내용 (설계 의도)

변경 사항

에어코리아 3단계 API 체인을 단일 Gateway 뒤로 캡슐화하고, 측정 결과를 Redis에 캐시해 500/day 한도·P95 1.5초를 보호한다. BE-03의 계약을 구현한다. 근거 TDD: ../TDD.md FR-8·FR-10·“방안 비교 대기질 캐싱”.

  • infrastructure/airquality/gateway/AirQualityProperties.kt@ConfigurationProperties("external.air-quality")(base-url + api-key).
  • application.ymlexternal.air-quality 블록 추가(base-url ${EXTERNAL_AIR_QUALITY_BASE_URL:http://localhost:9102}, api-key ${DATA_GO_KR_SERVICE_KEY:mock-service-key}). 이 wave에서 application.yml 단독 수정자.
  • infrastructure/airquality/gateway/AirKoreaAirQualityGatewayImpl.ktExternalRestClientFactory로 RestClient 생성. current(lat,lng): 그리드키(lat·lng 소수 3자리) 캐시 조회 → 미스 시 getTMStdrCrdnt → getNearbyMsrstnList → getMsrstnAcctoRltmMesureDnsty 순 호출 → measurement 캐시 저장. Client DTO는 infra에 위치, @JsonIgnoreProperties. ConsumerRecord 아님/LocalDateTime 금지(ZonedDateTime). 임의 단계 RestClientException·타임아웃·빈응답 → AirQualityMeasurement.empty().
  • infrastructure/airquality/redis/AirQualityRedisCache.ktAirQualityMeasurementCache 구현. StringRedisTemplate + ObjectMapper(기존 PopularProductsRedisRepository 패턴). 키 airquality:measurement:{gridKey}, TTL 10분 + jitter. Redis 장애 시 캐시 skip.

롤백

  • 실연동 → env를 mock host로 되돌려 재기동(즉시). 기능 전체 → 컨트롤러 미노출.

의존

  • BE-03

다이어그램

처리 흐름

sequenceDiagram
    participant S as AirQualityDomainService
    participant G as AirKoreaGatewayImpl
    participant K as AirQualityRedisCache
    participant A as 에어코리아 API
    S->>G: current(lat,lng)
    G->>K: findBy(gridKey)
    alt 미스
        G->>A: getTMStdrCrdnt→getNearbyMsrstnList→getMsrstnAcctoRltmMesureDnsty
        A-->>G: pm10,pm25 (or 실패)
        G->>K: save(gridKey, measurement)
    end
    G-->>S: measurement (or empty)

클래스 의존

flowchart LR
    Impl[AirKoreaGatewayImpl] --> Props[AirQualityProperties]
    Impl --> Cache[AirQualityRedisCache]
    Impl -.->|impl| Gw[AirQualityGateway]
    Cache -.->|impl| CIf[AirQualityMeasurementCache]

테스트 케이스

  • 3단계 체인이 순서대로 호출되어 pm10·pm25·측정소·측정시각을 파싱한다
  • 캐시 히트 시 외부 API를 0회 호출하고 캐시 값을 반환한다
  • getNearbyMsrstnList가 5xx면 AirQualityMeasurement.empty()를 반환한다(예외 전파 없음)
  • read 타임아웃 시 empty로 degrade한다
  • 동일 그리드키 반복 요청은 TTL 내 캐시로 수렴한다(멱등)