Backend/Java

오라클 - 마이바티스 날짜형 데이터 맵핑

비비빅B 2023. 7. 12. 20:51

코드를 보다 보니 날짜를 의미하는 데이터를 마이바티스->어플리케이션 맵핑하는 방식이 다양한 것을 봤다. 내 기억으로는 특정 마이바티스 버전부터는 자동으로 [LocalDateTime, LocalDate, LocalTime]으로 맵핑해 주는 걸로 알고 있었는데 활용하지 못한 것처럼 보여 의아했다. 이유를 알아보니 기존 레거시 코드를 그대로 옮기다 보니 그런 것도 있지만, 현재 환경이 위의 Java 타입으로 맵핑이 깔끔하게 되지는 않기 때문이었다.

 

데이트 타입 맵핑 장애물

오라클DB 날짜 타입

먼저 현재 환경은 오라클과 비슷한 국산DB 티베로를 사용하고 있다(이 글에서 말하려는 특징은 동일하기 때문에 오라클로 설명하겠다). 오라클 날짜 형식의 데이터는 4가지로 분류된다.

 

  • DATE
    • 연, 월, 일, 시, 분, 초를 저장
    • 연월일까지만 저장할 경우, 해당 날의 자정의 시간으로 저장
    • 연월까지만 저장할 경우, 해달 월의 1일 자정의 시간으로 저장
  • TIMESTAMP
    • DATE 타입에서 소수점 2자리 초까지 저장
  • TIMESTAMP WITH TIME ZONE
  • TIMESTAMP WITH LOCAL TIME ZONE

 

문제는 시간 없이 날짜만, 혹은 날짜 없이 시간만 저장하는 데이터를 오라클에서는 지원해 주지 않는다. 이런 이유에서 DB팀에서는 날짜 혹은 시간을 저장해야 하는 경우, CHAR(6)로 문자열 데이터('230717', '123054')로 저장하기로 기준을 잡았다. 그러다 보니 데이터 타입 변환이 특정 시점에 필요했고, 대부분 크게 아래의 3가지 경우로 분류할 수 있었다.

  • 날짜를 다루는 로직이 없어서, 그냥 문자열(String)로 받아서 화면에 전달
  • 날짜를 다루는 로직이 있어서, DB에서 Date타입으로 변환 후 Java 날짜 타입으로 맵핑
  • 날짜를 다루는 로직이 있어서, 일단 문자열로 받은 후 자바 라이브러리로 타입 변환

 

마이바티스 타입 핸들러 지원 범위

반복적으로 나타나는 타입 변환 코드는 읽기도 힘들고 지저분하게 만들어서, 프레임워크 영역에서 처리해주고 싶었다. 현재 환경은 3.4.5 버전 이상의 마이바티스를 쓰고 있었기 때문에, Java 날짜 타입 변환을 자동으로 지원해주고 있었다. 그래서 6자리 CHAR를 LocalDate, LocalTime으로 자동 변환시켜주길 기대했지만 그렇지 못했다.

 

yyyyMMdd CHAR(6) -> Java LocalDate로 변환 실패
HHmmss CHAR(6) ->  java LocalTime 변환 실패

 

테스트를 해보니 오라클 DATE 데이터를 Java [LocalDateTime, LocalDate, LocalTime)으로 변환은 성공적으로 됐으나, 오라클 CHAR(6) 데이터를 Java [LocalDate, LocalTime]으로 변환하는데 실패했다.

 

Mybatis Custom TypeHandler

 

기존에 DATE 타입에서 Java 날짜 타입(LocalDateTime, LocalDate, LocalTime)으로 변환은 잘 됐기 때문에, 해당 기능을 유지하면서 추가적인 CHAR(6) 타입을 변환할 기능이 필요했다. 마이바티스에서 자동으로 타입 변환이 가능하도록, CHAR(6)에서 Java 날짜 타입(LocalDate, LocalTime)으로 변환시킬 마이바티스 타입핸들러를 정의했다. 

LocalDateTypeHandler

package com.example.date.typehandler;

public class LocalDateTypeHandler extends BaseTypeHandler<LocalDate> {

  private final DateTimeFormatter localDateFormatter = new DateTimeFormatterBuilder()
    .optionalStart()
    .appendPattern("yyyyMMdd")
    .optionalEnd()
    .optionalStart()
    .appendPattern("yyyy-MM-dd HH:mm:ss")
    .toFormatter();

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, LocalDate parameter, JdbcType jdbcType) throws SQLException {
    ps.setObject(i, parameter);
  }

  @Override
  public LocalDate getNullableResult(ResultSet rs, String columnName) throws SQLException {
    return LocalDate.parse(rs.getString(columnName), localDateFormatter);
  }
 ...
}

LocalTimeTypeHandler

package com.example.date.typehandler;

public class LocalTimeTypeHandler extends BaseTypeHandler<LocalTime> {

  private final DateTimeFormatter localTimeFormatter = new DateTimeFormatterBuilder()
    .optionalStart()
    .appendPattern("HHmmss")
    .optionalEnd()
    .optionalStart()
    .appendPattern("yyyy-MM-dd HH:mm:ss")
    .toFormatter();

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, LocalTime parameter, JdbcType jdbcType) throws SQLException {
    ps.setObject(i, parameter);
  }

  @Override
  public LocalTime getNullableResult(ResultSet rs, String columnName) throws SQLException {
    return LocalTime.parse(rs.getString(columnName), localTimeFormatter);
  }
  ...
}

Mybatis 타입핸들러 설정

mybatis.type-handlers-package= com.example.date.typehandler // 생성한 타입핸들러 패키지 선언

 

LocalDate 타입의 필드로 선언될 경우, DateTimeFormatter이 'yyyyMMdd'와 'yyyy-MM-dd HH:mm:ss' 문자열을 파싱 하도록 작성했다. LocalTime의 경우 'HHmmss'와 'yyyy-MM-dd HH:mm:ss' 형식의 문자열 파싱을 지원한다.

2개의 타입핸들러를 작성 후,  application.properties에 마이바티스 설정을 해주었다.

 

자동 타입 변환 테스트

@Mapper
public interface DateMapper {

  @Select("select sysdate from dual")
  LocalDateTime selectNow();
  @Select("select '20230711' from dual")
  LocalDate selectDate();
  @Select("select '123001' from dual")
  LocalTime selectTime();

  @Select("select sysdate from dual")
  LocalDate selectDateAsLocalDate();
  @Select("select sysdate from dual")
  LocalTime selectDateAsLocalTime();
}
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@MybatisTest
@TestPropertySource(value = "/application-test.properties")
public class DateMappingTest {

  @Autowired
  DateMapper dateMapper;

  @Test
  @DisplayName("CHAR6타입을 LocalDate로 매핑한다.")
  void whenChar6InDBAndLocalDateInJavaType_success() {
    LocalDate localDate = dateMapper.selectDate();
    System.out.println("localDate = " + localDate);
    assertThat(localDate).isInstanceOf(LocalDate.class);
    assertThatNoException();
  }

  @Test
  @DisplayName("CHAR6타입을 LocalTime로 매핑한다.")
  void whenChar6InDBAndLocalTimeInJavaType_success() {
    LocalTime localTime = dateMapper.selectTime();
    System.out.println("localTime = " + localTime);
    assertThat(localTime).isInstanceOf(LocalTime.class);
    assertThatNoException();
  }

  @Test
  @DisplayName("Date타입을 LocalDateTime로 매핑한다.")
  void whenDateInDBAndLocalDateTimeInJavaType_success() {
    LocalDateTime localDateTime = dateMapper.selectNow();
    System.out.println("localDateTime = " + localDateTime);
    assertThat(localDateTime).isInstanceOf(LocalDateTime.class);
    assertThatNoException();
  }

  @Test
  @DisplayName("Date타입을 LocalTime로 매핑한다.")
  void whenDateInDBAndLocalTimeInJavaType_success() {
    LocalDate localDate = dateMapper.selectDateAsLocalDate();
    System.out.println("localDate = " + localDate);
    assertThat(localDate).isInstanceOf(LocalDate.class);
    assertThatNoException();
  }

  @Test
  @DisplayName("Date타입을 LocalDate로 매핑한다.")
  void whenDateInDBAndLocalDateInJavaType_success() {
    LocalTime localTime = dateMapper.selectDateAsLocalTime();
    System.out.println("localTime = " + localTime);
    assertThat(localTime).isInstanceOf(LocalTime.class);
    assertThatNoException();
  }

}

 

  • Date -> LocalDateTime, LocalDate, LocalTime
  • CHAR(6) -> LocaDate, LocalTime

5가지 케이스를 모두 만족하는 것을 볼 수 있다.

 

이 경험을 하기 전까지는 사실 데이터팀에서 날짜, 시간을 CHAR(6) 타입으로 쓰는 이유에 대해서 깊게 생각해보지 않았다. 단순히 AS-IS가 그렇게 돼있어서 그런 줄 알았는데 DB에서 지원하는 날짜 타입이 없어서 그런 것인 줄은 생각 못했다.

예전에 오라클이 null을 빈값과 동일하게 처리한다는 것에 대해서도 MySQL과 달라 당황했던 기억이 새록새록 났다. 이런 데이터베이스 간의 차이점도 잘 기억해 둬야겠다.

'Backend > Java' 카테고리의 다른 글

Spring XSS  (0) 2023.09.10
Spring Actuator로 Property 변경하기  (0) 2023.08.29
Java에서 Null을 다루는 방식  (0) 2023.05.22
공통 lib -> spring-boot-starter로 바꾸기(3)  (0) 2023.04.23
Stream에 Decorator 패턴 써먹기  (2) 2023.04.03