나는 B2B솔루션 회사에 다니는 1년차 백엔드 개발자다.
몇달전 회사에서 Mysql 데이터 마이그레이션 작업을 진행할 일이 있었다.
간단하게 Insert쿼리만 작성해서 해결하긴 어려웠던 문제라
이러한 복잡한 문제를 어떻게 해결했는지 공유하려고 한다.
마이그레이션을 진행해야 했던 상황을 비유해서 표현하자면 아래와 같다.
적포도를 판매하는 서비스가 있었다.
시간이 지나면서 신규사업으로 샤인머스켓도 취급하게 되었는데
적포도 판매 서비스의 소스코드, DB 스키마를 그대로 가져와서 샤인머스켓 판매 서비스를 만들었다.
각 서비스는 서로다른팀이 운영하면서 DB 스키마도 일부 달라졌다.
그리고 다시 시간이 흘러 다른 포도도 취급하게 되면서 기존에 존재하던 서비스 2개를 합쳐서 통합 포도 판매 서비스를 런칭해야 하는 상황이 발생했다.
팀에서는 샤인머스켓 서비스의 소스코드와 DB스키마를 기준으로 통합 포도 판매서비스를 만들기로 결정했고 기존 서비스의 DB데이터를 새로운 통합 서비스 DB로 이전하는 마이그레이션 작업이 필요했다.
위에서 언급했듯이 두 서비스는 기본적으로 동일한 DB 스키마를 사용하였으나, 서로 다른 팀이 운영하면서 일부 스키마에 차이가 발생하였다.
아래 그림은 각 서비스별 DB 테이블을 나타낸다.
운영 중에 추가된 추천인 컬럼은 적포도 서비스에서는 구매자 정보 테이블에 저장되었지만, 샤인머스켓 서비스에서는 포도 판매 이력 테이블에 저장되었다.
게다가, 적포도 서비스의 포도등급 테이블의 등급코드(PK)는 UUID 형태로, 이를 포도 판매이력 테이블에서 논리적 FK로 참조하고 있다. 그리고 샤인머스켓 서비스의 포도등급 테이블 등급코드는 적포도 서비스와 동일한 UUID를 사용하고 있으며, 이는 적포도 서비스의 테이블 데이터를 그대로 복사하여 사용한 결과이다.😱
여기서 중요한 점은, 해당 등급코드가 포도 판매이력에서만 사용되는 것이 아니라, 다양한 다른 테이블에서도 참조되고 있다는 것이다. 그렇기 때문에 서비스 통합을 진행할 때 아래와 같이 포도등급 테이블을 변경해서 사용해야 했다.
이 과정에서 적포도의 등급코드와 샤인머스켓의 등급코드를 별개의 UUID로 지정하고, 데이터를 추가할 때 해당 UUID를 변환하는 작업까지 필요했다.
첫 번째로 검토했던 방법은 Mysql Workbench의 마이그레이션 기능을 활용하는 것이었다. Mysql Workbench는 다른 RDBMS에서의 데이터를 Mysql로 마이그레이션하거나 Mysql의 버전 업그레이드 시 활용할 수 있는 기능을 제공한다. 심지어 테이블 구조가 약간 다르더라도, 데이터 매핑과 변환을 통한 마이그레이션이 가능하다.
그러나 앞서 언급한 문제들을 완전히 해결하기에는 이 방법만으로는 한계가 있었으며, 이 기능에 익숙해지는 데에 시간이 필요해 보였다.
또 다른 방법으로는 적포도 서비스의 DB 데이터를 Mysql Workbench를 통해 추출하여, 각각을 insert 문으로 변환해 샤인머스켓 서비스의 DB에 직접 넣는 것을 고려해보았다.
하지만, 이 방식은 작업할 양이 너무 많아져서 최후의 방법으로 생각하고 다른 대안을 우선적으로 찾아보기로 했다.
완벽한 해결책은 없었다. 따라서, 스키마 차이를 직접 처리하고, DB에 자동으로 데이터를 넣어주는 전용 마이그레이션 프로그램을 개발하는 것을 검토했다.
시간이 촉박하고 오류 없이 정확한 데이터 마이그레이션이 필요했기 때문에, 이 방법이 최선이라고 생각했다.
내 주력언어는 자바이다.
내가 느끼기에 자바는 빠른 결과를 봐야하는 상황에서는 적절하지 않다고 생각한다.
개발환경 세팅과 내가 원하는 결과도출까지 작성해야하는 보일러플레이트 코드가 많기 때문이다.
그래서 이번에는 인터프리터 언어인 자바스크립트를 이용해 빠르게 마이그레이션 프로그램 개발을 하기로 했다.
그러나 자바스크립트는 내 전문 분야가 아니었기에, ChatGPT에 도움을 요청하기로 결정했다.
당시 마이그레이션 프로그램 작성을 위해 질문했던 기록
구상했던 코드의 흐름도는 아래와 같다.
1. 마이그레이션 데이터 UUID 리스트 생성
옮겨야할 데이터를 특정 테이블의 PK(UUID)를 기준으로 모두 가져올 수 있었기 때문에 UUID를 리스트로 저장
2. 테이블 마이그레이션 함수 실행 (파라미터 : UUID 리스트)
2-1. Mysql 커넥션 연결
마이그레이션 진행할 원본 DB 커넥션 (적포도서비스 DB)
마이그레이션된 데이터를 넣을 타겟 DB 커넥션 (통합서비스 DB)
2-2. 원본 DB에서 데이터 SELECT해서 가져오기
UUID에 해당하는 값을 가져옴
2-3. 가져온 데이터 변환
2-4. 타겟 DB에 변환한 데이터 INSERT
2-5. Mysql 커넥션 종료
3. 2번과 동일하게 다른 테이블 마이그레이션 함수 순차적으로 실행
이제 위의 흐름을 코드로 작성하기 위해
ChatGPT에게 받은 예시 코드는 아래와 같다.
const mysql = require('mysql');
// connect1 설정
const connect1 = mysql.createConnection({
host: 'localhost',
user: 'your_username1',
password: 'your_password1',
database: 'your_database1'
});
// connect2 설정
const connect2 = mysql.createConnection({
host: 'localhost',
user: 'your_username2',
password: 'your_password2',
database: 'your_database2'
});
// MySQL 연결
const connect1Promise = new Promise((resolve, reject) => {
connect1.connect((err) => {
if (err) {
console.error('connect1 연결 오류:', err);
reject(err);
} else {
console.log('connect1에 연결되었습니다.');
resolve();
}
});
});
const connect2Promise = new Promise((resolve, reject) => {
connect2.connect((err) => {
if (err) {
console.error('connect2 연결 오류:', err);
reject(err);
} else {
console.log('connect2에 연결되었습니다.');
resolve();
}
});
});
async function executeQuery() {
try {
await Promise.all([connect1Promise, connect2Promise]);
// SELECT 쿼리 실행
const selectResults = await executeSelectQuery(connect1);
// 데이터 변환
const transformedData = transformData(selectResults);
// INSERT 쿼리 실행
await executeInsertQuery(connect2, transformedData);
console.log('데이터가 성공적으로 삽입되었습니다.');
} catch (err) {
console.error('오류 발생:', err);
} finally {
connect1.end();
connect2.end();
}
}
function executeSelectQuery(connection) {
return new Promise((resolve, reject) => {
connection.query('SELECT * FROM table1', (err, results) => {
if (err) {
reject(err);
} else {
resolve(results);
}
});
});
}
function transformData(results) {
return results.map((row) => ({
...row,
product: 'RED_GRAPE'
}));
}
function executeInsertQuery(connection, data) {
return new Promise((resolve, reject) => {
connection.query('INSERT INTO table2 SET ?', data, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
}
// 함수 실행
executeQuery();
위 예시코드는 첫번째 커넥션으로 가져온 데이터에 product
라는 필드를 추가하고 두번째 커넥션에 INSERT하는 코드이다.
ChatGPT가 작성해준 코드를 참고해서
프로그램 구상부터 개발 및 테스트까지 5시간정도 소요되었다.
(아마 ChatGPT가 없었으면 훨씬 오래걸렸을거라 생각한다.)
개발하면서 자바스크립트를 사용하길 잘했다고 생각했던 부분이 Object.map()
함수이다.
데이터 변환하는 메소드인 transformData(results)
에서 파라미터로 받은 오브젝트를
map()
함수를 이용해 기존 필드를 그대로 넣고 필요한 필드가 새로 추가하거나 기존 필드를 수정해서 새 오브젝트로 만들 수 있어서 테이블 마이그레이션 작업에 매우 용이했다.
패치 전날 테스트 서버에서 프로그램을 검증하며 버그를 수정했다.
그리고 패치 당일엔 10분만에 마이그레이션 작업을 마친 후 정상적으로 서비스를 런칭할 수 있었다.
마이그레이션으로 잘못된 데이터가 발생한 이슈는 단 한건도 없었다. (깜빡하고 안넣은 데이터는 있었다)
+
서비스 런칭 이후에도 기존 DB에서 데이터를 추가로 가져와서 마이그레이션해야 할 일이 있었는데 이때도 마이그레이션 프로그램이 유용하게 쓰였다.
이번 경험을 통해 얻은 교훈과 느낀 점을 간략하게 정리해보면 다음과 같다.
문제 정의: 어떠한 작업을 진행하기 전에 문제의 본질을 명확하게 정의하는 것은 매우 중요하다. 이번 마이그레이션 작업에서도 처음에는 문제의 크기와 복잡도를 제대로 파악하지 못하고 시작했다면 중간에 많은 어려움에 부딪혔을 것이다.
도구 선택: 작업을 진행하는 데 필요한 도구나 언어를 선택할 때는 그 작업의 특성과 필요성에 따라 결정해야 한다. 이번에는 자바보다는 자바스크립트가 더 적합하다고 판단하여 사용했는데, 그 선택이 결과적으로 좋은 결과를 가져다줬다.
외부 도움: 모든 것을 혼자 해결하려고 하는 것은 비효율적일 수 있다. 필요한 지식이나 부족한 경험은 외부의 도움을 받아 더 빠르게 문제를 해결하는 것이 좋다. 이번에도 ChatGPT와 같은 도구의 도움으로 보다 효율적인 방법으로 문제를 해결할 수 있었다.
테스트의 중요성: 마이그레이션 같은 중요한 작업을 진행할 때는 항상 테스트의 중요성을 간과해서는 안 된다. 실제 환경에 적용하기 전에 반드시 테스트 환경에서 여러번 테스트를 진행하며 문제점을 찾아내고 수정하는 과정이 필요하다.
마지막으로, 이번 작업을 통해 나 자신의 능력과 한계도 다시 한번 깨달을 수 있었다. 항상 더 배우고 더 성장하려는 노력을 멈추지 않아야겠다는 생각을 갖게 되었다. 👍
많은 것을 배웠습니다, 감사합니다.