본문으로 건너뛰기

AI 코드 리뷰를 기다리지 않아도 되도록 만들기

Published2026.11.04
Read Time17 min read
Related Project: CodeMate

AI 코드 리뷰를 기다리지 않아도 되도록 만들기

CodeMate는 GitHub의 Pull Request(PR)를 가져와 변경된 코드를 함께 살펴보고, AI에게 코드 리뷰를 요청할 수 있는 서비스다. 사용자가 리뷰 버튼을 누르면 서버는 GitHub에서 변경 파일을 가져와 분석할 내용을 만들고, AI의 응답에서 코드 이슈와 점수를 추출해 저장한다.

문제는 이 과정이 버튼을 누르는 순간 끝나는 작업이 아니라는 점이었다. GitHub에서 파일을 가져오고 AI가 코드를 분석하는 동안 HTTP 요청을 열어 둔다면, 사용자는 응답이 올 때까지 로딩 상태를 보게 된다. 요청이 접수된 것인지, 화면을 벗어나도 되는지도 응답 전에는 알기 어렵다.

리뷰 결과가 바로 나오지 않더라도 요청이 접수됐다는 응답은 먼저 줄 수 있다. CodeMate에서는 리뷰 요청에 답하는 일과 실제 분석을 마치는 일을 분리했다. 사용자는 요청 후 다른 작업을 하다가 결과를 확인할 수 있다.

요청을 접수하면 먼저 응답하기

AI 분석을 하나의 API 요청 안에서 끝내려고 하면, 서버는 GitHub 조회와 AI 호출, 결과 저장이 모두 끝난 뒤에야 응답을 반환한다. 예를 들어 다음과 같이 분석 함수를 await한다면 브라우저는 분석이 끝날 때까지 요청의 완료를 기다린다.

// 분석이 모두 끝나야 응답이 반환되는 방식
const result = await analyzeReview(pr.id, session.user.id);
return NextResponse.json({ status: result.status });

CodeMate의 요청 API는 인증과 PR 접근 권한을 확인한 뒤 { status: "PENDING" }을 반환한다. 이 값은 분석 결과가 아니라 후속 작업을 등록했다는 응답이다. 이 시점에 Review 기록이나 알림이 저장됐다고 보장할 수는 없다. 둘 다 응답 이후의 작업에서 처리한다.

AI 리뷰 요청에 응답한 뒤 서버에서 분석과 알림 갱신을 이어가는 흐름

브라우저는 이 응답을 받으면 분석이 끝날 때까지 같은 요청을 기다리지 않는다. 사용자가 다른 PR을 살펴보는 동안에도 서버는 분석을 이어간다. 이제 남은 문제는 응답을 보낸 뒤 작업을 어떻게 실행하고, 그 결과를 어디에서 확인하게 할 것인가였다.

after()로 응답 이후에도 분석 이어가기

여기서 사용한 기능이 Next.js의 after()다. after()는 Route Handler 등에서 응답이 완료된 이후 실행할 콜백을 등록하는 함수다. 로깅이나 분석처럼 응답을 지연시킬 필요가 없는 후속 작업에 사용할 수 있다.

일반적인 await analyzeReview()는 분석이 끝나야 다음 줄로 진행한다. after()에는 응답 이후 실행할 콜백을 등록하고 바로 응답을 반환할 수 있다. 지원되는 실행 환경에서는 Next.js가 콜백의 비동기 작업을 기다릴 수 있도록 요청 실행 수명을 이어간다.

다만 분석을 별도 서버나 영구적인 작업 큐로 옮기는 것은 아니다. 콜백은 여전히 해당 서버의 실행 시간과 배포 환경에 영향을 받는다.

리뷰 요청 API에서 실행 순서를 보여 주는 부분을 발췌했다. 인증과 PR 조회는 이 코드 앞에서 끝나며, 오류 처리와 대시보드 캐시 갱신은 여기서 생략했다.

// 응답을 보낸 다음 실행할 후속 작업을 등록한다.
after(async () => {
  // 리뷰 요청이 접수되었음을 알림에 반영한다.
  await notifyReviewStatus({
    repositoryId: pr.repoId,
    prId: pr.id,
    prTitle: pr.title,
    prNumber: pr.number,
    status: "PENDING",
  });

  // GitHub 변경 파일 조회부터 AI 분석과 결과 저장까지 수행한다.
  const result = await analyzeReview(pr.id, session.user.id);

  // 같은 PR의 분석이 이미 진행 중이라면 이 요청은 더 처리하지 않는다.
  if (result.status === "SKIPPED_ACTIVE") return;

  // 분석 결과에 따라 완료 또는 실패 알림으로 갱신한다.
  await notifyReviewStatus({
    repositoryId: pr.repoId,
    prId: pr.id,
    prTitle: pr.title,
    prNumber: pr.number,
    status: result.status,
  });
});

// AI 분석 완료를 기다리지 않고 요청 접수 여부부터 응답한다.
return NextResponse.json({ status: "PENDING" });

코드를 보면 after() 안에서 알림을 먼저 갱신하고, analyzeReview()를 실행한 다음 최종 상태를 다시 알림에 반영한다. 하지만 브라우저는 이 모든 과정이 끝날 때까지 기다리지 않는다. 마지막 줄의 PENDING 응답을 받으면 해당 HTTP 요청은 끝난다.

그 뒤 서버에서는 먼저 PENDING 알림을 만들거나 갱신하고, analyzeReview()를 호출한다. 이 함수는 새 Review를 만든 다음 GitHub 변경 파일 조회, AI 분석, 결과 저장을 진행한다. 정상적으로 끝나면 COMPLETED, 처리 중 예외가 발생하면 FAILED를 반환한다. 이미 Review가 만들어진 뒤의 실패라면 실패 상태와 이유도 그 기록에 남긴다. 마지막으로 반환된 상태에 맞춰 알림을 갱신한다.

따라서 API의 PENDING 응답을 분석 성공으로 읽어서는 안 된다. 응답 후 GitHub 조회나 AI 호출이 실패할 수 있고, Review를 만들기 전에 실패하면 남길 리뷰 기록 자체가 없을 수도 있다. 이 경우에도 함수는 FAILED를 반환해 실패 알림 갱신을 시도한다.

또한 분석 시작 경로가 리뷰 버튼만 있는 것도 아니다. GitHub Webhook으로 PR이 열리거나 변경됐을 때도 분석을 시작할 수 있다. 이때도 Webhook에 대한 응답을 먼저 반환하고 after()에서 후속 작업을 진행하며, 실제 AI 리뷰 로직은 같은 analyzeReview() 함수로 이어진다.

같은 PR을 두 번 분석하지 않으려면

분석을 요청하는 경로가 둘이므로 중복 실행도 고려해야 했다. 사용자가 리뷰 버튼을 연달아 누르거나 비슷한 시점에 Webhook이 들어오면, 동일한 PR에 대한 분석 요청이 겹칠 수 있다. 실제로 이런 중복으로 잘못된 결과가 발생했다는 뜻은 아니다. 코드가 막으려는 동시 실행 상황이다.

AI 리뷰는 GitHub의 변경 파일을 조회하고 외부 AI API를 호출하는 작업이다. 같은 분석이 여러 번 실행되면 불필요한 호출이 발생할 뿐 아니라, 늦게 끝난 분석이 다른 결과나 알림에 영향을 줄 여지도 있다. 버튼을 비활성화하는 것만으로는 다른 브라우저 탭이나 Webhook에서 발생한 요청까지 제어할 수 없다.

이를 막기 위해 analyzeReview()는 분석을 시작하기 전에 해당 PR에 이미 진행 중인 리뷰가 있는지 확인한다. 진행 중인 리뷰가 없다면 새 Review를 생성한다. 하지만 이 확인만으로는 충분하지 않다. 서로 다른 두 요청이 거의 동시에 조회하면 둘 다 '진행 중인 리뷰가 없다'고 판단할 수 있기 때문이다.

그래서 DB에도 한 PR에 진행 중인 리뷰는 하나만 존재하도록 제약을 두었다.

-- 아직 처리 중인 리뷰에 한해서 PR별 중복 생성을 막는다.
CREATE UNIQUE INDEX "review_active_unique"
  ON "Review" ("pullRequestId")
  WHERE status IN ('PENDING', 'IN_PROGRESS');

이것은 일반적인 고유 인덱스가 아니라 조건을 가진 **부분 고유 인덱스(Partial Unique Index)**다. 동일한 pullRequestId라도 상태가 PENDING이나 IN_PROGRESS인 리뷰는 한 개만 허용한다. 반면 이미 COMPLETED 또는 FAILED가 된 리뷰까지 제한하지는 않는다. 기존 리뷰 이력을 남겨 두면서, 나중에 같은 PR을 다시 분석할 수 있도록 하기 위해서다.

예를 들어 두 요청이 거의 동시에 조회해 둘 다 진행 중인 리뷰가 없다고 판단하더라도, 레코드를 만들 때는 DB의 고유 제약이 적용된다. 하나가 먼저 생성되면 다른 하나는 충돌한다. CodeMate는 이 충돌을 분석 중인 작업이 있다는 신호로 처리해 SKIPPED_ACTIVE를 반환한다. after() 코드에서도 이 상태라면 완료·실패 알림 갱신으로 넘어가지 않는다.

다만 SKIPPED_ACTIVE를 확인하는 시점은 PENDING 알림을 갱신한 뒤다. 이미 진행 중인 리뷰에 중복 요청이 들어오면 분석은 건너뛰지만, 알림 문구는 다시 ‘진행 중’으로 바뀔 수 있다. 이 요청에서는 최종 알림 갱신도 건너뛰므로, 다른 실행과 알림 갱신 시점에 따라 문구가 실제 리뷰 결과보다 뒤처질 가능성이 남는다. 중복 분석을 막는 DB 제약과 알림의 정합성은 별개의 문제다.

분석이 끝나면 알림에서 결과 확인하기

요청을 빠르게 끝냈다면, 이제 사용자는 분석이 언제 끝났는지 알 수 있어야 한다. CodeMate에서는 분석 상태를 별도 알림으로 연결했다.

분석이 진행되는 동안에는 해당 PR에 대한 요청 접수 알림을 처리하고, 분석이 끝난 후에는 그 알림을 완료 또는 실패 상태에 맞게 갱신한다. 알림을 허용한 사용자에 대해 같은 PR의 AI 리뷰 알림이 이미 있으면 제목과 메시지를 바꾸고 다시 읽지 않은 상태로 만든다. 기존 알림이 없으면 새로 생성한다. 따라서 분석 과정에서 상태가 달라질 때마다 똑같은 알림을 계속 추가하는 방식은 아니다.

분석에 성공하면 저장된 리뷰 요약과 코드 이슈를 확인할 수 있다. 실패 이유는 Review가 생성된 뒤 실패했을 때 그 기록에 저장된다. 알림 갱신 자체도 실패할 수 있고, DB에서 갱신됐더라도 그 순간 브라우저 화면에 나타나지는 않는다. 현재 알림 목록은 주기적으로 다시 조회한다.

CodeMate에서 완료된 AI 리뷰의 요약과 코드 이슈를 확인하는 화면

사용자는 다른 작업을 하다가 알림 목록에서 완료 여부를 확인하고, 해당 PR로 돌아가 리뷰 결과를 살펴볼 수 있다. 첫 응답은 작업 등록을 알려 주고, 실제 결과는 서버에 저장된 뒤 조회한다. 알림이 언제 화면에 반영되는지와 진행 단계가 어떻게 표시되는지는 별도의 글에서 다룬다.

after()를 사용할 때 남는 한계

이번 구현에서는 after() 덕분에 AI 분석이 끝날 때까지 HTTP 응답을 붙잡아 둘 필요가 없어졌다. 다만 응답을 먼저 반환한다고 해서 이후 작업의 실행이 무조건 보장되는 것은 아니다.

Next.js 공식 문서에 따르면 after()는 라우트에 설정된 최대 실행 시간의 영향을 받는다. CodeMate의 요청 API에는 maxDuration = 300이 있지만, 이는 300초 안에 분석이 끝난다는 보장이 아니다. 배포 환경이 이 설정을 지원하는지도 확인해야 한다.

또한 after()는 작업을 영구 저장하거나 실패한 작업을 자동으로 다시 실행하는 기능을 제공하지 않는다. 서버 실행이 중단되거나 허용 시간을 넘겨 분석이 끝나지 못하는 상황까지 안정적으로 처리하려면, 작업 상태를 별도로 관리하고 재시도할 수 있는 큐나 워커 구조를 고려해야 한다. 특히 요청 접수 응답을 보냈지만 실제 분석이 시작되지 못한 경우까지 처리하려면 지금보다 명확한 작업 보장 방식이 필요하다.

현재 구현은 HTTP 응답을 기다리는 시간과 실제 분석 시간을 분리해 준다. 대신 응답 직후의 성공 표시가 분석 완료를 뜻하지 않으며, 서버 작업의 재시도와 중복 요청 시 알림 정합성도 보장하지 않는다. 사용자가 오래 열린 요청을 기다리지 않도록 한 다음, 결과를 다시 확인할 수 있게 연결한 것까지가 이번 구현의 범위다.