날짜형 데이터, 타입보다 데이터와 SQL을 먼저 보아야 합니다
날짜형 타입 변환에서 타입 매핑보다 먼저 살펴야 할 데이터 값과 SQL의 함정을 정리합니다.
데이터 타입 변환에서 문자형과 숫자형은 그나마 직관적입니다. VARCHAR2가 TEXT로, NUMBER가 NUMERIC으로 바뀌는 것은 이름이 달라도 의미가 크게 다르지 않습니다. 그래서 매핑표 한 줄로 정리하고 넘어가는 경우가 많습니다.
날짜형은 다릅니다. DATE라는 이름은 세 DB 모두에 있습니다. 그런데 Oracle의 DATE는 시간까지 담고, PostgreSQL의 DATE는 날짜만 담습니다. 그리고 이 차이는 매핑 시점에 오류를 내지 않습니다. 데이터가 들어간 것처럼 보이고, 이관은 완료됩니다. 문제는 나중에, 그것도 예상하지 못한 방식으로 드러납니다.
날짜형 변환이 어려운 이유는 기술적 복잡함이 아닙니다. 틀려도 바로 티가 나지 않는다는 점입니다.
이름이 같아도 담긴 것이 다르다
날짜형 이관은 저장의 문제가 아니라 해석의 문제입니다. 이 해석을 검증하지 않으면, 데이터는 결국 틀어집니다.
날짜형 변환에서 가장 자주 발생하는 실수는 대부분 같은 곳에서 출발합니다. Oracle의 DATE를 PostgreSQL의 DATE로, MySQL의 DATETIME을 Oracle의 DATE로 그대로 매핑하는 것입니다. 이름이 같으니 같은 것이라는 가정입니다.
Oracle의 DATE는 년/월/일/시/분/초를 모두 담는 7바이트 타입입니다. 반면 PostgreSQL의 DATE는 날짜만 저장합니다. 시간 정보는 아예 담을 수 없습니다. Oracle DATE를 PostgreSQL DATE로 매핑하는 순간, 수십 년간 쌓인 이력 데이터의 시간 정보가 전부 사라집니다. 오류도 없이. 같은 날 발생한 거래, 변경, 처리 이력은 모두 자정으로 수렴하고, 그 안에서의 순서는 복원할 수 없습니다. 데이터는 들어간 것처럼 보이지만 이미 일부가 사라진 상태입니다.
문제는 이 판단을 타입 이름에서 시작하는 데 있습니다. 실제로 그 컬럼에 시간 데이터가 들어 있는지, 타임존에 의존하고 있는지를 먼저 봐야 합니다. 컬럼이 날짜만 저장하는 용도였다면 TIMESTAMP(0)으로 받을 이유가 없고, 시간까지 쓰고 있었다면 DATE로 받을 수 없습니다. 타입 매핑표는 그 확인이 끝난 뒤에 만들어야 합니다.
타임존은 이관 후에 드러난다
타임존 문제가 까다로운 이유는 이관 직후에는 정상처럼 보인다는 점입니다. 같은 타임존 환경에서 테스트하기 때문입니다. 문제는 글로벌 사용자가 접근하거나, 시간 비교 로직이 실제로 동작하는 시점에 드러납니다. 서울과 뉴욕의 사용자가 같은 레코드를 서로 다른 시간으로 읽는 상황이 그렇게 만들어집니다.
Oracle의 두 타임존 타입, 이름보다 동작을 먼저 보자
감사나 규제 대응 시스템에서는 "언제"만큼이나 "어디서"가 중요합니다. 어느 타임존에서 입력된 데이터인지가 의미를 갖는 경우입니다. Oracle이 타임존을 다루는 두 가지 TIMESTAMP 타입을 제공하는 이유이기도 합니다.
TIMESTAMP WITH TIME ZONE은 원본 오프셋을 그대로 보존합니다. 서울에서 입력한 데이터는 +09:00 정보와 함께 저장됩니다. TIMESTAMP WITH LOCAL TIME ZONE은 다릅니다. DB 서버 타임존 기준으로 변환하여 저장하고, 조회 시에는 세션 타임존으로 되돌려 보여줍니다. 입력 당시의 오프셋 정보는 저장되지 않습니다. 같은 타임존 환경에서는 두 타입이 동일하게 동작하는 것처럼 보입니다. 차이는 타임존이 달라지는 순간 드러납니다.
이 두 타입을 구분하지 않고 매핑하면 이관 후 단일 타임존 환경에서의 테스트는 통과합니다. 문제는 감사 로그를 검토하거나 규제 요건을 충족해야 하는 시점에 드러납니다. 입력 타임존 정보가 사라진 뒤에는 되돌릴 방법이 없습니다. 이런 시스템이라면 타입 매핑만으로는 부족하고, 별도 컬럼으로 타임존 정보를 보존하는 설계를 이관 전에 결정해야 합니다.
타임존 의존 컬럼이 실제로 존재하는지, 그 컬럼이 어떤 맥락에서 입력되고 조회되는지를 먼저 파악해야 하는 이유가 여기 있습니다. 그 파악 없이 내린 타입 결정은 추정입니다.
Oracle과 PostgreSQL, 대응 타입이 있다고 안심하면 안 된다
Oracle에서 PostgreSQL로 가는 방향에서 TIMESTAMP WITH TIME ZONE의 대응 타입은 TIMESTAMPTZ처럼 보입니다. 그런데 동작 방식이 다릅니다. Oracle TIMESTAMP WITH TIME ZONE은 원본 오프셋을 그대로 보존합니다. 서울에서 입력한 2024-03-23 14:30:00 +09:00은 그 형태 그대로 저장됩니다. 반면 PostgreSQL TIMESTAMPTZ는 내부적으로 UTC로 변환해서 저장하고 조회 시 세션 타임존으로 되돌려 보여줍니다. 저장된 시점은 같지만 원본이 어느 타임존에서 입력됐는지는 더 이상 알 수 없습니다. 감사 목적으로 입력 타임존을 추적하는 시스템이라면 이 차이가 이관 후에 설계 문제로 돌아옵니다.
TIMESTAMP WITH LOCAL TIME ZONE은 더 주의가 필요합니다. PostgreSQL에는 정확히 대응하는 타입이 없습니다. TIMESTAMPTZ로 받으면 유사하게 동작하는 것처럼 보이지만, 소스 Oracle DB의 서버 타임존이 UTC가 아닌 경우 시간이 틀어집니다. 예를 들어 Oracle 서버 타임존이 Asia/Seoul로 설정되어 있고 TIMESTAMP WITH LOCAL TIME ZONE 컬럼에 2024-03-23 14:30:00이 저장되어 있다면, 이 값은 내부적으로 KST 기준입니다. 이것을 TIMESTAMPTZ로 받으면 PostgreSQL은 이 값을 UTC로 해석해서 저장하고, 결과적으로 2024-03-23 23:30:00 UTC가 됩니다. 원본과 9시간이 틀어진 상태입니다. 이관 전에 소스 DB의 서버 타임존 설정을 반드시 확인해야 하는 이유입니다.
역방향인 PostgreSQL에서 Oracle로 오는 경우는 반대 문제가 생깁니다. PostgreSQL TIMESTAMPTZ 컬럼은 내부적으로 UTC로 저장되어 있습니다. 이 데이터를 Oracle로 가져올 때 받는 타입을 DATE나 TIMESTAMP로 결정하면 UTC 값이 타임존 변환 없이 그대로 들어갑니다. KST 기준 2024-03-23 14:30:00이었던 데이터가 Oracle에서는 2024-03-23 05:30:00으로 저장됩니다. 이관은 완료됐고 오류도 없습니다. 시간만 9시간 틀어진 상태입니다. Oracle 쪽 타입을 TIMESTAMP WITH TIME ZONE으로 설계하는 것이 UTC 기반 데이터를 올바르게 받을 수 있는 방법입니다.
MySQL은 타임존을 온전히 담을 타입이 없다
MySQL에는 TIMESTAMP와 DATETIME 두 가지 날짜형 타입이 있지만, 둘 다 온전한 답이 아닙니다.
MySQL TIMESTAMP는 입력값을 UTC로 변환해서 저장하고 조회 시 세션 타임존으로 되돌려 보여줍니다. 타임존을 인식하는 것처럼 보이지만, 저장 가능한 범위가 1970-01-01부터 2038-01-19까지로 제한됩니다. Unix Timestamp 기반 32비트 정수의 한계입니다. DATETIME은 범위 문제는 없지만 타임존 정보를 전혀 갖지 않습니다.
Oracle에서 MySQL로 가는 방향이라면 TIMESTAMP WITH TIME ZONE 데이터를 받을 마땅한 타입이 없습니다. DATETIME으로 받으면 타임존 정보가 소실되고, 이 선택이 적절한지는 실제 데이터가 타임존에 의존하고 있는지를 확인한 뒤에야 판단할 수 있습니다.
역방향인 MySQL에서 Oracle로 오는 경우는 다른 문제가 생깁니다. MySQL TIMESTAMP 컬럼은 내부적으로 UTC로 저장되어 있습니다. 이 데이터를 Oracle로 가져올 때 받는 타입을 DATE나 TIMESTAMP로 결정하면 UTC 값이 타임존 변환 없이 그대로 들어갑니다. KST 기준으로 2024-03-23 14:30:00이었던 데이터가 Oracle에서는 2024-03-23 05:30:00으로 저장되는 것입니다. 이관은 완료됐고 오류도 없지만, 시간이 9시간 틀어진 상태입니다. 이 경우 Oracle 쪽 타입을 TIMESTAMP WITH TIME ZONE으로 설계해야 UTC 기반 데이터를 올바르게 받을 수 있습니다. 그런데 현장에서 이 판단을 사전에 하는 경우는 많지 않습니다. MySQL TIMESTAMP가 UTC로 저장된다는 사실 자체를 인지하지 못한 채 이관을 진행하기 때문입니다.
이관 도구는 타입 이름을 먼저 본다
날짜 타입과 타임존 처리는 이관 도구에 따라 결과가 달라집니다. UTC 오프셋을 반영해서 올바르게 변환해주는 도구가 있는 반면, UTC 값을 그대로 밀어 넣는 도구도 있습니다. 도구가 다르면 같은 소스 데이터를 이관했는데 결과가 다르게 나올 수 있다는 뜻입니다.
문제는 AWS DMS를 포함한 이관 도구 대부분이 타입 이름 기준으로 매핑을 결정한다는 점입니다. 컬럼에 실제로 어떤 데이터가 담겨 있는지, 타임존 변환이 필요한 컬럼인지는 고려하지 않습니다. 그 판단은 도구가 아니라 사람의 책임입니다. 도구가 실제로 어떻게 처리하는지는 직접 확인한 뒤에야 알 수 있습니다. 도구를 믿고 넘어갔다가 이관 완료 후 시간 데이터가 9시간씩 틀어진 상태를 발견하는 일이 생기는 이유입니다.
타임존만이 아니라 정밀도도 도구가 판단하지 않는 영역입니다. Oracle TIMESTAMP(9)는 나노초까지 저장하지만 MySQL DATETIME(6)은 마이크로초가 최대입니다. 나노초 단위 데이터가 실제로 존재하는지, 비즈니스적으로 의미가 있는지 확인하지 않고 타입만 바꾸면 데이터가 잘릴 수 있습니다.
타입을 결정하기 전에 SQL 영향도를 먼저 파악한다
타입 결정은 시작이 아니라 확인의 결과입니다. 어떤 타입을 선택하느냐에 따라 SQL 수정 범위가 달라지고, 그 범위는 결정 전에만 파악할 수 있습니다. 결정과 확인의 순서가 바뀌면, 타입은 정했지만 그 결정이 얼마나 많은 SQL 수정으로 이어지는지는 나중에야 알게 됩니다.
Oracle에서 TRUNC(date_col) = TRUNC(SYSDATE)로 날짜를 비교하던 SQL은 PostgreSQL TIMESTAMP(0) 환경에서 DATE_TRUNC('day', date_col) = DATE_TRUNC('day', NOW())로 다시 써야 합니다. 함수 이름만 바뀌는 것이 아닙니다. 인자 순서와 동작 방식이 달라집니다. SYSDATE를 NOW()로 바꾸는 것도 단순하지 않습니다. NOW()는 트랜잭션 시작 시점, CLOCK_TIMESTAMP()는 호출 순간 — 어느 쪽이 맞는지는 해당 로직이 시간을 어떻게 쓰는지에 달려 있습니다.
날짜 컬럼을 조건절에 쓰는 SQL이 몇 개인지, 날짜 함수를 어떤 방식으로 사용하고 있는지를 타입 결정 전에 파악하지 않으면 타입 결정의 파급력을 모른 채 결정하는 것입니다. 이 순서가 뒤집히면 후반부에 예상보다 훨씬 넓은 수정 범위를 마주하게 됩니다.
프로시저와 패키지 안에 숨어 있는 날짜 처리 로직도 빠뜨리면 안 됩니다. 애플리케이션 SQL만 검토하다가 DB 서버 안의 프로시저에서 날짜 함수 호환성 문제가 뒤늦게 드러나는 경우가 있습니다. 날짜 함수 사용 현황은 애플리케이션과 DB 양쪽을 함께 봐야 전체 그림이 나옵니다.
타입을 결정하기 전에 데이터와 SQL을 먼저 보아야 한다
어떤 컬럼에 시간 데이터가 실제로 들어 있는지, 타임존에 의존하는 컬럼이 어디에 있는지, 그 컬럼을 다루는 SQL이 얼마나 되는지, 날짜 함수가 어떤 방식으로 쓰이고 있는지. 이 질문들에 대한 답이 나오기 전에 타입을 결정하면 그 결정은 추정입니다. 추정이 맞을 때도 있지만, 틀렸을 때 수정해야 하는 범위는 타입 하나로 끝나지 않습니다. 그 타입을 참조하는 SQL 전체로 번집니다.
결국 세 가지 질문으로 수렴됩니다.
첫째, 이 컬럼에 실제로 시간 데이터가 존재하는가. 둘째, 이 값은 특정 타임존에 의존하는 데이터인가. 셋째, 이 컬럼을 사용하는 SQL은 어떤 방식으로 작성되어 있는가.
날짜형은 숫자형보다 작고 단순해 보입니다. 그래서 더 쉽게 넘어갑니다. 하지만 이기종 DB 간에 날짜를 저장하고 해석하는 방식은 생각보다 많이 다르고, 그 차이는 데이터와 SQL을 직접 확인한 사람만 알 수 있습니다.
날짜형이 데이터와 SQL을 같이 봐야 했다면, 다음 글의 주제인 숫자형 — 가장 단순해 보여서 가장 위험한 영역 — 도 같은 시선을 요구합니다.
