H2 데이터베이스 Unknown data type 트러블슈팅

dev_Hyun·2024년 8월 4일
0
post-thumbnail

서론

H2 데이터베이스는 Java 기반 프로젝트에서 자주 사용되는 경량 임베디드 데이터베이스입니다. 특히 테스트 환경에서 MySQL을 대체하여 사용되곤 합니다. 하지만 MySQL과 H2 사이의 미묘한 차이점으로 인해 예상치 못한 문제에 직면하였습니다. 이 글에서는 MySQL 문법으로 작성된 SQL 쿼리를 H2 데이터베이스에서 실행할 때 발생한 문제와 그 해결 과정을 공유하고자 합니다.

문제 발생

테스트 환경에서 H2 데이터베이스를 사용하여 MySQL용으로 작성된 쿼리를 실행하던 중, 다음과 같은 오류가 발생했습니다.

Caused by: org.h2.jdbc.JdbcSQLNonTransientException: Unknown data type: "?, ?"; SQL statement:

원인 : Prepared Statement 와 Unknown data type 오류

문제의 원인은 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 모드 한계

H2 데이터베이스는 MySQL 호환 모드를 제공하지만, 100% 완벽한 호환성을 보장하지는 않습니다. 이번 사례에서 볼 수 있듯이, 특히 복잡한 쿼리나 특정 MySQL 고유 기능을 사용할 때 예상치 못한 문제가 발생할 수 있습니다.

Spring Data JPA와 같은 ORM을 사용하면 데이터베이스 종속성을 줄일 수 있겠지만, 매퍼방식을 사용할 경우 해결되지 않습니다.

대안으로 TestContainers 사용을 고려해보면 좋을 것 같다는 생각으로 해당 트러블 슈팅을 마무리했습니다. TestContainers 는 테스트 실행 시 Docker 컨테이너를 사용하여 실제 MySQL 인스턴스를 제공하여 테스트의 격리성과 재현성을 높일 수 있다는 장점이 있습니다.

profile
공룡, 다람쥐 그리고 돌고래!

0개의 댓글