티스토리 뷰

프로그래밍은 언제나 숫자와의 경쟁인 것 같다. 반올림이 되느냐, 부동소수점이냐, 정수 오버플로우(integer overflow) 등은 백발이 되어 코드를 만질 때 까지 항상 따라다니는 문제가 될 것이다.

MySQL 날짜 관련 이슈

얼마 전 필자가 다니는 회사에서 발생한 데이터베이스 관련 이슈로 다음과 같은 문제가 발생하였다. 아래는 MySQL 관련 문제에 대하여 공유된 내용이다.

MySQL 5.6.4 부터 시간값 저장시 밀리세컨드를 지원한다. 하지만 DATETIME의 경우 길이가 6일 경우에만 가능하다. 그런데, DATETIME 타입(이는 DATETIME(4)와 같다)일 경우 밀리세컨드 부분을 반올림(round)하는 버그가 있다. #68760
이 버그로 인해 1999년 12월 31일 23시 59분 59초 500ms 같은 값을 DATETIME 타입에 입력하면 반올림되어 2000년 1월 1일 0시 0분 0초 0ms 가 돼 버리는 현상이 발생한다.

MySQL Community Server 의 C 언어 소스코드를 보면 날짜와 관련된 데이터를 다루는 세 가지가 데이터 타입이 있다. MYSQL_TYPE_DATE, MYSQL_TYPE_DATETIME, MYSQL_TYPE_TIMESTAMP.

// MySQL 데이터 타입  
/*****************/  
#define CLIENT_MULTI_QUERIES    CLIENT_MULTI_STATEMENTS      
#define FIELD_TYPE_DECIMAL     MYSQL_TYPE_DECIMAL  
#define FIELD_TYPE_NEWDECIMAL  MYSQL_TYPE_NEWDECIMAL  
#define FIELD_TYPE_TINY        MYSQL_TYPE_TINY  
#define FIELD_TYPE_SHORT       MYSQL_TYPE_SHORT  
#define FIELD_TYPE_LONG        MYSQL_TYPE_LONG  
#define FIELD_TYPE_FLOAT       MYSQL_TYPE_FLOAT  
#define FIELD_TYPE_DOUBLE      MYSQL_TYPE_DOUBLE  
#define FIELD_TYPE_NULL        MYSQL_TYPE_NULL  
#define FIELD_TYPE_TIMESTAMP   MYSQL_TYPE_TIMESTAMP  
#define FIELD_TYPE_LONGLONG    MYSQL_TYPE_LONGLONG  
#define FIELD_TYPE_INT24       MYSQL_TYPE_INT24  
#define FIELD_TYPE_DATE        MYSQL_TYPE_DATE  
#define FIELD_TYPE_TIME        MYSQL_TYPE_TIME  
#define FIELD_TYPE_DATETIME    MYSQL_TYPE_DATETIME  
#define FIELD_TYPE_YEAR        MYSQL_TYPE_YEAR  
#define FIELD_TYPE_NEWDATE     MYSQL_TYPE_NEWDATE  
#define FIELD_TYPE_ENUM        MYSQL_TYPE_ENUM  
#define FIELD_TYPE_SET         MYSQL_TYPE_SET  
#define FIELD_TYPE_TINY_BLOB   MYSQL_TYPE_TINY_BLOB  
#define FIELD_TYPE_MEDIUM_BLOB MYSQL_TYPE_MEDIUM_BLOB  
#define FIELD_TYPE_LONG_BLOB   MYSQL_TYPE_LONG_BLOB  
#define FIELD_TYPE_BLOB        MYSQL_TYPE_BLOB  
#define FIELD_TYPE_VAR_STRING  MYSQL_TYPE_VAR_STRING  
#define FIELD_TYPE_STRING      MYSQL_TYPE_STRING  
#define FIELD_TYPE_CHAR        MYSQL_TYPE_TINY  
#define FIELD_TYPE_INTERVAL    MYSQL_TYPE_ENUM  
#define FIELD_TYPE_GEOMETRY    MYSQL_TYPE_GEOMETRY  
#define FIELD_TYPE_BIT         MYSQL_TYPE_BIT  

날짜가 포함되는 데이터 타입에 대해 코드를 분석해 본 결과 날짜 데이터를 조작하여 저장하는 코드를 찾지 못했다. 꼼꼼하게 분석한 것은 아니기 때문에 혹시 놓친 부분도 있을 거라고 생각한다.

MySQL Server에서 문제를 발견하지 못했다면,다음 봐야 할 것이 MySQL Client와 MySQL Connector/J 소스 코드인데, MySQL Connector/J 에서 이슈와 직/간접적으로 관련된 코드를 발견하였다.

MySQL Connector/J

MySQL Connector/J의 소스 코드에서 /src/com/mysql/jdbc/TimeUtil.java 파일에서 한 가지 이슈를 재연할 수 있는 코드를 발견했다. 메서드 이름  public static Time changeTimezone(…) 메서드를 살펴보면 클라이언트와 서버 간에 TimeZone이 다른 경우 이를 보정하는 연산이 포함된다.

다음은 TimeZone.java의 코드의 일부분이다.

Calendar fromCal = Calendar.getInstance(fromTz);  
fromCal.setTime(tstamp);  

int fromOffset = fromCal.get(Calendar.ZONE_OFFSET)
               + fromCal.get(Calendar.DST_OFFSET);  
Calendar toCal = Calendar.getInstance(toTz);  
toCal.setTime(tstamp);  

int toOffset = toCal.get(Calebndar.ZONE_OFFSET)
              + toCal.get(Calendar.DST_OFFSET);  
int offsetDiff = fromOffset - toOffset;  
long toTime = toCal.getTime().getTime();  

if (rollForward || (conn.isServerTzUTC() && !conn.isClientTzUTC())) {  
    toTime += offsetDiff;  
} else {  
    toTime -= offsetDiff;  
}  

Timestamp changedTimestamp = new Timestamp(toTime);  

만약, 클라이언트와 서버 사이에 0.001ms 시간 차이가 생긴다면, TimeZone은 13이라는 값의 차이가 발생한다. 클라이언트와 서버의 시간 차이 기준은 MySQL Server에서 열린 세션(session)을 기준으로 판단한다. 혹시나 클라이언트와 서버의 TimeZone이 상이하게 설정된 경우 어마 어마한 시간 차이가 생기게 된다.

다음과 같이 필자가 의도적으로 0.001ms 차이가 발생하는 경우 MySQL Connector/J 가 DateTime을 교정하는 것을 확인하였다. (단, getTimeFast 메서드는 MySQL Connector/J 에 포함된 메서드임)

Calendar calendar = Calendar.getInstance();  
calendar.set(1999, 12, 31, 23, 59, 59);  
calendar.setTimeInMillis(nanos);  

System.out.println(getTimeFast(0, bits, 12, 14, calendar, tz, true));  

// output  
2000-01-01 00:00:00.0  

결론

MySQL 날짜 관련 이슈의 직접적인 발생 원인은 찾지 못했다. 하지만 MySQL Connector는 MySQL Client와 쌍으로 움직이니 간접적으로 영향을 미칠 수 있을 것이다.

현재까지 살펴본 바로는 가장 가능성이 있는 이 이슈/버그는 MySQL Connector/J와 MySQL Client일 가능성이 가장 크고, MySQL Server 내부 버그일 가능성은 다소 낮다고 본다. 이 이슈/버그 밑에 코멘트에 MySQL .NET Connector를 사용하는 경우도 문제가 있다고 하니.. 어쨌거나 저쨋거나..

댓글