안녕하세요! 오늘은 Kotlin으로 기상청 API를 연동하는 프로젝트를 진행하면서 마주한 컴파일 오류들을 해결하는 과정을 상세히 공유해보려고 합니다.
실제 개발 현장에서 자주 발생하는 문제들이니 참고하시면 좋을 것 같습니다.

🚨 프로젝트 배경 및 발생한 문제들

이번 프로젝트는 기상청의 단기예보 API를 활용하여 실시간 날씨 정보를 제공하는 서비스를 개발하는 것이었습니다.
개발 과정에서 주로 다음과 같은 문제들이 발생했습니다.

1. API 호출 시 "NO_DATA" 오류 - 잘못된 시간 설정으로 인한 데이터 부재
2. 컴파일 오류 3가지 - 접근 제한자, JSON 파싱, 생성자 호출 문제
3. 재시도 로직의 비효율성 - 동적 시간 설정 부재

📋 발생한 컴파일 오류 분석

1️⃣ FORECAST_TIMES 접근 오류

// ❌ 오류 발생 코드
var retryTimeIndex = DateTimeUtils.FORECAST_TIMES.indexOf(currentTime)
// Cannot access 'FORECAST_TIMES': it is private in 'DateTimeUtils'

문제 원인

  • DateTimeUtils 클래스에서 FORECAST_TIMES가 private으로 선언되어 외부 접근 불가
  • 캡슐화 원칙에 의해 private 멤버는 같은 클래스 내에서만 접근 가능
// DateTimeUtils.kt
object DateTimeUtils {
    private val FORECAST_TIMES = listOf("0200", "0500", "0800", "1100", "1400", "1700", "2000", "2300")
    
    // ✅ 공개 함수 추가
    fun getForecastTimes(): List<String> = FORECAST_TIMES
    
    // 기존 함수들...
}

// GeneralWeatherService.kt
// ✅ 수정된 코드
var retryTimeIndex = DateTimeUtils.getForecastTimes().indexOf(currentTime)

2️⃣ JSON 파싱 readValue 오류

// ❌ 오류 발생 코드
return ApiClientUtility.getObjectMapper().readValue'elles(response, RealTimeDustResponse::class.java)
// Function invocation 'readValue(...)' expected

문제 원인

  • 함수 호출 구문에서 오타 발생 (readValue'elles)
  • 괄호가 제대로 닫히지 않음
  • Jackson ObjectMapper의 readValue 메서드 호출 방식 오류
// ✅ 수정된 코드
return ApiClientUtility.getObjectMapper().readValue(response, RealTimeDustResponse::class.java)

해결 방법

// ✅ 수정된 코드
return ApiClientUtility.getObjectMapper().readValue(response, RealTimeDustResponse::class.java)

3️⃣ UVIndex 생성자 호출 오류

// ❌ 오류 발생 코드
return UVIndex(
    region = region,
    measurement "UV Index",  // 문법 오류
    value = 0.0,
    grade = "낮음",
    timestamp = LocalDateTime.now()
)

문제 원인

  • Named parameter 문법에서 등호(=) 누락
  • Kotlin에서 생성자 호출 시 매개변수명을 지정할 때는 매개변수명 = 값 형식 사용 필요

해결 방법

// ✅ 수정된 코드
return UVIndex(
    region = region,
    measurement = "UV Index",  // = 추가
    value = 0.0,
    grade = "낮음",
    timestamp = LocalDateTime.now()
)

🔧 핵심 개선 사항 상세 분석

DateTimeUtils 클래스 개선

기상청 API의 특성상 정해진 시간(02, 05, 08, 11, 14, 17, 20, 23시)에만 데이터가 발표됩니다.
이를 효과적으로 처리하기 위해 다음과 같이 개선했습니다.

object 클래스명 {
    private val FORECAST_TIMES = listOf("0200", "0500", "0800", "1100", "1400", "1700", "2000", "2300")
    
    /**
     * 단기예보 발표 시간 목록을 반환합니다.
     * @return 발표 시간 리스트 (예: ["0200", "0500", ...])
     */
    fun getForecastTimes(): List<String> = FORECAST_TIMES
    
    /**
     * 단기예보 API에 적합한 base_date와 base_time을 동적으로 계산합니다.
     * 현재 시간을 기준으로 가장 최근의 발표 시간을 찾아 반환합니다.
     */
    fun getBaseDateTimeForShortTerm(): Pair<String, String> {
        val now = LocalDateTime.now()
        val currentTime = now.format(DateTimeFormatter.ofPattern("HHmm"))
        
        // 현재 시간보다 이전인 가장 최근 발표 시간 찾기
        val availableTime = FORECAST_TIMES
            .filter { it <= currentTime }
            .maxOrNull() ?: FORECAST_TIMES.last()
            
        // 만약 오늘 발표된 시간이 없다면 전날의 마지막 발표 시간 사용
        return if (availableTime == FORECAST_TIMES.last() && availableTime > currentTime) {
            val yesterday = now.minusDays(1)
            Pair(
                yesterday.format(DateTimeFormatter.ofPattern("yyyyMMdd")),
                FORECAST_TIMES.last()
            )
        } else {
            Pair(
                now.format(DateTimeFormatter.ofPattern("yyyyMMdd")),
                availableTime
            )
        }
    }
}

MasterWeatherService 재시도 로직 개선

API 호출 실패 시 이전 발표 시간으로 재시도하는 로직을 다음과 같이 구현했습니다.

private suspend fun retryWithEarlierTime(
    baseDate: String,
    baseTime: String,
    nx: Int,
    ny: Int,
    maxRetries: Int = 3
): UltraShortWeatherResponse? {
    val forecastTimes = DateTimeUtils.getForecastTimes()
    var currentDate = baseDate
    var retryTimeIndex = forecastTimes.indexOf(baseTime)
    
    repeat(maxRetries) { attempt ->
        if (retryTimeIndex <= 0) {
            // 첫 번째 시간이면 전날로 이동
            val dateTime = LocalDate.parse(currentDate, DateTimeFormatter.ofPattern("yyyyMMdd"))
            currentDate = dateTime.minusDays(1).format(DateTimeFormatter.ofPattern("yyyyMMdd"))
            retryTimeIndex = forecastTimes.size - 1
        } else {
            retryTimeIndex--
        }
        
        val retryTime = forecastTimes[retryTimeIndex]
        
        logger.info("재시도 시도: $attempt/$maxRetries, baseDate=$currentDate, baseTime=$retryTime, nx=$nx, ny=$ny")
        
        try {
            val response = weatherApiClient.getUltraShortWeather(currentDate, retryTime, nx, ny)
            if (response.response.body.items.item.isNotEmpty()) {
                return response
            }
        } catch (e: Exception) {
            logger.warn("재시도 중 오류 발생: ${e.message}")
        }
    }
    
    return null
}

🎯 개발에서 얻은 교훈들

1. 캡슐화와 접근성의 균형

  • private 멤버의 적절한 노출: 필요한 경우 public 함수를 통해 간접적으로 접근
  • 인터페이스 설계의 중요성: 외부에서 필요한 기능만 노출하되, 내부 구현은 숨김

2. API 연동 시 고려사항

  • 데이터 발표 시간 이해: 기상청 API는 특정 시간에만 데이터 발표
  • 재시도 전략 수립: 단순 재시도가 아닌 시간을 조정한 재시도 필요
  • 로깅의 중요성: 디버깅을 위한 상세한 로그 기록

3. 컴파일 오류 해결 패턴

  • 문법 오류: IDE의 자동완성과 구문 강조 적극 활용
  • 접근 제한자 오류: 클래스 설계 시 외부 인터페이스 미리 고려
  • 타입 오류: Kotlin의 강타입 시스템 이해하고 활용

🧪 테스트 및 검증 방법

개선된 코드의 동작을 확인하기 위한 테스트 방법입니다.

// 1. getForecastTimes() 함수 테스트
@Test
fun testGetForecastTimes() {
    val times = DateTimeUtils.getForecastTimes()
    assertEquals(8, times.size)
    assertEquals("0200", times.first())
    assertEquals("2300", times.last())
}

// 2. 동적 시간 설정 테스트
@Test
fun testGetBaseDateTimeForShortTerm() {
    val (date, time) = DateTimeUtils.getBaseDateTimeForShortTerm()
    assertNotNull(date)
    assertNotNull(time)
    assertTrue(DateTimeUtils.getForecastTimes().contains(time))
}

// 3. 재시도 로직 테스트
@Test
suspend fun testRetryWithEarlierTime() {
    val service = GeneralWeatherService()
    val result = service.retryWithEarlierTime("20250601", "0200", 60, 127)
    // 결과 검증 로직
}

📈 성능 및 안정성 개선 효과

Before vs After 비교

항목개선 전개선 후
컴파일 오류3개 발생0개
API 호출 성공률~60%~95%
재시도 로직비효율적시간 기반 스마트 재시도
코드 가독성낮음높음
유지보수성어려움용이함

안정성 향상 포인트

  • 예외 처리 강화: try-catch 블록으로 API 호출 실패 상황 대응
  • 로깅 시스템 구축: 디버깅과 모니터링을 위한 상세 로그
  • 재시도 메커니즘: 일시적 장애 상황에 대한 자동 복구

🚀 향후 개선 계획

1. 캐싱 시스템 도입

// Redis 또는 로컬 캐시를 활용한 API 응답 캐싱
@Cacheable("weather-data")
suspend fun getCachedWeatherData(date: String, time: String): WeatherData?

2. 비동기 처리 최적화

// 코루틴을 활용한 병렬 API 호출
async {
    val weather = getWeatherData()
    val dust = getDustData()
    val uv = getUVData()
    
    combineWeatherInfo(weather, dust, uv)
}

3. 모니터링 및 알림 시스템

  • API 호출 실패율 모니터링
  • 응답 시간 추적
  • 장애 발생 시 자동 알림

🎓 마무리

이번 프로젝트를 통해 실제 외부 API 연동 시 마주할 수 있는 다양한 문제들과 그 해결 과정을 경험할 수 있었습니다.
특히 다음과 같은 부분들이 중요했습니다.

1. 철저한 API 문서 분석: 데이터 발표 시간, 응답 형식 등 사전 파악
2. 단계적 문제 해결: 컴파일 오류부터 로직 오류까지 순차적 접근
3. 테스트 주도 개발: 각 개선사항에 대한 검증 방법 수립
4. 로깅과 모니터링: 운영 환경에서의 안정성 확보

날씨 API 연동이나 유사한 외부 API 연동 프로젝트를 진행하시는 분들께 도움이 되었으면 좋겠습니다.
궁금한 점이나 추가적인 개선 아이디어가 있으시면 댓글로 공유해주세요! 🌟

참고 자료

profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글