트러블 슈팅
MyBatis 코드를 JPA + QueryDSL로 이관하는 작업을 하고 있습니다. MySQL 컬럼 중 date, datetime을 LocalDate, LocalDateTime으로 매핑하고 개발을 진행하던 중 QueryDSL에서 where(), set() 등에서 LocalDate, LocalDateTime 객체를 파라미터로 넣어서 처리를 하고 있습니다. 문제는 하이버네이트의 바인딩 쿼리를 봤을 때 날짜 형식 문자열이 'MM/dd/yyyy HH:mm:ss'로 표시가 되는 것을 확인했습니다. 이 문자열 형식은 timestamp 컬럼의 경우 정상적으로 동작을 하지만, date, datetime의 경우 처리가 되지 않는 문제가 있습니다. 원하는 형식 문자열은 'yyyy-MM-dd HH:mm:ss' 입니다.
도대체 왜 LocalDate, LocalDateTime이 원치 않은 형식 문자열로 바인딩이 되는지 궁금하여 내부 소스를 확인해 봤습니다. 봐야 할 클래스는 BasicBinder, DateTypeDescriptor 2개 입니다. 우선 BasicBinder를 살펴보면 아래와 같습니다.
value가 null이 아니면 doBind 메서드를 호출하여 바인딩할 파라미터를 만듭니다. 제네릭 타입에 따라 Descriptor가 호출되어 처리되는 것 같습니다. 날짜 데이터는 DateTypeDescriptor에서 처리를 하고 있었습니다. 다음은 소스 코드입니다.
코드를 보면 value를 Date 객체로 매핑하여 PreparedStatement 객체에 setDate로 넣어주고 있었습니다. 이러니 LocalDate, LocalDateTime, @Temporal(value = TemporalType.TIME), @Temporal(value = TemporalType.DATE), @Temporal(value = TemporalType.TIMESTAMP) 등 갖가지 시도를 해봤자 소용이 없던 것이었습니다.
물론 'yyyy-MM-dd HH:mm:ss' 형식으로 날짜 데이터를 바인딩하는 방법은 있습니다. Expressions.stringTemplate, Expressions.dateTimeTemplate 두 가지 메서드를 이용하여 처리가 가능합니다. Expressions는 동적 표현 생성을 위한 팩토리 클래스이며, DSL 형식이 사용될 수 없는 곳에서 사용돼야 합니다. 예를 들면, Dynamic Paths, Custom syntax 또는 Custom operation 등에서 말이죠. 다음은 코드 예시입니다.
final QEntity qEntity = QEntity.entity;
final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
final BooleanBuilder booleanBuilder = new BooleanBuilder();
final String regDateFmStr = inputParams.getRegDate().format(dateFormatter);
booleanBuilder.and(
Expressions.stringTemplate(
"DATE_FORMAT({0}, '%Y-%m-%d')",
qEntity.regDate
)
.eq(regDateFmStr)
)
위의 코드는 booleanBuilder에 reg_date라는 컬럼의 비교식을 추가합니다. 'reg_date = '2024-01-24'' 와 같은 형식으로 처리가 되는데요. Expressions.stringTemplate을 사용할 때 필요한 것은 데이터베이스의 내장함수를 사용하는 표현식과 입력받은 파라미터, 그리고 형식이 변환된 문자열 객체입니다. RegDate 객체는 LocalDate 타입으로 쿼리에서 'yyyy-MM-dd' 형식으로 비교가 돼야 합니다. 입력받은 파라미터에서 regDate를 꺼낸 후 'yyyy-MM-dd' 형식 문자열로 변환합니다. Expressions.stringTemplate의 "DATE_FORMAT({0}, '%Y-%m-%d')"과 qEntity.regDate 를 입력한 것은 reg_date를 '%Y-%m-%d' 형식으로 변환해서 비교하겠다는 의미입니다. 그래서 eq로 비교하면 됩니다.
final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
...
.set(
qEntity.updDate,
Expressions.dateTimeTemplate(
LocalDateTime.class,
"STR_TO_DATE({0}, '%Y-%m-%d %H:%i:%s')",
LocalDateTime.now().format(DATETIME_FORMATTER)
)
)
위의 코드는 Expressions.dateTimeTemplate 을 사용한 예시입니다. 필요한 파라미터는 표현 타입과 템플릿(내장함수 표현식), 사용할 객체입니다. LocalDateTime.class 로 명시한 것은 qEntity.updDate가 LocalDateTime 타입이어서 그렇습니다. 템플릿을 이용해서 'yyyy-MM-dd HH:mm:ss' 형식 문자열을 MySQL의 date 컬럼 데이터로 변환시킵니다.
QueryDSL은 쿼리가 길어지면 가독성이 떨어지는 단점이 있습니다. Expressions는 문제를 해결할 수는 있으나 특정 데이터베이스의 함수를 사용해야 하므로 유연하지 않고, 메서드 호출부에 입력될 데이터들이 많아 코드가 길어지므로 가독성을 저해하는 요소라고 생각이 됩니다. 제가 선호하는 방식은 아니다 보니 다른 방법을 찾게 되었습니다.
Custom으로 AttributeConverter를 구현하여 처리하는 방법이 있습니다. 구현한 Converter는 엔티티 클래스에 지정할 필드를 선정하고 @Convert 어노테이션을 붙여서 처리가 가능합니다. 이 방법이 훨씬 깔끔하고 가독성에도 좋습니다. 코드와 함께 설명을 하겠습니다.
LocalDateTime과 LocalDate 를 대상으로 하는 converter입니다. convertToDatabaseColumn은 데이터베이스 컬럼에 쓰일 데이터를 어떻게 처리할지에 대한 메서드이고, convertToEntityAttribute는 데이터베이스에 데이터를 가져와서 매핑할 때 어떻게 처리할 것인지에 대한 메서드입니다. 원하는 형식 문자열(yyyy-MM-dd, yyyy-MM-dd HH:mm:ss)을 이용해서 처리를 해주었습니다. @Converter 어노테이션의 속성으로 autoApply가 있는데요. 기본적으로 false이기 때문에 전역 설정이 안 된다는 점을 참고해 주세요.
이제 만들어진 @Converter를 어떻게 적용할 지에 대해서 알아보겠습니다.
public class Entity {
@Column
@Convert(converter = CustomDateConverter.class)
private LocalDate localDateTypeField;
@Column
@Convert(converter = CustomDateTimeConverter.class)
private LocalDateTime localDateTimeTypeField;
}
@Convert를 원하는 필드에 붙이고, converter에 구현한 CustomConverter를 추가하면 적용됩니다. 이후 동일한 QueryDSL 메서드를 수행하면 제대로 동작하는 것을 확인할 수 있습니다.
Hibernate는 Date 클래스로 날짜를 바인딩한 이유
날짜 데이터로 고생을 하다 보니 왜 하이버네이트는 Date 클래스로 날짜 데이터를 바인딩할까 라는 궁금증이 생겼습니다. 이유는 호환성, 표준성 그리고 범용적인 데이터 처리를 위한 설계 결정 때문이었습니다. 자세히 알아 보겠습니다.
호환성과 JDBC 표준 준수
Hibernate는 데이터베이스와 Java 애플리케이션 간의 매핑을 책임지는 ORM(객체-관계 매핑) 프레임워크입니다. JDBC는 데이터베이스와 Java 간의 통신 표준으로, 기본적으로 날짜 타입을 java.sql.Date, java.sql.Time, java.sql.Timestamp와 같은 클래스를 통해 처리합니다. JDBC는 날짜와 시간 데이터를 처리할 때 java.sql.* 패키지를 사용하며, 이는 java.util.Date를 확장한 클래스를 기반으로 설계되었습니다. Hibernate는 이러한 JDBC의 표준을 준수하기 위해 Date 클래스 계열(java.util.Date 및 java.sql.Date)을 기본으로 사용합니다.
java.util.Date와 java.sql.Timestamp의 범용성
Hibernate는 다양한 데이터베이스와 호환되도록 설계되었습니다. 데이터베이스에서 사용하는 날짜와 시간 데이터의 종류는 주로 아래와 같습니다.
- DATE: 날짜만 저장 (예: 2024-12-14)
- TIME: 시간만 저장 (예: 14:30:00)
- DATETIME/TIMESTAMP: 날짜와 시간을 함께 저장 (예: 2024-12-14 14:00:00)
Hibernate는 이러한 다양한 타입을 하나의 Java 객체(java.util.Date 또는 java.sql.Timestamp)로 바인딩하여 처리합니다. 이로 인해 개발자는 데이터 타입에 구애받지 않고 동일한 방식으로 작업할 수 있습니다.
Hibernate와 초기 설계 시점 문제
Hibernate의 설계가 시작된 시점(Java 1.4 ~ 1.5 시대)에는 java.util.Date가 날짜와 시간을 표현하는 표준 클래스였습니다. 당시에는 Java에서 날짜와 시간을 처리하는 더 나은 대안(예: java.time 패키지)이 없었기 때문에 java.util.Date와 java.sql.Timestamp가 널리 사용됐습니다. JPA 표준 사양도 이러한 클래스에 기반을 두고 설계되었습니다.
데이터베이스 독립성을 위한 설계
Hibernate는 데이터베이스 독립적으로 동작하도록 설계되었습니다. 즉, 특정 데이터베이스에서 날짜가 DATE 타입으로 저장되고, 다른 데이터베이스에서는 DATETIME 또는 TIMESTAMP 타입으로 저장되더라도, Hibernate는 이를 단일 Java 객체로 처리할 수 있어야 합니다. 이를 위해 Hibernate는 내부적으로 다음과 같은 매핑을 수행합니다.
- DATE: java.sql.Date 또는 java.util.Date로 매핑
- DATETIME/TIMESTAMP: java.sql.Timestamp로 매핑
- TIME: java.sql.Time으로 매핑
Java 8의 java.time API는 데이터베이스 독립성을 위해 설계된 것이 아니며, LocalDateTime, LocalDate 등은 특정 데이터베이스 타입과의 직접적인 호환성을 고려하지 않습니다. Hibernate는 이를 보완하기 위해 내부적으로 java.time을 java.sql.Date 또는 java.sql.Timestamp로 변환해 바인딩합니다.
Java 8 이후의 변화
Java 8에서는 날짜와 시간을 더 직관적이고 명확하게 처리하기 위해 java.time 패키지가 도입되었습니다. 이 패키지는 기존의 java.util.Date와 달리 불변 객체로 설계되었으며, 명확한 API를 제공합니다. 하지만, Hibernate가 JPA 2.2부터 java.time 타입을 지원하게 되었음에도 불구하고, 내부적으로는 여전히 데이터베이스와의 상호 작용 시 java.sql.Timestamp 등을 사용합니다. 이는 기존의 데이터베이스 표준 및 호환성을 유지하면서도 새로운 API와의 통합을 지원하기 위함입니다.
데이터베이스 타입의 제약
일부 데이터베이스는 날짜 및 시간 데이터를 처리할 때 정밀도(밀리초, 나노초)를 제한하거나, 특정 타입(DATE, TIME, DATETIME, TIMESTAMP) 만 지원합니다. Hibernate는 이러한 데이터베이스 제약에 적응하기 위해 다음과 같은 방식을 사용합니다.
- 날짜 데이터를 기본적으로 java.sql.Date로 처리
- 정밀도가 높은 데이터 (TIMESTAMP) 는 java.sql.Timestamp 로 처리
- 필요에 따라 데이터베이스 타입과 Java 타입 간의 변환을 자동으로 수행
http://querydsl.com/static/querydsl/5.0.0/reference/html_single/
gpt
'[개발] 프레임워크 > QueryDSL' 카테고리의 다른 글
[QueryDSL] projection으로 테이블 조인 결과 데이터 매핑하기 (0) | 2024.12.03 |
---|---|
QueryDSL 셋팅 시 발생하는 에러 개선 (4) | 2024.11.14 |