CodeRabbit으로 코드 속 예상 SQL을 정리하고 DBA 검토를 요청한 이유

정기창·

코드 리뷰는 통과했는데, 데이터베이스가 받게 될 요청은 충분히 검토하지 못할 수 있습니다. 저는 이 간격을 줄이려고 CodeRabbit에 예상 SQL과 확인할 지점을 정리하고, DBA의 검토를 요청하도록 설정했습니다.

DBA에게 코드만 건네면 설명이 다시 필요했습니다

서비스의 데이터베이스 구조를 바꾸거나 조회 방식을 수정할 때는 DBA, 즉 데이터베이스 관리자의 검토가 필요했습니다. 잘못된 변경이 배포되면 조회가 느려지거나, 다른 작업이 잠금 해제를 기다리면서 서비스 응답에 영향을 줄 수 있기 때문입니다.

테이블을 만드는 SQL은 비교적 직접적으로 읽을 수 있습니다. 반면 애플리케이션 안의 조회 코드는 사정이 다릅니다. 검색 조건이 있을 때만 테이블을 연결하고, 특정 기능에서만 잠금을 걸고, 호출하는 쪽에 따라 기간 조건을 생략하기도 합니다. ORM이나 쿼리 빌더를 사용하면 실제 SQL이 여러 메서드 호출 뒤에 숨어 있기도 합니다.

DBA가 코드를 읽지 못한다는 뜻은 아닙니다. DB 영향을 판단하기 전에, 애플리케이션의 분기와 호출 맥락부터 다시 해석해야 한다는 점이 문제였습니다. 개발자가 이미 알고 있는 조건을 DBA가 같은 코드에서 다시 찾아야 했습니다.

그래서 AI 코드 리뷰에 역할을 하나 더 주기로 했습니다. 코드의 문제점을 지적하는 데서 끝내지 않고, 어떤 SQL이 만들어질 수 있는지와 DB 관점에서 무엇을 확인해야 하는지를 함께 정리하도록 했습니다.

실제로 설정한 것은 SQL 검토 지침과 담당자 검토 요청입니다

CodeRabbit은 코드 변경 요청(PR)을 검토하는 도구입니다. 경로별 검토 지침을 이용하면 특정 파일에 어떤 관점의 검토를 적용할지 정할 수 있습니다. 저는 데이터 조회 코드에 실행될 SQL의 유추, 조회 조건과 테이블 연결, 결과 개수 제한, 인덱스와 성능 영향 검토를 요청했습니다. 이어서 DB 담당자의 추가 검토가 필요하다는 멘션을 남기도록 지시했습니다.

당시 설정의 의도를 짧게 옮기면 아래와 같습니다. 실제 저장소 경로와 담당자 식별자는 제거한 요약이며, 특정 회사의 설정 파일을 그대로 공개한 것은 아닙니다.

reviews:
  path_instructions:
    - path: "src/data/**/*.ts"
      instructions: |
        변경된 코드에서 실행될 SQL을 유추해 작성하세요.
        WHERE, JOIN, LIMIT 조건을 확인하세요.
        인덱스와 성능 영향, 최적화 검토 지점을 정리하세요.
        DB 담당자의 추가 리뷰가 필요하다는 멘션을 남기세요.

위 경로는 설명용입니다. 실제 적용 시에는 SQL을 구성하는 파일에 맞추고, 마지막 문장에는 팀에서 사용하는 담당자 계정을 지정해야 합니다. 이 지침은 AI 리뷰에 무엇을 요청할지를 정합니다. 담당자 자동 배정이나 승인 전 병합 차단을 설정하는 기능과는 구분해야 합니다.

이 글에서 경험으로 다루는 범위는 이러한 검토 지침을 도입하고 설정한 일입니다. 아래 코드는 그 의도를 설명하려고 새로 만든 예시이며, 실제 회사 코드나 당시 CodeRabbit의 리뷰 출력은 아닙니다.

같은 함수라도 조건에 따라 다른 SQL이 나옵니다

가상의 주문 조회를 예로 들어 보겠습니다. 상품 조건이 있으면 주문 상품 테이블을 연결하고, 시작일이 있으면 날짜 조건을 추가합니다. 이후 갱신을 준비하는 호출에서는 잠금 옵션도 켤 수 있다고 가정합니다.

아래 JavaScript는 SQL 문자열과 입력값을 만들어 출력하는 검토용 예제입니다. Node.js에서 추가 라이브러리 없이 실행할 수 있으며, 데이터베이스에 연결하지 않습니다. SQL은 MySQL 8.4를 설명 기준으로 삼았습니다. 의도적으로 검토할 지점을 남긴 코드이므로 서비스에 바로 적용할 구현은 아닙니다.

function buildOrderQuery({ shopId, from, sku, lock = false }) {
  const values = [shopId];
  let sql = "SELECT o.id, o.total FROM orders o";

  if (sku) {
    sql += " JOIN order_items i ON i.order_id = o.id";
  }

  sql += " WHERE o.shop_id = ?";

  if (from) {
    sql += " AND DATE(o.created_at) >= ?";
    values.push(from);
  }
  if (sku) {
    sql += " AND i.sku = ?";
    values.push(sku);
  }

  sql += " ORDER BY o.created_at DESC, o.id DESC LIMIT 100";
  if (lock) sql += " FOR UPDATE";

  return { sql, values };
}

const query = buildOrderQuery({
  shopId: "DEMO-SHOP",
  from: "2026-01-01",
  sku: "DEMO-SKU",
  lock: true,
});
console.log(query.sql);
console.log(query.values);

입력값은 SQL 문자열에 직접 붙이지 않고 ? 자리와 별도 배열로 나눴습니다. 실제 애플리케이션에서는 해당 드라이버의 파라미터 바인딩 API로 전달해야 합니다. 위 예제는 그 직전의 구성을 보여 줍니다.

호출 조건SQL에서 달라지는 부분리뷰할 질문
상품 조건 없음주문 테이블만 조회상품 필터 없는 조회가 의도한 동작인가?
상품 조건 있음상품 테이블 JOIN 추가주문이 중복해서 나오는가?
시작일 있음날짜 함수와 필터 추가어떤 인덱스로 범위를 읽는가?
잠금 옵션 있음FOR UPDATE 추가어디서 잠금을 해제하는가?

마지막 호출처럼 세 옵션을 모두 켜면 다음 SQL이 만들어집니다. 여기서는 짧은 함수라 직접 확인할 수 있지만, 여러 파일과 메서드에 나뉜 코드라면 이런 형태로 정리하는 과정 자체가 검토 준비 작업이 됩니다.

SELECT o.id, o.total
FROM orders o
JOIN order_items i ON i.order_id = o.id
WHERE o.shop_id = ?
  AND DATE(o.created_at) >= ?
  AND i.sku = ?
ORDER BY o.created_at DESC, o.id DESC
LIMIT 100
FOR UPDATE;

?에 바인딩할 가상 값은 순서대로 DEMO-SHOP, 2026-01-01, DEMO-SKU입니다. 위 SQL은 드라이버에 전달할 템플릿이며, 그대로 SQL 콘솔에 붙여 실행하는 문장은 아닙니다.

“느릴 수 있습니다”보다 구체적인 질문이 필요합니다

SQL 한 개를 보여 주는 것만으로 검토가 끝나지는 않습니다. 조건이 달라지면 SQL도 달라지므로, 어떤 호출 조건을 가정했는지 함께 남겨야 합니다. 이 가상 예제에서 검토할 질문은 세 가지입니다.

날짜를 잘라 비교할 필요가 있는가?

DATE(o.created_at)는 시각에서 날짜 부분을 취해 비교합니다. 이 표현이 일반적인 created_at 인덱스의 범위 탐색에 어떤 영향을 주는지 확인해야 합니다. 단지 함수가 보인다는 이유로 “인덱스를 전혀 쓰지 못한다”거나 “반드시 느리다”고 결론낼 수는 없습니다.

요구사항이 특정 시점 이후의 조회라면 컬럼을 그대로 두고 o.created_at >= ?로 비교하는 방안을 검토할 수 있습니다. 다만 날짜의 기준 시간대와 경계를 먼저 맞춰야 합니다. MySQL의 범위 최적화 설명을 참고하되, 실제 인덱스 구성과 실행 계획으로 비교해야 합니다.

100개는 주문 100개인가, 연결된 행 100개인가?

가상의 데이터에서 한 주문에 같은 상품의 항목이 두 줄 있다면, JOIN 결과에는 같은 주문이 두 번 나올 수 있습니다. 이때 LIMIT 100은 서로 다른 주문 100개를 보장하지 않습니다. 결과 개수 제한이 있다고 해서 원하는 단위로 제한되는 것은 아닙니다.

상품의 존재 여부만 필요한 기능이라면 EXISTS로 바꿀 수 있는지 검토할 만합니다. 하지만 주문 상품별 정보를 반환해야 한다면 요구사항이 다릅니다. AI가 중복 가능성을 짚더라도, 어떤 결과가 맞는지는 기능을 아는 사람이 결정해야 합니다.

잠금은 어느 작업을 보호하고, 얼마나 오래 유지되는가?

FOR UPDATE가 있다면 호출부에서 같은 연결의 트랜잭션을 시작하는지, 어떤 갱신을 보호하려는지, 언제 커밋하거나 롤백하는지까지 봐야 합니다. 이 함수는 SQL만 만들기 때문에 그 정보를 자체적으로 알려 주지 않습니다.

MySQL InnoDB의 잠금 읽기는 트랜잭션 맥락에서 이해해야 하며, 잠금은 커밋이나 롤백 시 해제됩니다. 실제 잠금 범위와 대기는 실행 계획·격리 수준·다른 작업의 접근 방식에 따라 확인해야 합니다. 결과가 최대 100행이라는 이유로 잠금 영향까지 작다고 판단하면 안 됩니다. MySQL 잠금 읽기 공식 문서에서 이 동작을 확인할 수 있습니다.

예상 SQL과 실제 SQL을 대조하는 단계가 필요합니다

CodeRabbit이 코드에서 유추한 SQL과, 프로그램이 실제 드라이버에 전달하는 SQL은 다를 수 있습니다. ORM이 넣는 기본 조건이나 실행 시점의 옵션을 놓칠 수도 있습니다. 그래서 AI의 결과는 개발자가 실제 동작과 대조할 초안으로 취급하는 편이 적절합니다.

앞의 설정을 바탕으로 검토 흐름을 보완한다면, 다음처럼 역할을 나눌 수 있습니다. 아래 도식은 설명용 절차이며 당시의 개별 승인 기록을 재현한 것은 아닙니다.

개발자: 코드 변경과 기능 조건 설명
    ↓
CodeRabbit: 조건별 예상 SQL · 근거 위치 · 확인할 질문 정리
    ↓
개발자: 테스트 환경의 실제 SQL · 바인딩과 대조
    ↓
DBA: 실행 계획 · 데이터 분포 · 인덱스 · 잠금 영향 검토
    ↓
개발자: 수정 · 재검증 → 팀의 승인 절차에 따라 반영

리뷰 요청도 “이 쿼리가 괜찮은가요?”보다 아래처럼 쓰는 편이 판단에 도움이 됩니다. 이것 역시 가상 예시입니다.

상품 조건이 있는 주문 조회에 JOIN이 추가됩니다. 조회 결과는 서로 다른 주문을 기준으로 해야 합니다. 같은 상품 항목이 한 주문에 여러 줄 있는 경우를 포함해 결과 개수를 확인하려고 합니다.

기간 조건에는 DATE()가 있고, 일부 호출은 FOR UPDATE를 사용합니다. 실제 SQL과 바인딩, 현재 인덱스, 대표 조건별 실행 계획을 첨부한 뒤, 날짜 비교 방식과 잠금 범위가 적절한지 검토를 요청드리겠습니다.

첨부 자료에는 최소한 호출 조건, 실제 SQL과 값의 형태, 테이블·인덱스 구조, 데이터 규모와 치우침, 호출 빈도, 트랜잭션 범위가 필요합니다. 고객 값은 익명화하되, 검색 범위나 데이터 분포를 판단할 정보까지 지워 버리지 않도록 구분해야 합니다.

실행 계획은 DB가 어떤 순서와 방법으로 데이터를 찾을지 보여 줍니다. MySQL의 EXPLAIN ANALYZE는 쿼리를 실제로 실행하므로, 검토용 데이터와 실행 범위를 정한 환경에서 확인해야 합니다. 명령만 붙이면 안전하게 검토가 끝나는 것은 아닙니다. EXPLAIN 공식 문서도 두 방식의 차이를 설명합니다.

지금 보완한다면, 불확실한 부분도 출력하게 하겠습니다

당시 지침은 예상 SQL과 성능 검토, 담당자 요청에 초점을 맞췄습니다. 이를 다시 구성한다면 AI가 모르는 조건까지 답을 만들어 내지 않도록, 다음 항목을 추가하고 싶습니다. 아래는 당시 설정의 원문이 아닌 보완안입니다.

SQL이 바뀌는 조건과 근거가 되는 코드 위치를 함께 적습니다.
추정 SQL과 런타임에서 확인한 SQL을 구분합니다.
호출부·스키마·인덱스 정보가 없으면 확인이 필요한 항목으로 남깁니다.
성능 문제를 단정하지 말고 가정과 검증할 질문을 분리합니다.
결과의 의미가 달라지는 수정은 기능 요구사항부터 확인합니다.

AI의 멘션은 사람에게 요청을 남기는 단계입니다. 실제 승인 여부를 보장하려면 팀의 리뷰 절차에서 따로 확인해야 합니다. 특정 DB 변경에 승인이 필수라면 검토자 지정과 병합 조건도 별도로 관리해야 합니다.

돌이켜 생각해보면, 제가 줄이고 싶었던 것은 검토 자체보다 검토를 시작하기까지의 번역 작업이었습니다. 개발자는 코드와 기능 조건을 보고, DBA는 실제 SQL과 데이터베이스의 상태를 봅니다. AI가 그 사이의 설명을 먼저 준비하고, 사람은 그 설명이 맞는지와 변경을 받아들일 수 있는지를 판단하도록 역할을 나누는 데 의미가 있었습니다.

CodeRabbitAI 코드 리뷰SQLDBA개발 경험