Trouble Shooting

[Android] UTC와 KST의 Timezone 보정 오류 해결

Marchbreeze 2025. 5. 16. 00:17
2024년 4월 6일에 작성되었던 글입니다.

문제 상황

  • 방 생성 시 시간을 설정해도, 생성된 방에서는 9시간 이후의 시간으로 보여지는 이슈가 있었습니다.
  • 서버로직을 점검해 보니, 생성 요청(request)에는 "2024-04-15T10:00:00.000Z"(UTC) 포맷을 보냈지만, 조회 응답(response)에서는 "2024-04-15 19:00:00"(KST) 형태로 받아와 9시간 차이가 발생했습니다.

 

 


1. TimeZone에 대한 이해

타임존(TimeZone)

서로 다른 지역의 현지 시간을 관리하기 위한 기준입니다.

전 세계를 UTC 기준으로 나눈 표준시로, 각 지역은 UTC에 더하거나 뺀 시차(offset)를 가집니다.

  • UTC (Coordinated Universal Time) : 지구 자전과 원자시계를 결합해 만든 국제 표준시입니다
  • KST (Korea Standard Time) : 대한민국의 표준시로, UTC에 9시간을 더한 시간대입니다. (ex. UTC 10:00 → KST 19:00)

 

ISO 8601 vs 로컬 표현

 

"2024-04-12T10:00:02.000Z"

  • ISO 8601 국제 표준 포맷
  • T는 날짜와 시간 구분, Z는 UTC 기준임을 나타냅니다.

"2024-04-12 19:00:02"

  • 시간대 정보 없이 로컬 표기로만 제공된 경우
  • 컨텍스트에 따라 KST(UTC+9)나 다른 시간대로 해석될 수 있습니다.

 

 


2. 시간 변환 로직 수정

(1) 시간 기준 설정

private const val UTC_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
private const val NO_TIME_FORMAT = "yyyy-MM-dd"
private const val WRONG_FORMAT = "날짜 형식이 잘못되었습니다."

private val UTC_TIME_ZONE: TimeZone = TimeZone.getTimeZone("UTC")
private val KOREA_TIME_ZONE: TimeZone = TimeZone.getTimeZone("Asia/Seoul")

private val utcFormat = SimpleDateFormat(UTC_DATE_FORMAT, Locale.KOREA).apply {
    timeZone = UTC_TIME_ZONE
}
private val kstFormat = SimpleDateFormat(UTC_DATE_FORMAT, Locale.KOREA).apply {
    timeZone = KOREA_TIME_ZONE
}
private val noTimeKstFormat = SimpleDateFormat(NO_TIME_FORMAT, Locale.KOREA).apply {
    timeZone = KOREA_TIME_ZONE
}
  • 클라이언트에서 사용하는 모든 시간 : KST(UTC +9) 기준
  • 서버에서 사용하는 모든 시간 : UTC(UTC +0) 기준

 

(2) Date → Calendar 변환

// String 값으로 받은 expiration을 Date로 변환
private fun getDateInstanceFromLocalFormat(localFormatString: String): Date {
    val inputFormat = SimpleDateFormat(LOCAL_DATE_FORMAT, Locale.KOREA)
    return inputFormat.parse(localFormatString)
        ?: throw IllegalArgumentException("날짜 형식이 잘못되었습니다.")
}

// Date을 시간대를 적용한 후 Calendar 로 변환
fun getCalendarFromLocalFormat(localFormatString: String): Calendar {
    // KST(TimeZone) 기준으로 Calendar 인스턴스 생성
    return Calendar.getInstance(KOREA_TIME_ZONE).apply {
        // 파싱된 Date(UTC 기준)를 Calendar.time에 설정
        time = getDateInstanceFromLocalFormat(localFormatString)
        // KST(UTC+9) → UTC 보정
        add(Calendar.HOUR_OF_DAY, -9)
    }
}
  • Calendar.getInstance으로 KST 기준 Calendar를 얻은 뒤,
  • time = Date 로 변환된 시간을 설정하고,
  • add(Calendar.HOUR_OF_DAY, -9)로 9시간 뒤(UTC 기준)로 이동했습니다.

 

(3) 날짜 차이 계산

// 밀리초 단위 두 시각 차이로 일수 계산 (올림)
private fun getDayDiff(laterMs: Long, earlyMs: Long): Int {
    val gap = laterMs - earlyMs
    val days = gap / (24f * 60 * 60 * 1000)
    return days.roundToInt()
}

// String 입력 두 개를 Calendar로 변환해 일수 차이 계산
fun getDayDiff(later: String, early: String): Int {
    val laterCal = getCalendarFromLocalFormat(later).apply { initToMidnight() }
    val earlyCal = getCalendarFromLocalFormat(early).apply { initToMidnight() }
    return getDayDiff(laterCal.timeInMillis, earlyCal.timeInMillis)
}

// 현재 시각 대비 남은 일수 계산
fun getDayDiffFromNow(localFormatString: String): Int {
    val targetCal = getCalendarFromLocalFormat(localFormatString).apply { initToMidnight() }
    // 시스템 기본 타임존 기준 현재 시각
    val nowCal = Calendar.getInstance().apply { initToMidnight() }
    return getDayDiff(targetCal.timeInMillis, nowCal.timeInMillis)
}

// 시·분·초 이하를 0으로 초기화해 '날짜 단위'만 비교
private fun Calendar.initToMidnight() {
    set(Calendar.HOUR_OF_DAY, 0)
    set(Calendar.MINUTE, 0)
    set(Calendar.SECOND, 0)
    set(Calendar.MILLISECOND, 0)
}
  • getCalendarFromLocalFormat 으로 파싱한 시각을 UTC 기준으로 보정한 뒤,
  • initToMidnight() 확장 함수로 시·분·초·밀리초를 모두 0으로 만들어 날짜 단위로 비교했습니다.
  • Calendar.getInstance()로 얻은 현재 시각에도 같은 초기화를 적용해, 올바른 일수 차이를 구했습니다.

 

다음과 같이 UTC와 KST 간 9시간 차이를 명확히 이해하고, 타임존 보정을 적용한 문자열 변환 과정을 통해서 문제 상황을 해결했습니다.