
안녕하세요! 오늘은 Kotlin으로 기상청 API를 연동하는 프로젝트를 진행하면서 마주한 컴파일 오류들을 해결하는 과정을 상세히 공유해보려고 합니다.
실제 개발 현장에서 자주 발생하는 문제들이니 참고하시면 좋을 것 같습니다.
이번 프로젝트는 기상청의 단기예보 API를 활용하여 실시간 날씨 정보를 제공하는 서비스를 개발하는 것이었습니다.
개발 과정에서 주로 다음과 같은 문제들이 발생했습니다.
1. API 호출 시 "NO_DATA" 오류 - 잘못된 시간 설정으로 인한 데이터 부재
2. 컴파일 오류 3가지 - 접근 제한자, JSON 파싱, 생성자 호출 문제
3. 재시도 로직의 비효율성 - 동적 시간 설정 부재
// ❌ 오류 발생 코드
var retryTimeIndex = DateTimeUtils.FORECAST_TIMES.indexOf(currentTime)
// Cannot access 'FORECAST_TIMES': it is private in 'DateTimeUtils'
문제 원인
// 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)
// ❌ 오류 발생 코드
return ApiClientUtility.getObjectMapper().readValue'elles(response, RealTimeDustResponse::class.java)
// Function invocation 'readValue(...)' expected
문제 원인
// ✅ 수정된 코드
return ApiClientUtility.getObjectMapper().readValue(response, RealTimeDustResponse::class.java)
해결 방법
// ✅ 수정된 코드
return ApiClientUtility.getObjectMapper().readValue(response, RealTimeDustResponse::class.java)
// ❌ 오류 발생 코드
return UVIndex(
region = region,
measurement "UV Index", // 문법 오류
value = 0.0,
grade = "낮음",
timestamp = LocalDateTime.now()
)
문제 원인
해결 방법
// ✅ 수정된 코드
return UVIndex(
region = region,
measurement = "UV Index", // = 추가
value = 0.0,
grade = "낮음",
timestamp = LocalDateTime.now()
)
기상청 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
)
}
}
}
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. 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)
// 결과 검증 로직
}
| 항목 | 개선 전 | 개선 후 |
|---|---|---|
| 컴파일 오류 | 3개 발생 | 0개 |
| API 호출 성공률 | ~60% | ~95% |
| 재시도 로직 | 비효율적 | 시간 기반 스마트 재시도 |
| 코드 가독성 | 낮음 | 높음 |
| 유지보수성 | 어려움 | 용이함 |
// Redis 또는 로컬 캐시를 활용한 API 응답 캐싱
@Cacheable("weather-data")
suspend fun getCachedWeatherData(date: String, time: String): WeatherData?
// 코루틴을 활용한 병렬 API 호출
async {
val weather = getWeatherData()
val dust = getDustData()
val uv = getUVData()
combineWeatherInfo(weather, dust, uv)
}
이번 프로젝트를 통해 실제 외부 API 연동 시 마주할 수 있는 다양한 문제들과 그 해결 과정을 경험할 수 있었습니다.
특히 다음과 같은 부분들이 중요했습니다.
1. 철저한 API 문서 분석: 데이터 발표 시간, 응답 형식 등 사전 파악
2. 단계적 문제 해결: 컴파일 오류부터 로직 오류까지 순차적 접근
3. 테스트 주도 개발: 각 개선사항에 대한 검증 방법 수립
4. 로깅과 모니터링: 운영 환경에서의 안정성 확보
날씨 API 연동이나 유사한 외부 API 연동 프로젝트를 진행하시는 분들께 도움이 되었으면 좋겠습니다.
궁금한 점이나 추가적인 개선 아이디어가 있으시면 댓글로 공유해주세요! 🌟