
최근 기업 데이터 아키텍처에서 클라우드 구축, 데이터 카탈로그 관리, ETL 파이프라인 같은 주제를 다룰 때면 대개 Airflow, Spark, Debezium 같은 굵직한 오픈소스나 관리형 서비스 이름이 먼저 나오기 마련입니다.
설계를 시작하는 단계에서는 자연스럽게 이러한 대규모 솔루션을 검토하게 되지만, 현재 마주한 조직의 요구사항과 제약 조건을 냉정하게 뜯어보면 본질적으로 필요한 시스템의 실체가 보이기 시작합니다.
이번 프로젝트의 핵심 과제는 Oracle 시스템의 데이터를 PostgreSQL 기반으로 마이그레이션하는 환경을 안정적으로 통제하는 것이었습니다.
이 과정에서 깨달은 점은 지금 당장 우리에게 시급한 것은 수십 테라바이트의 데이터를 실시간으로 스트리밍하는 고성능 "데이터 적재 엔진" 그 자체가 아니라는 사실이었습니다.
정작 중요한 문제는 'Oracle에 존재하는 특정 테이블들을 PostgreSQL으로 마이그레이션하겠다'는 일련의 작업을 누가, 언제, 어떤 커넥션 정보와 계정 권한을 가지고 실행했는가를 명확히 추적하고 제어할 수 있는 관리 및 거버넌스 층을 확보하는 것이었습니다.
이 시스템을 한 줄로 요약하자면 데이터 카탈로그 제어 플레인입니다.
데이터를 직접 하부 파이프라인에서 쏟아붓고 변형하는 런타임의 주인공이 아니라, 다양한 이기종 데이터베이스의 연결 정보, 메타데이터, 그리고 마이그레이션 작업의 생명주기를 REST API로 엮어서 단일 진입점으로 통제하는 서버를 구축한 과정과 아키텍처적 선택에 대해 상세히 공유하고자 합니다.
제어용 메타데이터를 저장하고 관리하는 DB의 데이터 액세스 기술을 고를 때, 현대적인 Spring Boot 환경이라면 당연히 Spring Data JPA를 먼저 떠올리는 것이 자연스럽습니다.
객체 지향적인 도메인 모델을 구축하고 생산성을 높이기에 최적의 선택이기 때문입니다.
그러나 조직의 인프라 표준과 기존 레거시 데이터베이스의 운영 패턴을 상세히 분석하면서 설계의 방향을 수정해야 했습니다.
도메인의 데이터 처리 흐름에서 아주 뚜렷한 세 가지 규칙이 발견되었기 때문입니다.
조회 연산: 애플리케이션에서 직접 복잡한 테이블 조인을 수행하지 않고, 성능과 보안을 위해 미리 정의된 연결 프로필 뷰, 메타데이터 테이블 목록 뷰 같은 데이터베이스 뷰를 호출하여 데이터를 읽어옵니다.
CUD 연산: 단일 테이블에 대해 INSERT INTO t_lnkg_profile ...과 같은 표준 DML 문을 직접 실행하는 패턴이 존재하지 않았습니다.
대신 업무 도메인별로 완결성을 가진 단 하나의 통일된 저장 프로시저를 호출하여 모든 상태 변화를 처리하고 있었습니다.
식별자 채번 규칙: 새로운 마이그레이션 작업이 생성될 때의 ID는 애플리케이션 레이어에서 UUID나 Sequence를 통해 발행하는 것이 아니라, 프로시저 내부에서 DB 함수를 호출하여 동기적으로 발급하는 구조였습니다.
이러한 환경에서 JPA를 억지로 도입하려면 모든 엔티티마다 어노테이션을 복잡하게 매핑하거나, 프로시저 중심의 설계를 포기하고 엔티티 직접 DML 방식으로 데이터베이스 레이어의 구조를 완전히 뜯어고쳐야만 했습니다.
그러나 이미 데이터베이스 관리를 전담하는 조직과의 협업 체계 및 기존 시스템의 아키텍처 방향성상 "CUD 연산은 데이터 무결성을 위해 프로시저로 제한한다"는 규칙이 확고히 자리 잡고 있었습니다.
이에 따라 프레임워크의 이점을 살리기 위해 억지로 기술을 끼워 맞추기보다, SQL과 프로시저 호출을 가장 직관적으로 제어할 수 있는 MyBatis를 선택하고, CALLABLE 구문을 활용하며, 프로시저의 형상 관리를 위해 Flyway를 조합하는 아키텍처 스택으로 선회했습니다.
실제 도메인 층과 인프라 층을 연결하는 퍼시스턴스 어댑터의 코드는 다음과 같이 명확하고 단순하게 유지됩니다.
도메인 모델은 오직 "연결 프로필을 영속화한다"는 비즈니스적 행위만 인지할 뿐, 하부 저장소가 프로시저로 움직이는지 여부는 전혀 알 필요가 없도록 은닉했습니다.
// ConnectionPersistenceAdapter — 작업 유형(op)만 결정하여 저장 프로시저 호출
String op = resolveOp(profile); // C(Create) / U(Update) / D(Delete) 중 하나로 분기
var params = ConnectionPersistenceMapper.toProcedureParams(profile, op);
params.put("op", op);
// MyBatis 매퍼를 통해 프로시저 실행
commandMapper.executeLnkgProfile(params);
// 새롭게 생성된 경우 DB 프로시저 내부에서 INOUT 파라미터를 통해 채번된 ID를 반환받음
String id = profile.id() != null && !profile.id().isBlank()
? profile.id()
: (String) params.get("lnkgId");
PostgreSQL 데이터베이스 엔진에서 구동되는 MyBatis XML 매퍼 설정은 파라미터 모드를 엄격히 지정한 전형적인 CALLABLE 구문의 형태를 띱니다.
<update id="executeLnkgProfile" databaseId="PostgreSQL" statementType="CALLABLE">
{call sp_lnkg_profile(
#{op, mode=IN, jdbcType=CHAR},
#{lnkgId, mode=INOUT, jdbcType=VARCHAR},
)}
</update>
이 설계 과정에서 직면했던 중요한 비용과 주의점: 초기 개발 단계에서는 로컬 및 CI 환경에서 가볍게 구동할 인메모리 데이터베이스로 H2를 검토했습니다.
데이터베이스 형상이 단순할 때는 H2의 가벼움이 무기가 되지만, "모든 CUD가 프로시저"라는 제약이 들어오는 순간 상당한 비용이 발생합니다.
H2는 PostgreSQL 고유의 CREATE PROCEDURE 문법을 직접적으로 지원하지 않기 때문에, Java 언어로 작성된 정적 메서드를 데이터베이스 내부에 등록하는 CREATE ALIAS 기능을 활용해 프로시저의 동작을 흉내 내야 했습니다.
더욱 까다로웠던 부분은 INOUT 파라미터를 처리하는 방식의 차이였습니다.
H2는 PostgreSQL처럼 단일 프로시저 내에서 입력과 출력을 동시에 처리하는 INOUT 변수를 완벽히 소화하지 못하여, 결국 로컬 테스트 환경을 위한 전용 매퍼 파일에는 {? = call sp_*(...)}와 같이 함수 반환형태로 SQL 구문을 분리해야 했습니다.
결과적으로 "운영 환경용 PostgreSQL 구문 한 벌, 테스트 환경용 H2 구문 한 벌"을 동시에 유지보수해야 하는 아키텍처적 부채와 숨은 비용이 발생하게 되었습니다.
만약 이러한 데이터베이스 간의 특성 차이를 간과한 채 배포 파이프라인만 PostgreSQL 타깃으로 검증하면 운영은 잘 돌아가겠지만, 로컬 개발 생산성을 위한 테스트 스위트가 깨지는 현상을 겪게 됩니다.
따라서 프로시저 중심의 아키텍처를 잡을 때는 개발 초기 단계부터 통합 테스트의 범위를 어떻게 격리할 것인지 명확한 전략을 수립해야 합니다.
여기서 기술적으로 명확히 짚고 넘어가야 할 지점이 있습니다.
많은 엔지니어들이 카탈로그 제어 서버 내에 MyBatis가 도입되었다고 하면, 실제 원천 데이터베이스에서 타깃 데이터베이스로 대량의 데이터를 긁어오는 적재 로직 역시 MyBatis나 JPA 같은 ORM 프레임워크를 거쳐 갈 것이라고 오해하곤 합니다.
하지만 실제 데이터 마이그레이션의 실행 본체는 제어 메타데이터를 다루는 MyBatis 레이어와 철저하게 물리적·논리적으로 분리되어 있습니다.
데이터를 이관하는 핵심 컴포넌트인 MigrationCommandService는 프레임워크의 무거운 객체 매핑 컨텍스트를 전혀 타지 않습니다.
순수한 JDBC 커넥션 포트를 직접 제어하여 원천 데이터베이스에서 데이터를 스트리밍 방식으로 쿼리한 뒤, 타깃 데이터베이스의 벌크 인서트 API로 밀어 넣는 로우 레벨 작업에 집중합니다.
기본적인 파이프라인의 이관 루프 아키텍처는 다음과 같은 형태로 추상화되어 있습니다.
int offset = 0;
while (true) {
// 1. 원천 데이터베이스 포트로부터 정해진 배치를 페이지네이션 단위로 읽어옴
List<Map<String, Object>> rows = sourceDataReaderPort.readRows(
command.source(), command.sourceSchema(), command.tableName(),
columnNames, command.batchSize(), offset);
if (rows.isEmpty()) break;
// 2. 타깃 데이터베이스 포트의 대량 적재 기능을 호출하여 배치 인서트 실행
totalRows += targetDatabasePort.batchInsert(
command.target(), command.targetSchema(), command.tableName(),
columnNames, values);
// 3. 오프셋을 읽어온 행 수만큼 전진시키며 다음 루프 준비
offset += rows.size();
// 4. 배치 사이즈보다 작게 읽어온 경우에 도달하면 루프 종료
if (rows.size() < command.batchSize()) break;
}
구현 코드 자체는 매우 직관적입니다. 타깃 테이블의 메타데이터 구조를 읽어서 DDL을 동적으로 생성하고, 사용자의 선택에 따라 타깃 환경에 기존 테이블이 존재할 경우 DROP 후 CREATE를 수행한 뒤, 지정한 배치 크기만큼 LIMIT와 OFFSET 기반의 페이지네이션으로 데이터를 분할 조회하여 타깃에 삽입을 반복하는 구조입니다.
하지만 이 단순한 루프 메커니즘을 아무런 보완 조치 없이 곧바로 운영 환경에 배포하여 대용량 테이블에 던지면 아키텍처적인 재앙을 맞이하게 됩니다.
기술 검증 단계에서 반드시 인지하고 고도화해야 하는 세 가지 명확한 한계점이 존재하기 때문입니다.
OFFSET 기반 페이지네이션의 성능 저하: 이관 대상 테이블의 레코드 수가 수백만, 수천만 건을 넘어가기 시작하면 LIMIT / OFFSET 구문은 급격한 성능 저하를 일으킵니다.
데이터베이스 엔진 특성상 뒤쪽 페이지를 조회할 때도 결국 앞선 행들을 모두 스캔한 뒤 버려야 하므로, 오프셋 값이 커질수록 데이터베이스의 CPU와 I/O 자원을 극심하게 소모하게 됩니다.
수백만 건 이상의 대용량 이관을 안정적으로 소화하려면 정렬된 고유 키를 기반으로 이전 페이지의 마지막 값을 조건절에 넣고 조회하는 키셋 페이지네이션 방식으로 조회 포트를 개선해야 합니다.
그렇지 않으면 "처음 몇만 건은 빠르게 가다가 뒤로 갈수록 시스템이 멈춘 듯 밤새 렌더링만 하는" 현상이 발생합니다.
애플리케이션 레이어의 행 상한 제어 부재: 현재 작성된 배치의 파라미터 중 batchSize는 '한 번의 JDBC 네트워크 왕복 시 메모리에 적재할 윈도우 크기'를 결정할 뿐, 해당 테이블에서 '최대 몇 건까지 안전하게 마이그레이션을 허용할 것인가'에 대한 총량 제한이 없습니다.
로컬 테스트 환경에서는 단 2행 정도의 인메모리 데이터만 검증했기 때문에 메모리나 커넥션에 문제가 없었지만, 실제 수억 건의 레코드가 쌓인 실운영 데이터베이스에서 대용량 통합 테스트 없이 이 API를 호출하면 힙 메모리 부족이나 트랜잭션 타임아웃이 발생할 위험이 있습니다.
현실적인 시스템 아키텍처 관점에서 이 경량 엔진이 보장할 수 있는 안정적인 테이블당 적재 상한선은 대략 1만 건에서 최대 50만 건 사이로 제한하는 것이 안전하며, 그 이상의 데이터는 전용 인프라 솔루션으로 넘겨야 합니다.
동기식 처리와 비동기식 처리의 인터페이스 분리: 단건 테이블 마이그레이션을 수행하는 /load API는 HTTP 요청-응답 스레드가 이관 작업이 완전히 끝날 때까지 동기적으로 커넥션을 붙잡고 대기하는 구조입니다.
데모 시연이나 소량의 개발 데이터를 맞출 때는 직관적이라 편하지만, 마이그레이션 시간이 수분을 넘어가면 HTTP 커넥션 타임아웃으로 인해 커뮤니케이션이 끊어집니다.
따라서 실제 운영 환경의 엔드포인트는 즉시 배치 작업의 ID만 반환하고 백그라운드 스레드 풀에서 작업을 처리하는 비동기식 /load/batch 라우트로 설계를 이원화하고, 작업의 진행 상태는 폴링이나 웹소켓으로 추적하도록 구조를 잡아야 합니다.
또한 Oracle 데이터베이스의 NUMBER나 VARCHAR2 같은 고유한 데이터 타입을 타깃 데이터베이스인 PostgreSQL이나 MySQL의 표준 타입으로 매핑해 주는 DdlTypeMapper 아키텍처의 경우, 기본적인 데이터 타입 변환 기능의 단위 테스트는 통과하도록 설계되었습니다.
그러나 데이터베이스 스키마 마이그레이션에서 정말 까다로운 영역인 인덱스, 외래키 제약 조건, 파티셔닝 구조, 시퀀스 등은 현재 경량 제어 플레인의 스코프 밖으로 설정되어 있습니다.
본 시스템은 철저하게 '스키마의 기본 테이블 뼈대를 생성하고 데이터 로우를 유실 없이 복사하는 것'까지를 목표로 삼아야 복잡도의 한계에 부딪히지 않습니다.
이 프로젝트의 패키지 아키텍처는 cdw.catalog 루트 하위에 웹 레이어 → 비즈니스 서비스 → 핵심 비즈니스 모델 → 퍼시스턴스 및 외부 JDBC 연결 구조로 명확하게 계층을 분리한 아키텍처를 채택했습니다.
PoC 단계의 작은 프로젝트임에도 불구하고 의도적으로 레이어를 격리한 명분은 확실했습니다.
향후 요구사항이 확장되어 마이그레이션 작업을 트리거하는 주체가 HTTP 웹 요청이 아니라 아파치 카프카나 RabbitMQ 같은 메시지 큐의 이벤트 기반 컨슈머로 전환되더라도, 비즈니스 로직이 담긴 애플리케이션 서비스와 도메인 레이어는 단 한 줄의 코드도 수정하지 않고 포트의 구현체 어댑터만 갈아끼울 수 있는 유연성을 확보하기 위함이었습니다.
외부 인프라의 변화가 핵심 비즈니스 도메인을 오염시키는 것을 막으려는 아키텍처적 장치입니다.
다만, 기술 검증 단계라는 현실적인 일정과 리소스의 제약 속에서 모든 영역에 100% 순수하게 JPA를 고집하는 비효율은 피하고자 했습니다.
웹 진입점인 MigrationController 내부에서는 계층 간 완벽한 분리를 위한 별도의 도메인 DTO 변환 과정을 생략하고 HTTP 요청/응답으로 들어오는 자바 record 객체를 애플리케이션 서비스까지 깊숙이 전달하도록 설계했습니다.
또한 원천 데이터베이스에서 읽어오는 로우 데이터 역시 굳이 복잡한 객체 그래프로 매핑하지 않고 MyBatis나 JDBC가 반환하는 Map<String, Object> 형태의 로우 DTO 구조를 날것 그대로 파이프라인 루프에 태웠습니다.
즉, 인프라와 도메인의 경계선은 포트 인터페이스로 엄격하게 그어두되, 레이어 내부의 데이터 전달 구조까지 과도하게 추상화하여 생산성을 떨어뜨리지 않는 타협점을 잡았습니다.
프로젝트의 생명주기를 관리할 때 가장 중요한 기술적 결정 중 하나는 '무엇을 만들 것인가'보다 '무엇을 만들지 않을 것인가'를 명확히 선언하는 것입니다. 시스템이 미니 데이터 플랫폼으로 비대해져 본질을 잃지 않도록, 다음 기능들은 설계 단계에서 의도적으로 스코프 밖으로 밀어냈습니다.
JWT 및 정교한 사용자 회원가입/인증 기능: ETL 제어 API 서버의 보안과 권한 관리를 위해 전용 인증 시스템과 관리자 UI를 설계하는 것은 현재 검증 단계의 범위를 초과합니다.
개발 및 데모 환경에서는 엔지니어가 API 명세서인 Swagger UI를 통해 즉시 기능을 테스트하고 실행하는 것을 최우선 목표로 삼아 인증 레이어를 과감히 생략했습니다.
Apache Spark / Airflow 수준의 고도화된 오케스트레이션: t_mig_job 및 t_mig_job_tbl 테이블을 설계하여 어떤 마이그레이션 작업이 돌았고 상태가 성공인지 실패인지, 언제 재시도되었는지에 대한 거버넌스 이력은 남깁니다.
그러나 특정 레코드 이관 실패 시 행 단위로 상태를 기억해 실패한 지점부터 트랙을 재개하는 정교한 체크포인트 기능은 구현하지 않았습니다. 본 시스템의 실패 복구 최소 단위는 철저하게 '테이블 단위 전체 재실행'으로 제한합니다.
독립형 Extract Worker 분리: 대규모 데이터 분산 추출을 위한 에이전트 아키텍처는 HTTP 연동을 위한 인터페이스 인터셉터 스켈레톤 코드로만 열어두었으며, 기본 프로퍼티 설정은 enabled: false 상태로 비활성화했습니다.
데이터 추출용 컴포넌트는 인터페이스 명세만 정의해 두고 실제 구현은 다음 마일스톤으로 이관했습니다.
이러한 스코프 컷은 시스템의 미완성이 아니라 아키텍처의 한계를 명확히 규정함으로써 프로젝트의 목표를 기한 내에 정밀하게 달성하기 위한 전략적 선택입니다.
시스템을 구동하고 실제 이기종 데이터베이스 간의 통합 테스트를 수행하는 과정에서 맞닥뜨린 현실적인 트레이드오프와 해결 과정에서의 체크포인트들은 다음과 같습니다.
초기 설계 시 일부 단순 입력 구문은 MyBatis의 일반 <insert>와 <update> 구문으로 처리하고, 복잡한 채번이나 원천 확인이 필요한 비즈니스만 저장 프로시저를 혼용하여 사용했습니다. 이 방식은 개발 초기에는 빠르게 굴러가는 것처럼 보이지만, 결국 시스템이 고도화되면서 "어떤 코드 경로를 타고 들어오느냐에 따라 식별자 채번 규칙이 깨지거나 히스토리 로그가 누락되는" 치명적인 동시성 이슈가 터지기 시작했습니다.
시스템 전반의 예측 가능성을 높이기 위해 데이터의 상태를 바꾸는 모든 변경 진입점을 오직 데이터베이스 내의 sp_* 프로시저 내부로 완전 강제하고 통합하는 리팩토링 마이그레이션을 단행했습니다.
데이터의 무결성 통제권을 데이터베이스 레이어로 단일화하여 아키텍처적 안정성을 확보했습니다.
애플리케이션 소스 코드 레벨에서 UUID를 무작위로 생성하여 PK로 박는 대신, 데이터 비즈니스 유연성과 가독성을 위해 lnkg-, etl-, migt- 같은 업무별 접두어에 연월일 날짜 조합과 순번을 결합한 비즈니스 식별자 채번 방식을 도입했습니다.
이 채번의 책임은 데이터베이스 내부의 전용 함수가 동기적으로 제어하도록 구성했습니다. 덕분에 운영 데이터베이스에서는 순서와 의미가 보장되는 깔끔한 PK를 얻을 수 있었으나, 앞서 언급한 프로시저가 없는 로컬 H2 테스트 환경에서는 이 채번 규칙의 논리를 Java 언어로 완전히 똑같이 복제한 가짜 데이터베이스 별칭 함수를 별도로 작성하여 이중으로 관리해야 하는 트레이드오프가 발생했습니다.
배치 아키텍처 상 하나의 마이그레이션 작업이 트리거되면, 해당 작업에 속한 여러 개의 테이블들을 자바의 스레드 풀을 활용하여 동시에 병렬로 적재하는 구조를 취했습니다.
그러나 PoC 환경으로 구성된 마이그레이션 전용 커넥션 풀의 최대 크기 기본값은 매우 보수적인 숫자인 3으로 설정되어 있었습니다.
단일 테이블을 옮길 때는 문제가 없으나, 원천 데이터베이스와 타깃 데이터베이스의 사양이 같은 상태에서 여러 스레드가 동시에 커넥션을 획득하려고 경쟁하는 순간 풀 고갈 현상이 발생하며 스레드들이 서로를 붙잡고 대기하는 교착 상태에 준하는 병목이 관찰되었습니다.
현재는 PoC 수준의 기본값으로 제한되어 있으나, 실운영 환경의 동시성 요구량을 소화하기 위해서는 동시 적재 테이블 개수와 커넥션 풀 크기 간의 수리적 튜닝 배율을 고도화해야 하는 숙제가 남아있습니다.
이 기종 간 데이터 타입 매핑 로직이 완벽하지 않은 상태에서 테이블 생성과 데이터 삽입을 단일 트랜잭션 흐름이나 연속된 프로세스로 묶어버리면, 테이블은 정상적으로 만들어졌는데 데이터의 특정 정밀도나 널 제약 조건 때문에 실제 데이터 적재 단계에서 조용히 에러를 뱉으며 멈추거나 데이터가 손상되어 들어가는 리스크가 있었습니다.
이를 해결하기 위해 사용자의 마이그레이션 여정 인터페이스 상에 반드시 /migration/ddl/preview 엔드포인트를 먼저 호출하여 변환된 DDL 명세를 육안 및 시스템으로 사전 검증한 뒤, 승인이 떨어지면 최종 데이터 이관 로드를 트리거하는 단계적 파이프라인 흐름을 설계했습니다.
운영 관리자의 UI 아키텍처 역시 '예측 가능한 미리보기 후 실제 실행'이라는 원칙을 고수하는 것이 시스템의 안정성 면에서 압도적으로 유리합니다.
데이터 엔지니어링 프로젝트를 시작할 때 시스템의 타이틀을 단순히 "ETL 데이터 마이그레이션 프로젝트"라고 명명하는 순간, 주변의 이해관계자들과 동료 엔지니어들이 기대하는 아키텍처의 스펙과 복잡도는 기하급수적으로 부풀어 오릅니다.
분산 스트리밍 처리가 되어야 하고, 장애 복구가 실시간으로 일어나야 하며, 무중단으로 수억 건의 데이터 정합성을 검증해야 할 것 같은 압박에 시달리게 됩니다.
그러나 이번 프로젝트에서 철저하게 지키고자 했던 원칙은 이 시스템이 거대한 데이터 웨어하우스를 직접 굴리는 엔진이 아니라 어디까지나 전체 파이프라인의 상태와 방향을 통제하는 경량 제어 플레인 PoC라는 본질이었습니다.
이 4가지 본질적인 핵심 가치가 API 레이어에서 정교하게 맞물려 돌아가기만 하면, 조직이 요구하는 데이터 마이그레이션 통제력의 데모 시연과 초기 기틀은 충분히 성공적으로 안착할 수 있습니다.
수천만 행의 대용량 데이터를 매일 밤새워 실시간으로 무중단 복사하는 솔루션을 만드는 무거운 접근법과는 궤를 달리해야 합니다.
이 아키텍처를 설계하면서 가장 중요하게 작용한 감각은 데이터베이스를 목적에 따라 철저하게 두 개의 관점으로 나누어 바라보는 아키텍처적 인지력이었습니다.
제어 전용 데이터베이스: 우리의 메타데이터, 마이그레이션 이력 로그, 비즈니스 무결성을 담은 고유한 저장 프로시저들이 굳건히 살아 숨 쉬는 전용 시스템.
원천 및 타깃 데이터베이스: 애플리케이션 프레임워크의 영속성 컨텍스트로 묶어서 관리하는 대상이 아니라, 철저하게 외부 포트를 통해 표준 JDBC API 커넥션으로만 일시적으로 접근하여 데이터를 스트리밍해 오는 격리된 대상.
만약 Spring Boot의 단일 데이터소스 설정 내에 제어 메타데이터 테이블과 마이그레이션 대상 테이블들을 모호하게 전부 때려 박고 설계를 시작했다면, 계층의 책임이 결합되어 도메인 모델이 레거시 테이블의 형상에 따라 끊임없이 흔들리는 부작용을 겪었을 것입니다.
마지막으로, 소프트웨어 아키텍처의 진실은 언제나 화려한 문서나 PPT 장표가 아니라 코드를 검증하는 테스트 스위트의 견고함에서 드러난다고 믿습니다.
현재 단위 테스트는 인메모리 상에서 단 2행의 레코드가 유실 없이 적재되는지를 엄격하게 검증하며 통과하고 있습니다.
기능 통합 테스트는 마이그레이션 작업이 데드락 없이 안정적으로 큐에 등록되고 상태가 변하는지 파이프라인의 흐름을 성공적으로 확인해 줍니다.
그러나 냉정하게 말해 "Oracle 인프라 환경에서 운영 PostgreSQL 카탈로그 시스템으로 실제 수백만 건의 운영 데이터가 아무런 사이드 이펙트 없이 완벽하게 마이그레이션되었다"는 명제는 아직 백퍼센트 입증된 상태가 아닙니다.
단위 테스트의 성공이라는 안락함에 안주하지 않고, 다가오는 단계에서는 실제 운영 환경과 유사하게 격리된 스테이징 데이터베이스 환경에서 최소 1만 건에서 수만 건 단위의 대용량 실데이터를 직접 밀어 넣으며 시스템의 메모리 프로파일링과 커넥션 누수 여부를 실측하고 정량적인 한계 수치를 확보하는 것이 아키텍처를 더욱 견고하게 다지기 위한 다음 마일스톤이자 숙제입니다.
본 제어 플레인 서버를 활용하여 마이그레이션 파이프라인 데모를 구동하는 표준 아키텍처 여정은 다음과 같이 5단계의 명확한 순서로 설계되어 있습니다.
연결 정보 등록: 마이그레이션의 시작점과 종착점이 될 원천 DB와 타깃 DB의 JDBC 접속 정보 및 크리덴셜을 /api/v1/conn 엔드포인트를 통해 시스템에 안전하게 등록합니다.
메타데이터 동기화: 원천 데이터베이스 스키마에 어떤 테이블들이 존재하는지 최신 형상을 파악하기 위해 /api/v1/meta/sync API를 호출하여 데이터 카탈로그의 메타데이터 정보를 동기화합니다.
DDL 매핑 검증: 이기종 데이터베이스 간 타입 변환이 올바르게 설계되었는지 확인하기 위해 /api/v1/migration/ddl/preview API를 요청하여 생성될 타깃 테이블 스키마의 DDL 구조를 사전에 검증합니다.
마이그레이션 작업 실행: 데이터 크기에 따라 최적의 경로를 선택합니다.
단일 소량 테이블의 즉시 적재는 동기식 엔드포인트인 /migration/load를 활용하고, 복잡하고 무거운 대량의 테이블 그룹 적재는 비동기 배치 스레드 풀로 위임하는 /migration/load/batch를 호출합니다.
Swagger 명세서 활용: PoC 단계의 원활한 데모 진행과 프론트엔드 엔지니어 및 운영 조직과의 원활한 협업을 위해 모든 API 엔드포인트의 리퀘스트 바디 예시와 예외 케이스 설명은 Swagger UI 명세서 내부의 각 파라미터 필드마다 상세히 명문화하여 반영해 두었습니다.
애플리케이션 저장소의 Description은 시스템의 목적과 아키텍처의 정체성을 한눈에 파악할 수 있도록 다음과 같이 명료하게 구성했습니다.
이 프로젝트를 지탱하는 기술 스택은 Java 21, Spring Boot 3, MyBatis, Flyway, 제어용 메타데이터 저장을 위한 PostgreSQL, 그리고 이관 처리를 위한 로우 레벨 JDBC 포트 구조입니다. 최신 유행하는 분산 프레임워크나 화려한 클라우드 네이티브 아키텍처를 전면에 내세우지는 않았습니다.
하지만 대규모 인프라 도입에 앞서 "우리 조직의 핵심 데이터 마이그레이션과 카탈로그 거버넌스를 완벽하게 통제하기 위해 필요한 최소한의 안정적인 조종석"을 엔지니어링 원칙에 기반하여 단단하게 세워 올린 과정이었습니다.