Hello

어플리케이션에서 시간 처리 시 반올림 되는 경우 본문

DB

어플리케이션에서 시간 처리 시 반올림 되는 경우

nari0_0 2023. 9. 5. 15:48
728x90

MySql 에서 소수초를 사용하지 않는 시간 데이터 타입의 경우 LocalTime.MAX 시간으로 데이터 생성 시 다음날 00:00:00 로 저장되는 이슈가 있었고 초 단위 이하 값을 다루지 않는 데이터였기에 23:59:59 로 저장하도록 수정 대응한적이 있습니다. ex ) 2023-09-05 23:59:59.999... -> 2023-09-06 00:00:00

 

또한, LocalDateTime.now()를 사용하는 경우 현재 날짜 시간을 생성하기 때문에 소수초가 반올림 될 수도 있습니다.

ex) 2023-09-05 13:13:45.584668789 -> 아래 이미지

소수 초를 쓰는 데이터 타입의 경우 값에 따라 소수 초단위 or 초단위로 반올림 될 수 있고,

소수 초를 쓰지 않는 데이터 타입의 경우 초 단위로 반올림 될 수 있습니다.

datetime(0) vs datetime(6)

 

추후, 조회 시 초 단위 미만에 대한 처리가 필요한 경우 23:59:59 ~ 00:00:00 사이의 데이터가 누락될 수 있는 문제가 있습니다.

이를 어플리케이션에서 대응 할 수 있는 방법을 정리하고자 합니다.

- 사용 기술 스택

  • java 8
  • spring boot 2
  • mysql 5.7

 

MySql Connector/J가 데이터 바인딩 시 초 단위 미만에 대한 처리를 합니다. 아래 과정을 살펴보겠습니다.

반올림 처리되는 과정

PreparedStatement.setTimestamp()
public void setTimestamp(int parameterIndex, Timestamp x) throws java.sql.SQLException {
    synchronized (checkClosed().getConnectionMutex()) {
        int fractLen = -1;
        if (!this.sendFractionalSeconds || !this.serverSupportsFracSecs) {
            fractLen = 0;
        } else if (this.parameterMetaData != null && parameterIndex <= this.parameterMetaData.metadata.fields.length && parameterIndex >= 0) {
            fractLen = this.parameterMetaData.metadata.getField(parameterIndex).getDecimals();
        }

        setTimestampInternal(parameterIndex, x, null, this.connection.getDefaultTimeZone(), false, fractLen,
                this.connection.getUseSSPSCompatibleTimezoneShift());
    }
}
  • fracLen : 소수 초 길이
  • sendFractionalSeconds or serverSupportsFracSecs 값이 false인 경우 소수 초를 사용하지 않도록 ( fracLen  = 0 )
  • this.setTimestampInternal() 호출

PreparedStatement.setTimestampInternal() 일부

protected void setTimestampInternal(int parameterIndex, Timestamp x, Calendar targetCalendar, TimeZone tz, boolean rollForward, int fractionalLength,
        boolean useSSPSCompatibleTimezoneShift) throws SQLException {
    if (x == null) {
        setNull(parameterIndex, java.sql.Types.TIMESTAMP);
    } else {
        checkClosed();

        x = (Timestamp) x.clone();

        if (!this.serverSupportsFracSecs || !this.sendFractionalSeconds && fractionalLength == 0) {
            x = TimeUtil.truncateFractionalSeconds(x);
        }

        if (fractionalLength < 0) {
            // default to 6 fractional positions
            fractionalLength = 6;
        }

        x = TimeUtil.adjustTimestampNanosPrecision(x, fractionalLength, !this.connection.isServerTruncatesFracSecs());

...

                        if (fractionalLength > 0) {
                            int nanos = x.getNanos();

                            if (nanos != 0) {
                                buf.append('.');
                                buf.append(TimeUtil.formatNanos(nanos, this.serverSupportsFracSecs, fractionalLength));
                            }
                        }
  • fractionalLength (=fractLen) 이 조건에 따라 MySql이 지원하는 소수 초 최대 길이인 6으로 설정합니다.
  • 반올림을 처리하는 TimeUtil.adjustTimestampNanosPrecision()호출합니다.

TimeUtil.adjustTimestampNanosPrecision()

 
  1. fsp = fractionalLength(6) 범위가 올바른지 확인하는 조건을 통과합니다.
  2. tail 계산 Math.pow(10, 3) = 10^3 = 1000.0
  3. serverRoundFracSecs는 메소드 호출 시 !this.connection.isServerTruncatesFracSecs() 를 넘겨 받는다.
    1. serverTruncatesFracSecs 기본값은 false 이나, !를 붙여 true를 넘긴다.
  4. nanos = Math.round(nanos/1000.0) * 1000 = 1000000000
  5. nanos %= 1000000000 = 0
  6. res.getTime() + 1000 = 기존 시간 + 1초
  7. setNanos(nanos) 0 넣어줌

 

소수 초 생략 

초 단위 미만에 대한 처리 과정을 살펴 보았는데요. 초 단위 미만에 대한 값을 생략 하도록 설정하는 방법도 있습니다.

sendFractionalSeconds = false

5.x 버전에서는 sendFractionalSeconds 를 false로 작성하는 것입니다.

  • 해당 설정은 전역적으로 적용되므로 선택적 사용이 불가능
  • 초 단위 이하는 버림으로 반올림 문제는 생기지 않음
  • 쿼리를 날릴때, 파라미터에 datetime 관련 타입의 fractional seconds를 사용할 수 없습니다.
url:jdbc:mysql:// ~ ?sendFractionalSeconds=false

MySql Connector/j 에 설정되는 datetime type config properties 중 소수 초 관련 옵션

5.x sendFractionalSeconds "false"로 설정하면 데이터를 서버에 보내기 전에 소수 초가 항상 잘립니다. 
default : true
8.x sendFractionalSecondsForTime "false"로 설정하면 JDBC 사양에서 요구하는 대로 'java.sql.Time'의 소수 초가 무시됩니다. "true"로 설정하면 해당 값은 MySQL TIME 열에 밀리초를 저장할 수 있도록 소수 초로 렌더링됩니다.
default : true

위 설정을 한 후 코드를 다시 살펴보겠습니다.

소수 초를 버릴 수 있도록 fractLen = 0 처리

TimeUtil.truncateFractionalSeconds 내부를 보면 setNanos(0) 강제로 0으로 셋팅해주는 것을 확인할 수 있습니다.

    public static Timestamp truncateFractionalSeconds(Timestamp timestamp) {
        Timestamp truncatedTimestamp = new Timestamp(timestamp.getTime());
        truncatedTimestamp.setNanos(0);
        return truncatedTimestamp;
    }
 
 

결론 : 필요한 경우에 따라 고민을 해보아야할 것 같다.

  • 초 단위 미만이 필요하지 않은 경우
    • DB에서 fractional seconds를 사용하는 곳이 없다면, false 설정을 통해 날짜 반올림 때문에 발생하는 문제도 해결할 수 있습니다. 
    • sendFractionalSeconds=false
  • 초 단위 미만의 처리가 필요한 경우
    • 데이터가 초 단위 미만으로 쌓이는 경우 조회하기위해 아래와 같이 사용이 필요하다.
    • LocalTime.of(23, 59, 59, 999_999) 

 

참고 : https://dev.mysql.com/doc/connector-j/8.1/en/connector-j-reference-configuration-properties.htmlhttps://dev.mysql.com/doc/connector-j/8.1/en/connector-j-connp-props-datetime-types-processing.html#cj-conn-prop_sendFractionalSeconds

728x90