본문 바로가기

전체보기/Spring

[Spring Boot] DateTime 다뤄보기(3) - Database에 저장하기

이번 글에서는 DateTime 데이터를

Database에 저장하는 과정에 대해 다뤄보겠습니다.

 

예제 코드는 다음 환경에서 실행하였습니다.

  • spring boot starter data jpa:2.4.5
  • MySQL 8.0.22

 

가장 먼저 DB 설정과 DB Schema를 살펴보겠습니다.

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/example?serverTimezone=UTC&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=mysqlpassword

 

위의 설정 중 serverTimezone을 보시면 UTC로 설정되어 있어 있습니다.

 

CREATE TABLE `purchase` (
  `id` decimal(19,2) NOT NULL,
  `amount` int DEFAULT NULL,
  `created_date_time` datetime(6) DEFAULT NULL,
  `purchase_date_time` datetime(6) DEFAULT NULL,
  `player_id` int DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `FK56apcx95fvl79c8240ew525fi` (`player_id`),
  CONSTRAINT `FK56apcx95fvl79c8240ew525fi` FOREIGN KEY (`player_id`) REFERENCES `player` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

 

다음은 Schema를 살펴보겠습니다.

(참고로 해당 schema는 JPA를 통해 자동으로 생성한 것으로 이를 생성한 entity는 다음과 같습니다.)

 

public class Purchase {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private BigInteger id;

    @ManyToOne
    @JoinColumn(referencedColumnName = "id")
    private Player player;

    @Column
    private int amount;

    @Column
    private OffsetDateTime purchaseDateTime;

    @Column
    @CreatedDate
    private LocalDateTime createdDateTime;
}

 

OffsetDateTime과 LocalDateTime 모두 datetime type으로 생성한 것을 알 수 있습니다.

그럼 여기서 서로 다른 offset 값을 전달하면 이를 어떻게 저장할까요?

 

//...

OffsetDateTime kstDateTime = OffsetDateTime.parse("2021-06-06T22:00:00+09:00");
OffsetDateTime utcDateTime = OffsetDateTime.parse("2021-06-06T22:00:00+00:00");

Purchase kstPurchase = new Purchase(player.get(), purchase.getAmount(), kstDateTime);
Purchase utcPurchase = new Purchase(player.get(), purchase.getAmount(), utcDateTime);

Purchase kstResult = purchaseRepository.save(kstPurchase);
Purchase utcResult = purchaseRepository.save(utcPurchase);

//...

 

위 코드에서는 서로 다른 offset을 가지는 두 시간을

결제 시간(purchaseDateTime)으로 설정하여 database에 저장하였습니다.

 

결과는 다음과 같습니다.

 

mysql> select * from purchase;
+-----+--------+----------------------------+----------------------------+-----------+
| id  | amount | created_date_time          | purchase_date_time         | player_id |
+-----+--------+----------------------------+----------------------------+-----------+
| 2   |   1000 | 2021-06-08 11:08:58.801927 | 2021-06-06 13:00:00.000000 |         1 |
| 3   |   1000 | 2021-06-08 11:08:58.827928 | 2021-06-06 22:00:00.000000 |         1 |
+-----+--------+----------------------------+----------------------------+-----------+
2 rows in set (0.00 sec)

 

2021-06-06T22:00:00+09:00을 UTC(offset +00:00) 시간인

2021-06-06T13:00:00 으로 치환하여 저장한 것을 알 수 있습니다.

 

위와 같이 database에 저장된 이유는 위에서 살펴본,

 

spring.datasource.url=jdbc:mysql://localhost:3306/example?serverTimezone=UTC&characterEncoding=UTF-8

 

serverTimezone을 UTC로 설정했기 때문입니다.

그렇다면 해당 내용 검증을 위해서

이번엔 serverTimezone Asia/Seoul로 설정하고 동일한 로직을 수행해 보겠습니다.

 

spring.datasource.url=jdbc:mysql://localhost:3306/jpa?serverTimezone=Asia/Seoul&characterEncoding=UTF-8

 

+-----+--------+----------------------------+----------------------------+-----------+
| id  | amount | created_date_time          | purchase_date_time         | player_id |
+-----+--------+----------------------------+----------------------------+-----------+
| 4   |   1000 | 2021-06-24 21:09:57.501449 | 2021-06-06 22:00:00.000000 |         1 |
| 5   |   1000 | 2021-06-24 21:09:57.593425 | 2021-06-07 07:00:00.000000 |         1 |
+-----+--------+----------------------------+----------------------------+-----------+

 

우리가 예상한대로 위의 예제와는 반대로 UTC 시간인 2021-06-06T22:00:00+00:00 

KST(offset +09:00)인 2021-06-07T07:00:00 으로 변경한 것을 확인할 수 있습니다.

 

그럼 hibernate에서는 어떻게 datetime 값을 변환하고 있을까요?

코드를 살펴보겠습니다.

 

//AbstractEntityPersister::dehydrate
//...
for ( int i = 0; i < entityMetamodel.getPropertySpan(); i++ ) {
	if ( includeProperty[i] && isPropertyOfTable( i, j ) && !lobProperties.contains( i ) ) {
		getPropertyTypes()[i].nullSafeSet( ps, fields[i], index, includeColumns[i], session );
		index += ArrayHelper.countTrue( includeColumns[i] ); 
	}
}
//...

 

dehydrate 메소드는 우리가 전달해준 값을 이용해 database에 저장할 수 있는 쿼리를 완성합니다.

(왜 메소드 이름이 dehydrate인지는 모르겠네요. 혹시 아시는 분이 계시다면 댓글 부탁드립니다!)

 

위 코드의 세번째 줄을 보면, 먼저 각 property의 타입을 가져오는데,

해당 타입에는 SqlTypeDescriptor, JavaTypeDescriptor 정보가 포함되어 있습니다.

 

우리가 관심있게 보고있는 날짜 데이터, OffsetDateTimeType의 경우,

 

public OffsetDateTimeType() {
	super( TimestampTypeDescriptor.INSTANCE, OffsetDateTimeJavaDescriptor.INSTANCE );
}

 

위와 같이,

  • TimestampTypeDescriptor(SqlTypeDescriptor)
  • OffsetDateTimeJavaDescriptor(JavaTypeDescriptor)

로 각각 정의 되어 있습니다.

 

이 SqlTypeDescriptor, JavaTypeDescriptor 하는 역할을 조금 더 자세히 살펴보면,

JavaTypeDescriptor 타입에 해당 하는 값을 변환해주고,

SqlTypeDescriptor는 위에서 변환된 값을 쿼리에 적용합니다.

 

//TimestampTypeDescriptor::getBinder::
return new BasicBinder<X>( javaTypeDescriptor, this ) {
	@Override
	protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException {
		final Timestamp timestamp = javaTypeDescriptor.unwrap( value, Timestamp.class, options );
		if ( value instanceof Calendar ) {
			st.setTimestamp( index, timestamp, (Calendar) value );
		}
		else if ( options.getJdbcTimeZone() != null ) {
			st.setTimestamp( index, timestamp, Calendar.getInstance( options.getJdbcTimeZone() ) );
		}
		else {
			st.setTimestamp( index, timestamp );
		}
	}
	//...

 

그럼 여기까지 DateTime이 저장되는 과정에 대한 글이었습니다.

반응형