대용량 LOB 데이터를 OOM 없이 비교하기
📘SyncChecker
운영에서 비교 성능 이슈를 만나면 보통 SQL부터 봅니다.
인덱스가 빠졌는지, 조인이 무거운지, 정렬이 과한지부터 의심합니다.
그런데 이번 건은 그쪽이 본질이 아니었습니다.
문제는 SQL 문장보다, Oracle의 LOB 타입을 어떤 방식으로 읽고 비교할 것인가에 있었습니다.
일반 컬럼 비교는 row hash 방식이 잘 먹힙니다.
문자열, 숫자, 날짜 컬럼을 일정한 규칙으로 정규화한 뒤 DB에서 hash를 계산해서 비교하면 읽는 양도 줄고 비교 비용도 낮출 수 있습니다.
하지만 LONG, LONG RAW, CLOB, NCLOB, BLOB가 들어오면 이야기가 달라집니다.
여기서는 기존 방식이 그대로 성립하지 않습니다.
핵심은 두 가지였습니다.
- Oracle에서 LOB 타입을 일반 컬럼처럼 기존 hash 경로에 그대로 태우기 어렵다
- 그렇다고 값을 통째로 들고 와서 비교하면 메모리 사용량이 커지고, 대량 처리에서는 OOM 위험이 생긴다
즉, 이번 개선은 “해시를 더 빠르게 만들었다”는 이야기가 아닙니다.
LOB는 다른 데이터다.같은 비교 방식으로 밀어붙이지 말고, 메모리와 타입 제약에 맞는 경로로 따로 처리해야 한다.
이게 핵심입니다.
문제: 비교는 가능했지만, 운영에서 버틸 수 있는 방식이 아니었다
LOB 비교가 아예 불가능한 건 아닙니다.
아주 단순하게 생각하면 이렇게 갈 수도 있습니다.
- source에서 LOB 값을 읽는다
- target에서 LOB 값을 읽는다
- 둘을 그대로 비교한다
기능만 놓고 보면 맞는 접근입니다.
하지만 운영에서는 이 방식이 곧바로 문제로 이어집니다.
LOB는 크기가 큽니다.
한 건만 보면 별일 아닐 수 있습니다. 하지만 비교 시스템은 보통 한 건만 처리하지 않습니다.
여러 row를 한 번에 읽고, 여러 비교 작업이 동시에 돌고, 중간 결과까지 메모리에 쌓이기 시작하면 문제는 금방 커집니다.
즉,
- 작은 테스트에서는 동작한다
- 하지만 운영 데이터량에서는 메모리 사용량이 급격히 커진다
- 결국 OOM 위험이 생긴다
이런 흐름으로 무너집니다.
여기에 Oracle LOB 타입 특유의 제약까지 붙습니다.
즉 이번 문제의 본질은 단순 성능 저하가 아니라,
비교는 할 수 있지만, 지금 방식으로는 대량 데이터를 안정적으로 감당할 수 없었다
는 데 있었습니다.
기존 사고방식: 비교 대상이면 다 같은 방식으로 처리하자
처음에는 당연히 이렇게 생각하게 됩니다.
“일반 컬럼이든 LOB든 결국 값 비교인데, 같은 비교 경로에 태우면 되지 않을까?”
일반 컬럼만 있을 때는 이 생각이 맞습니다.
하지만 LOB가 끼면 상황이 달라집니다.
일반 컬럼은 hash 기반 비교가 효율적입니다.
반면 LOB는
- 기존 hash 계산 경로에 자연스럽게 넣기 어렵고
- 통째로 읽어 비교하면 메모리를 크게 잡아먹고
- 대량 처리에서는 가장 비싼 타입이 됩니다
즉, 모든 타입을 같은 방식으로 처리하려는 순간 비교 시스템 전체가 가장 까다로운 타입의 제약을 그대로 떠안게 됩니다.
문제의 본질은 SQL 문법이 아니라 이거였습니다.
같은 row 비교라도, 모든 타입을 같은 방식으로 다루면 안 됐다
전환점: 계산 방식을 바꾸기보다, 읽는 방식을 바꾸자
이번 개선의 핵심은 해시 함수를 바꾼 게 아닙니다.
관점을 바꾼 겁니다.
기존 질문은 이랬습니다.
“LOB도 기존 hash 방식 안에서 같이 처리할 수 있을까?”
개선 후 질문은 이렇게 바뀌었습니다.
“LOB를 굳이 같은 경로에 넣지 말고,
메모리를 덜 쓰는 방식으로 따로 읽어서 비교하면 되지 않을까?”
여기서 나온 답이 스트리밍 방식입니다.
즉,
- 일반 컬럼은 기존 hash 비교 경로를 유지하고
- LOB 타입은 통째로 메모리에 올리지 않고
- 조금씩 읽으면서 처리하는 별도 경로로 분리한 겁니다
이 차이가 큽니다.
비교 로직 자체는 유지하면서, 가장 비싼 타입의 처리 방식만 바꿨기 때문입니다.
왜 스트리밍 방식이 맞았나
이번 개선에서 중요한 건 “LOB도 처리했다”가 아닙니다.
중요한 건 LOB를 통째로 메모리에 올리지 않고도 비교할 수 있게 만들었다는 점입니다.
스트리밍 방식의 장점은 명확합니다.
- 값을 한 번에 메모리에 다 올리지 않아도 된다
- 큰 데이터를 잘게 읽으면서 처리할 수 있다
- 비교 대상이 많아져도 메모리 사용량이 훨씬 안정적이다
- 대량 비교에서도 OOM 위험을 크게 낮출 수 있다
- 일반 컬럼 비교 경로를 그대로 유지할 수 있다
즉, 이 방식은 단순 최적화가 아닙니다.
처리가 가능하도록 만들면서, 운영에서도 버틸 수 있게 만든 방식입니다.
이 점이 중요합니다.
실무에서는 종종 “더 빠른 방식”보다 먼저 “안 터지는 방식”이 필요합니다.
LOB 비교가 딱 그런 영역입니다.
이번 개선의 본질은 “속도”보다 “메모리 안정성”이었다
겉으로 보면 이 작업은 비교 성능 개선처럼 보일 수 있습니다.
물론 결과적으로는 성능에도 도움이 됩니다.
하지만 더 본질적인 변화는 따로 있습니다.
이 작업의 핵심은
- SQL을 예쁘게 다듬은 것도 아니고
- 인덱스를 추가한 것도 아니고
- 해시 함수를 바꾼 것도 아닙니다
핵심은
비교 가능한지 여부보다,그 비교를 운영 메모리 안에서 계속 감당할 수 있는 방식으로 바꾼 것
입니다.
LOB는 비교 자체보다 읽는 방식에서 비용이 크게 갈립니다.
그런데도 통째로 읽어서 비교하면, 비교 건수가 늘어날수록 메모리 사용량은 계속 커집니다.
이건 작은 테스트에서는 잘 안 보이고, 운영에서만 크게 드러나는 종류의 문제입니다.
그래서 이번에는 속도보다 먼저 메모리 안정성을 확보하는 방향으로 구조를 바꿨습니다.
물론 트레이드오프는 있다
이 방식이 공짜는 아닙니다.
1. 구현은 더 복잡해진다
기존에는 한 가지 비교 경로만 생각하면 됐습니다.
하지만 이제는
- 일반 컬럼 경로
- LOB 스트리밍 경로
를 함께 관리해야 합니다.
구조는 분명 더 복잡해집니다.
2. “그냥 읽어서 비교한다”는 단순함은 포기해야 한다
기능만 보면 값을 다 읽어서 비교하는 방식이 가장 단순합니다.
하지만 운영에서는 그 단순함이 곧 메모리 부담으로 바뀝니다.
즉, 단순한 구현을 포기한 대신 지속 가능한 처리 방식을 얻은 셈입니다.
3. 그래도 운영 관점에서는 훨씬 낫다
이 경우에는 단순함을 유지하려고 LOB까지 같은 경로에 넣는 편이 더 나쁜 선택이었습니다.
실무에서는 자주 이렇게 결정합니다.
- 구현은 조금 복잡해져도
- 대량 데이터에서 버틸 수 있고
- OOM 없이 계속 돌 수 있는 쪽을 택한다
이번 사례가 딱 그랬습니다.
제가 이번 개선에서 좋게 본 지점
이번 작업이 좋았던 이유는 “LOB도 비교되게 만들었다”에서 끝나지 않는다는 점입니다.
핵심은 다음 세 가지입니다.
1. 기능보다 운영 조건을 먼저 봤다
비교가 되느냐만 보면 통째로 읽어와서 비교하는 방식도 답이 될 수 있습니다.
하지만 운영에서는 그 방식이 메모리 안에서 계속 버틸 수 있어야 합니다.
이번에는 바로 그 지점을 먼저 봤습니다.
2. 전체 비교 모델을 버리지 않았다
일반 컬럼의 hash 비교라는 큰 틀은 유지했습니다.
즉, 전체 구조를 갈아엎지 않고 정말 문제를 일으키는 타입만 별도 경로로 분리했습니다.
이건 운영 시스템에서 특히 좋은 선택입니다.
3. OOM을 “예외 처리”가 아니라 “설계 문제”로 봤다
메모리 이슈는 종종 힙 사이즈를 키우거나 배치를 줄이는 식으로 임시 대응합니다.
하지만 이런 문제는 결국 다시 나옵니다.
이번에는 OOM을 환경 문제가 아니라 처리 방식의 설계 문제로 보고 경로를 바꿨습니다.
이 판단이 좋았습니다.
마무리
성능 개선 글은 종종 “쿼리 줄였습니다”, “인덱스 추가했습니다”에서 끝납니다.
하지만 실제로 더 중요한 개선은 이런 쪽에서 나옵니다.
- 기능은 되지만 운영에서 메모리를 감당하지 못하는 구조를 발견하고
- 가장 비싼 타입을 별도 경로로 분리하고
- 대량 처리에서도 OOM 없이 돌 수 있게 비교 방식을 다시 설계하는 것
이번 건이 그랬습니다.
Oracle의 LONG, CLOB, BLOB 같은 LOB 타입은 기존 일반 컬럼 중심의 hash 경로에 그대로 넣기 어려웠습니다.
그렇다고 값을 통째로 읽어서 비교하면 데이터량이 커질수록 메모리 사용량이 빠르게 커지고, 대량 처리에서는 결국 OOM 위험으로 이어집니다.
그래서 일반 컬럼은 기존 방식으로 두고, LOB 타입만 스트리밍 방식으로 별도 처리하도록 바꿨습니다.
즉, 이번 개선의 핵심은 단순한 SQL 튜닝이 아니라
Oracle LOB의 타입 제약과 메모리 한계를 기준으로비교 경로를 다시 설계한 것
이었습니다.
결국 성능 문제는 종종 SQL보다 먼저, 무엇을 어떤 단위로 메모리에 올리고, 어떤 방식으로 계산할 것인가의 문제로 돌아옵니다.
LOB 비교가 딱 그런 사례였습니다.