들어가며
CUBRID 데이터베이스 환경에서 돌아가던 SQL 쿼리들을 MySQL 8.x로 마이그레이션하는 작업을 한 내용으로 포스팅을 해보려고 한다.
AI Copilot(Agent Mode)을 활용해 1000개가 넘는 쿼리 변환을 성공적으로 마칠 수 있었고, 최근 AI 기술 발전에 관심 있는 개발자분들에게 해당 경험 공유가 도움이 되었으면 좋겠다.

MySQL 8.x 로 마이그레이션을 결정한 이유
먼저 기존의 CUBRID 시스템에서 MySQL 8.x 로 옮기게된 이유부터 설명하려고 한다.

첫번째, 잦은 Lock 발생
RANGE 조건이 들어간 쿼리가 특정 시간대에 몰리면, 인덱스 범위 스캔 과정에서 잠금이 생각보다 넓게, 많이 잡히는 순간이 종종 있었다. 특히 업데이트 + 삭제처럼 쓰기 작업이 섞이면, 한 건이 느린 게 아니라 대기열이 길어지며 다른 트랜잭션까지 줄줄이 밀리는 형태로 체감 성능이 급격히 떨어졌다. MySQL(InnoDB)은 기본적으로 인덱스 레코드 단위로 잠금을 잡는 구조라 같은 범위 조건이라도 인덱스 + 쿼리 전략을 잘 짜줘서 영향 범위를 더 잘 쪼개줘서 버틸 수 있다고 알고있었지만, CUBRID 쪽은 특정 패턴에서 락이 누적되며 운영이 빡세다고 생각했었다.
두번째, 대용량 동시성 처리에서 비효율
이런 락이 많이 걸리는 순간은 데이터가 커지고 동시 요청이 늘면서 더 곤란해졌다. CUBRID는 락을 너무 많이 관리하게 되면 상위 단위로 락을 격상을 수행할 수 있는데, 이게 발생하면 개별 행 단위로 버티던 흐름이 더 거친 단위의 락으로 바뀌면서 동시 처리 효율이 확 떨어질 수 있다. 반면 MySQL(InnoDB)은 기본적으로 인덱스 레코드 기반 row-level locking을 중심으로 동시성을 유지하는 설계라 특정 트랜잭션이 과도하게 커질때에도 버티는 비용이 CUBRID 대비 많이 좋다고 판단했다.
본론
여기까지가 서론이고, 그렇다면 1000개 이상의 쿼리를 단기간에 변환하기 위한 효율적인 방법으로 Copilot을 어떻게 활용했을까 ?
마이그레이션에 Copilot 활용
핵심은 Copilot Agent Mode였다. Claude 4 기반 모델에 agent-prompt.md 를 컨텍스트로 주입해 변환 규칙을 고정하고, Agent가 쿼리를 읽어 변환 → 누락 체크 → 재수정까지 한 흐름으로 반복 처리하도록 구성해주었다.

프롬프트 작성 과정
일단 1000개라는 양의 쿼리를 한번에 바꾸기보다는, 프롬프트를 점차적으로 완성하는 방식으로 진행했다.
간단한 쿼리부터 점차적으로 변환을 시켜보고, Copilot 이 놓치거나 틀린 부분을 발견하면 특정 규칙을 agent-prompt.md 파일에 추가하는 방식으로 반복했다. (기억에 남는 내용들은 아래와 같았다.)
- OUTER JOIN (+) 변환 누락
- GROUP BY가 ONLY_FULL_GROUP_BY에서 터지게 변환됨
- 긴 WHERE 조건 일부 누락
agent-prompt.md 커스터마이징
- Alias 규칙 고정
a,b,c 금지. 테이블명 그대로 alias 사용
-> 쿼리만 봐도 어떤 테이블 컬럼인지 즉시 추적 가능해서 디버깅과 리뷰 시간이 줄어듦. 조인 늘어날수록 실수도 줄고, 팀 내 스타일도 자동으로 통일할 수 있었다. - CUBRID Outer Join(+) ANSI JOIN으로 강제 변환
(+)는 WHERE에 섞여 있어 조건 누락/방향 실수가 자주 발생하다고 판단
-> LEFT/RIGHT JOIN으로 명시하면 조인 의도가 드러나고, ON/WHERE 분리가 되어 유지보수와 성능 튜닝과 인덱스 확인이 쉬워졌다. - DB 힌트 제거
MySQL에서 의미 없는 힌트는 혼란만 준다고 판단
-> CUBRID 힌트가 남아 있으면 무시되거나 오해를 유발할 수 있었다. 실행계획 분석 시 방해만 되니, MySQL 기준으로 깔끔한 쿼리만 남기는 게 안전하독 생각하였다. - ONLY_FULL_GROUP_BY 대응은 원칙적으로 하도록
ANY_VALUE() 금지(정합성 보장 불가). ROW_NUMBER() + rn=1 패턴 사용
-> ANY_VALUE는 같은 그룹에서도 어떤 행이 선택될지 보장되지 않아 장애/데이터 불일치로 이어질 수 있기때문에, ROW_NUMBER로 대표 행 선택 기준을 고정하여 결과가 항상 동일해져 테스트가 용이해졌다. - 예약어 컬럼 처리
user, level 같은 애들 안전하게 백틱 처리
-> 환경과 버전에 따라 예약어 해석이 달라지면 배포 후 갑자기 쿼리가 깨질 수 있다고 판단하였다. 초기부터 백틱으로 고정해두어 신규 컬럼 추가 시에도 병목이 없도록 설정해주었다. - 재귀 쿼리 변환: CONNECT BY → WITH RECURSIVE
DISTINCT 금지 제약 인지 , 임의 depth 제한 X
-> MySQL 재귀 CTE는 제약이 있어서 임의 depth 제한은 데이터 누락을 만들기 쉬울 수 있다. 따라서 중복 제거가 필요하면 재귀 밖에서 처리해 원본 계층 구조를 그대로 살려주었다.

CUBRID -> MySQL 8 쿼리 변환 일부






이렇게 변환 쿼리들은 무조건 실제 DB에서 실행 하여서 오류 발생을 예방하였다. 또한 긴 쿼리 분할은 AI가 한번에 처리하도록 하기보다는, 여러 개의 작은 단위로 나누어 변환을 요청했을 때 더 안정적이고 정확한 결과를 얻을 수 있었다.
마치며
최근에 AI가 빠른 속도로 발전하면서, AI를 활용한 문서화나 자동화에 관심이 많이 생기게 되었다. 그중에서도 Claude 4는 이런 부분에서 선두에 있다고 개인적으로 생각한다. 물론 agent-prompt.md 를 반복적으로 커스텀한 이유처럼, AI한테 맡기되 맹신하기 보다는 결정적인 책임은 사람이 무조건 해야한다고 생각하고 있다.
아무리 agent-prompt.md 를 명확하게 작성하여 지시한다고 하여도, 결과물에 대한 꼼꼼한 검증이 필수적이라고 생각하였다. 이번 글에서는 방대한 양의 쿼리를 처리해야했기에 더더욱
참고한 자료
# MySQL: ONLY_FULL_GROUP_BY / GROUP BY 처리
https://dev.mysql.com/doc/refman/8.3/en/group-by-handling.html
https://dev.mysql.com/doc/en/sql-mode.html
# MySQL: Window Function (ROW_NUMBER 등)
https://dev.mysql.com/doc/refman/8.0/en/window-functions-usage.html
# MySQL: WITH / WITH RECURSIVE (CTE)
https://dev.mysql.com/doc/refman/en/with.html