H2 데이터베이스는 Java 기반 프로젝트에서 자주 사용되는 경량 임베디드 데이터베이스입니다. 특히 테스트 환경에서 MySQL을 대체하여 사용되곤 합니다. 하지만 MySQL과 H2 사이의 미묘한 차이점으로 인해 예상치 못한 문제에 직면하였습니다. 이 글에서는 MySQL 문법으로 작성된 SQL 쿼리를 H2 데이터베이스에서 실행할 때 발생한 문제와 그 해결 과정을 공유하고자 합니다.
테스트 환경에서 H2 데이터베이스를 사용하여 MySQL용으로 작성된 쿼리를 실행하던 중, 다음과 같은 오류가 발생했습니다.
Caused by: org.h2.jdbc.JdbcSQLNonTransientException: Unknown data type: "?, ?"; SQL statement:
문제의 원인은 H2 데이터베이스가 Prepared Statement를 처리하는 방식에 있었습니다. 사용된 쿼리는 다음과 같았습니다
<insert id="savePossibleDateTimeListWhereNotExist">
INSERT INTO t_possible_datetime (possible_datetime_id, possible_datetime, possible_datetime_status, user_id)
SELECT possibleDateTimeId, possibleDateTime, possibleDateTimeStatus, userId
FROM (
<foreach collection="possibleDateTimeList" item="possibleDateTime" separator=" UNION ALL ">
SELECT #{possibleDateTime.possibleDateTimeId} AS possibleDateTimeId,
#{possibleDateTime.possibleDateTime} AS possibleDateTime,
#{possibleDateTime.possibleDateTimeStatus} AS possibleDateTimeStatus,
#{possibleDateTime.userId} AS userId
</foreach>
) AS new_values
WHERE NOT EXISTS (
SELECT 1
FROM t_possible_datetime
WHERE user_id = new_values.userId
AND possible_datetime = new_values.possibleDateTime
);
</insert>
H2 데이터베이스는 MySQL과 달리 Prepared Statement
를 즉시 컴파일하려고 시도합니다. 이 과정에서 플레이스홀더(?
)의 데이터 타입을 즉시 결정하려고 하는데, 위 쿼리에서는 UNION ALL 구문 내의 값들에 대한 명시적인 데이터 타입이 없어 문제가 발생했습니다.
문제를 해결하기 위해 각 플레이스홀더에 명시적인 캐스팅을 적용했습니다.
H2 데이터베이스에서 CAST 함수는 다음과 같은 문법을 사용합니다
CAST(expression AS data_type)
지원하는 데이터 타입은 해당 공식문서 링크에서 확인할 수 있으며, 제가 사용한 타입은 INT
, TIMESTAMP
, VARCHAR
입니다.
수정된 쿼리는 다음과 같습니다
<insert id="savePossibleDateTimeListWhereNotExist">
INSERT INTO t_possible_datetime (possible_datetime_id, possible_datetime, possible_datetime_status, user_id)
SELECT possibleDateTimeId, possibleDateTime, possibleDateTimeStatus, userId
FROM (
<foreach collection="possibleDateTimeList" item="possibleDateTime" separator=" UNION ALL ">
SELECT CAST(#{possibleDateTime.possibleDateTimeId} AS INT) AS possibleDateTimeId,
CAST(#{possibleDateTime.possibleDateTime} AS TIMESTAMP) AS possibleDateTime,
CAST(#{possibleDateTime.possibleDateTimeStatus} AS VARCHAR) AS possibleDateTimeStatus,
CAST(#{possibleDateTime.userId} AS INT) AS userId
</foreach>
) AS new_values
WHERE NOT EXISTS (
SELECT 1
FROM t_possible_datetime
WHERE user_id = new_values.userId
AND possible_datetime = new_values.possibleDateTime
);
</insert>
이렇게 명시적 캐스팅을 사용함으로써 H2 데이터베이스가 각 플레이스홀더의 데이터 타입을 정확히 인식할 수 있게 되었고, 쿼리가 성공적으로 실행되었습니다.
참고 링크 :
H2 데이터베이스는 MySQL 호환 모드를 제공하지만, 100% 완벽한 호환성을 보장하지는 않습니다. 이번 사례에서 볼 수 있듯이, 특히 복잡한 쿼리나 특정 MySQL 고유 기능을 사용할 때 예상치 못한 문제가 발생할 수 있습니다.
Spring Data JPA와 같은 ORM을 사용하면 데이터베이스 종속성을 줄일 수 있겠지만, 매퍼방식을 사용할 경우 해결되지 않습니다.
대안으로 TestContainers
사용을 고려해보면 좋을 것 같다는 생각으로 해당 트러블 슈팅을 마무리했습니다. TestContainers 는 테스트 실행 시 Docker 컨테이너를 사용하여 실제 MySQL 인스턴스를 제공하여 테스트의 격리성과 재현성을 높일 수 있다는 장점이 있습니다.