LOB 변환은 타입 치환이 아니다
CLOB, BLOB, TEXT 계열 LOB 컬럼 변환 시 저장 구조, SQL, 애플리케이션 영향까지 함께 검토해야 하는 이유를 정리합니다.
이기종 마이그레이션에서 LOB 컬럼이 만드는 문제들
"컬럼 타입 하나 바꾸는 건데, 그게 그렇게 어렵습니까?"
이기종 마이그레이션 프로젝트에서 LOB 컬럼 변환 작업이 시작될 때 종종 듣는 말입니다. CLOB이나 BLOB을 오픈소스 데이터베이스의 TEXT나 BYTEA로 바꾸는 작업을 단순한 타입 치환 정도로 보는 시각은 생각보다 흔합니다. 반대로 오픈소스에서 Oracle로 올 때도 마찬가지입니다. TEXT니까 CLOB으로 받으면 되는 것 아니냐는 질문도 자주 받습니다.
하지만 LOB 변환은 그 순간부터 이야기가 달라집니다.
타입 하나가 바뀌었을 뿐인데
CLOB 컬럼에 = 비교, DISTINCT, JOIN, ORDER BY를 사용하면 Oracle은 ORA-00932를 포함한 다양한 오류를 발생시킵니다. LIKE도 예외가 아닙니다. CLOB 컬럼에 LIKE를 직접 사용하면 ORA-00932 오류가 발생합니다. SUBSTR로 범위를 제한하거나 Oracle Text 인덱스를 사용하는 우회 방법이 필요하며, 어느 쪽이든 인덱스를 활용할 수 없어 성능과 예측 가능성 측면에서 문제가 되기 쉽습니다.
문제는 이런 SQL이 기존 코드 어딘가에 반드시 존재한다는 점입니다. VARCHAR2였을 때는 아무 문제가 없었기 때문입니다. 에러 메시지가 친절하게 알려주면 오히려 다행입니다. 에러도 없이 빈 결과만 돌려주는 경우가 더 찾기 어렵습니다.
그리고 이 문제는 Oracle 안에서만 발생하는 것이 아닙니다. Oracle에서 오픈소스로 가져갈 때, 반대로 오픈소스에서 Oracle로 이행할 때, 방향에 따라 다른 형태의 문제가 기다리고 있습니다.
LOB은 단순히 큰 문자열이 아니다
CLOB을 "크기가 큰 VARCHAR2" 정도로 이해하는 경우가 많습니다. 하지만 데이터베이스 내부에서 LOB은 완전히 다른 존재입니다.
일반 컬럼의 데이터는 행(Row)과 함께 같은 공간에 저장됩니다. 조회하면 그냥 읽히고, 비교하면 그냥 비교됩니다. 반면 LOB은 데이터베이스가 별도의 저장 공간을 만들어 관리합니다. Oracle이라면 LOB Segment와 LOB Index가 자동으로 생성되고, 실제 데이터는 그곳에 따로 보관됩니다. 행에는 그 위치를 가리키는 locator만 남습니다.
이 구조는 단순히 저장 위치만 다른 것이 아닙니다. LOB 데이터는 piece 단위로 관리되며, 접근 시에도 일반 컬럼처럼 한 번에 읽히는 것이 아니라 별도의 접근 경로를 따릅니다. 이 때문에 optimizer는 LOB을 일반 컬럼과 동일하게 취급하지 않으며, 비교·정렬·집계 연산에 제약이 생깁니다.
저장 방식이 다르니 접근 방식도 달라집니다. SQL이 달라지고, 인덱스 전략이 달라지고, 애플리케이션 코드가 달라집니다. 그리고 이 차이는 Oracle에서 오픈소스로 갈 때와, 오픈소스에서 Oracle로 올 때 서로 다른 형태의 문제로 나타납니다.
타입 하나를 바꾸는 것이 아닙니다. 데이터를 다루는 방식 전체가 함께 바뀌는 것입니다.
결국 LOB은 "크기가 큰 문자열"이 아니라, 다른 저장 구조와 다른 접근 방식을 가지는 데이터 타입입니다.
LOB 변환, 어떻게 접근할 것인가
LOB 변환에서 가장 먼저 해야 할 일은 방향을 확인하는 것이 아닙니다. 현재 LOB이 어떻게 쓰이고 있는지를 먼저 보는 것입니다.
어떤 컬럼이 LOB인지, 그 컬럼의 데이터가 실제로 얼마나 큰지, DBMS_LOB을 쓰는 코드가 어디에 얼마나 있는지, LOB 컬럼을 조건절이나 정렬에 사용하는 SQL이 몇 개인지. 이 네 가지 질문에 대한 답이 나오기 전에는 어떤 타입으로 변환할지 결정할 수 없습니다.
이 숫자들이 나오면 판단의 중심은 데이터 크기 분포입니다. 단순 평균이 아니라 어느 구간에 얼마나 몰려 있는가를 봐야 합니다.
이 기준을 실제로 적용하면 다음과 같은 판단이 가능합니다. 전체의 대다수가 4000바이트 이하라면 VARCHAR2를 유지하고 초과 데이터를 별도로 처리하는 것이 낫습니다. 대다수가 초과한다면 그때 CLOB이나 Extended VARCHAR2를 검토합니다. SQL 사용 방식과 애플리케이션 처리 방식은 타입을 결정한 이후에 따라오는 문제입니다. 이 순서가 바뀌면 판단이 흔들립니다.
Oracle에서 오픈소스로 가는 방향이라면 제약이 풀리는 대신 DBMS_LOB 의존 코드를 얼마나 걷어내야 하는지가 핵심 과제가 됩니다. 반대로 오픈소스에서 Oracle로 오는 방향이라면 데이터 크기가 타입 결정의 기준이 되고, 그 결정이 이후 SQL과 애플리케이션 전체의 난이도를 결정합니다.
LOB 변환은 타입 매핑표 한 줄로 끝나는 작업이 아닙니다. 방향별로 전략이 달라야 하는 설계 결정입니다.
Oracle → 오픈소스: 제약이 풀리지만, 숙제가 남는다
Oracle에서 오픈소스로 넘어갈 때 LOB 변환은 방향 자체가 유리합니다. Oracle에서 까다롭던 제약들이 오픈소스에서는 대부분 사라지기 때문입니다. LIKE도 되고, ORDER BY도 되고, 일반 컬럼처럼 다룰 수 있습니다. 그래서 이 방향은 쉽다고 생각하기 쉽습니다.
하지만 제약이 풀린다는 것이 아무 준비 없이 넘어가도 된다는 뜻은 아닙니다. 오히려 그동안 제약을 피하기 위해 작성된 코드들이 의미를 잃는다는 뜻이기도 합니다. DBMS_LOB 기반 코드, Oracle Text 인덱스, 우회 로직들은 오픈소스 환경에서는 그대로 사용할 수 없습니다.
특히 문자열 함수의 인자 순서나 동작 방식 차이는 단순 치환으로 해결되지 않는 경우가 많습니다. DBMS_LOB.SUBSTR과 PostgreSQL SUBSTRING은 인자 순서가 다릅니다. 기계적으로 치환하면 전혀 다른 위치의 문자열을 잘라내게 됩니다. 단순히 함수 이름을 바꾸는 문제가 아닙니다. SQL뿐 아니라 프로시저, 함수, 패키지, 트리거 안에도 이런 코드가 숨어 있습니다.
Oracle Text 인덱스를 쓰고 있었다면 오픈소스의 Full-Text 방식으로 재구성해야 합니다. 기능은 비슷하지만 설계 방식이 다릅니다. 특히 한글 검색이 있다면 MySQL에서는 ngram 파서 설정을 별도로 챙겨야 하고, PostgreSQL이라면 tsvector 구성 방식을 처음부터 다시 생각해야 합니다.
BLOB은 CLOB보다 더 주의해야 합니다. 바이너리 데이터는 데이터베이스마다 표현 방식이 다릅니다. 마이그레이션 도구가 이 부분을 올바르게 처리했는지는 반드시 직접 검증해야 합니다. 데이터 건수가 맞는다고 데이터가 맞는 것이 아닙니다.
이 방향에서 판단의 출발점은 하나입니다. Oracle에서 LOB 제약을 피하기 위해 복잡하게 짜놓은 코드가 어디에 얼마나 있는지를 먼저 파악하는 것입니다. 그 범위가 작다면 이관 자체는 비교적 수월합니다. 그 범위가 크다면 타입 변환보다 코드 정리가 더 큰 과제가 됩니다. 방향이 유리하다고 준비를 줄이면 안 되는 이유가 여기 있습니다.
오픈소스 → Oracle: 타입 결정이 이후 모든 것을 결정한다
방향이 바뀌면 상황도 바뀝니다. 이 방향은 제약이 없던 곳에 제약이 생깁니다. 그리고 그 제약의 크기는 어떤 타입으로 받느냐에 따라 완전히 달라집니다.
선택지는 셋입니다. VARCHAR2, Extended VARCHAR2, CLOB.
VARCHAR2로 받을 수 있다면 받는 것이 가장 낫습니다. Oracle에서 VARCHAR2는 일반 컬럼입니다. 오픈소스 TEXT를 쓰던 방식 그대로 SQL을 쓸 수 있습니다. 문제는 크기입니다. Oracle VARCHAR2는 기본 설정에서 4000바이트까지입니다. 오픈소스에서 그 이상을 담고 있었다면 이야기가 달라집니다.
Extended VARCHAR2는 이 간극을 메우는 선택지처럼 보입니다. 12c부터 MAX_STRING_SIZE = EXTENDED로 설정하면 32767바이트까지 늘어납니다. SQL 호환성은 유지하면서 크기 제약을 넘을 수 있으니 매력적입니다. 하지만 이 선택에는 되돌아올 수 없는 길이라는 전제가 붙습니다. MAX_STRING_SIZE를 EXTENDED로 한 번 올리면 STANDARD로 되돌리는 공식적인 경로는 없습니다. 데이터베이스를 UPGRADE 모드로 올린 상태에서만 변경이 가능하고, utl32k.sql을 돌려야 합니다. 이 스크립트는 시스템 딕셔너리 컬럼들을 VARCHAR2(32767)로 재정의하는 작업으로, 실행 자체가 데이터베이스 전체에 영향을 미칩니다. 운영 환경에서는 사실상 '프로젝트 단위 결정'에 가깝습니다.
내부 저장 방식도 주의해야 합니다. 4000바이트를 초과하는 데이터는 내부적으로 out-of-line LOB 세그먼트에 저장됩니다. 겉으로는 VARCHAR2처럼 보이지만 성능 특성은 CLOB과 사실상 같습니다. GoldenGate는 이 컬럼을 LOB으로 인식해 복제 설정과 Trail 파일 포맷에 영향을 줄 수 있고, DB Link를 통한 원격 조회 시 4000바이트 초과 데이터는 상대 DB의 설정에 따라 오류가 발생할 수 있습니다. 여기에 더해 EXTENDED 설정 후 생성된 32767바이트 컬럼은 DataPump export 시 구버전 Oracle로의 import가 불가합니다. 다운그레이드 경로가 막히는 것은 DB 설정뿐 아니라 데이터 이동 측면에서도 마찬가지입니다. Extended VARCHAR2는 단순히 "길이가 늘었다"가 아니라, 스토리지 구조와 에코시스템 전체의 변화로 보는 것이 맞습니다.
CLOB은 크기 걱정 없이 받을 수 있지만, 오픈소스에서 문제없이 돌아가던 LIKE, ORDER BY, GROUP BY가 전부 재검토 대상이 됩니다.
결국 이 방향의 타입 결정은 데이터를 직접 보는 것에서 시작해야 합니다. 실제 데이터 크기 분포를 확인하지 않고 내린 결정은 추정일 뿐입니다. 전체 데이터의 1%도 안 되는 예외 케이스 때문에 DB 전체 설정을 바꾸는 것이 맞는지, 그 예외를 다른 방식으로 처리하는 것이 나은지는 숫자를 보고 나서 판단해야 합니다.
타입 하나를 잘못 결정하면 그 결정이 SQL 재작성, 인덱스 재설계, 애플리케이션 코드 수정으로 이어집니다. 이 방향에서 타입 결정은 기술 선택이 아니라 프로젝트 범위를 결정하는 일입니다.
애플리케이션까지 영향이 간다
데이터베이스 타입이 바뀌면 그 데이터를 읽고 쓰는 코드도 영향을 받습니다. 그런데 이 영향은 한 곳에서 나타나지 않습니다. SQL, JDBC 코드, ORM 설정, Connection Pool, 배치까지 각자 다른 방식으로 드러납니다.
Oracle CLOB을 JDBC로 읽을 때는 LOB Locator 방식으로 접근합니다. 오픈소스 TEXT는 그냥 문자열입니다. Oracle로 가는 방향이라면 getClob()과 스트림 처리가 필요해지고, 오픈소스로 가는 방향이라면 반대로 코드가 단순해집니다.
Oracle 환경에서는 LOB Locator가 세션에 묶여 있기 때문에 Connection Pool 설정도 함께 검토해야 합니다. LOB Locator를 열어둔 채 커넥션이 반납되면 다음 사용 시점에 Locator가 무효화되어 오류가 발생할 수 있습니다. 간헐적으로 발생하기 때문에 원인을 찾기가 쉽지 않습니다. 오픈소스로 가는 방향이라면 이 제약은 사라지지만, PgBouncer를 Transaction 모드로 사용하는 경우 세션 기반 오브젝트를 다루는 코드가 있다면 별도로 확인이 필요합니다.
ORM에서 놓치기 쉬운 부분이 있습니다. Hibernate라면 @Lob 어노테이션, MyBatis라면 jdbcType=CLOB 명시가 필요합니다. 기존에 VARCHAR2였다가 CLOB으로 바뀐 컬럼은 이 설정이 빠져 있는 경우가 많습니다. 에러가 바로 나면 찾기라도 쉽습니다. 4000바이트 이하 데이터만 있을 때는 멀쩡하다가 그 이상이 들어오는 순간 오류가 발생하기 때문에 발견이 늦어지는 경우가 있습니다.
프로시저와 패키지 안에 있는 코드도 빠뜨리면 안 됩니다. 애플리케이션 코드 리뷰에만 집중하다가 DB 서버 안의 프로시저에서 오류가 발생하는 경우가 있습니다. DBMS_LOB 사용 현황은 DB 안까지 포함해서 전수 조사해야 합니다.
배치와 ETL도 마찬가지입니다. DataPump는 CLOB 컬럼이 있는 테이블을 XML 방식으로 처리해서 속도가 눈에 띄게 느려집니다. MTK로 LOB 테이블을 이관할 때는 batchSize를 줄이지 않으면 메모리 압박이나 중단이 생길 수 있습니다. GoldenGate를 쓴다면 LOB 복제 설정을 별도로 확인해야 합니다.
LOB 컬럼이 바뀌면, 그 컬럼을 다루는 모든 레이어를 다시 봐야 합니다. 데이터베이스 변환이 끝났다고 해서 애플리케이션이 자동으로 따라오지는 않습니다.
마치며 — LOB 변환을 어떻게 시작할 것인가
LOB 변환에서 길을 잃는 경우는 대부분 순서가 잘못된 데서 시작합니다. 타입을 먼저 결정하고, 문제가 생기면 그때 범위를 파악하는 방식으로는 후반부에 감당하기 어려운 수정이 쌓입니다.
현황이 먼저입니다. LOB 컬럼이 몇 개인지, 데이터가 실제로 얼마나 큰지, DBMS_LOB을 쓰는 코드가 어디에 있는지, LOB 컬럼을 조건절에 쓰는 SQL이 얼마나 되는지. 이 숫자들이 나와야 타입을 결정할 수 있고, 코드를 어디서부터 손댈지 판단할 수 있습니다. 그다음 방향에 맞는 전략이 나옵니다. JDBC, ORM, Connection Pool, 배치까지 LOB이 닿아 있는 곳을 빠뜨리지 않고 확인하는 것은 그 이후입니다.
LOB 변환을 어렵게 만드는 것은 기술적 복잡함이 아닙니다. 영향 범위를 보지 않고 시작하는 것입니다. 범위가 먼저 보여야 전략이 나옵니다.
LOB이 가변 크기와 저장 구조의 문제였다면, 다음 글의 주제인 날짜형 — 이름은 같지만 담긴 것이 다른 타입 — 은 의미와 해석의 문제입니다.
