| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 |
- spring boot
- ES
- Elasticsearch
- Java
- 프레임워크
- 백준
- 애자일프로그래밍
- 스프링
- database
- 자바
- 코드
- 개발자
- kotlin
- 개발
- 코딩
- 읽기쉬운코드
- Spring
- framework
- 코딩테스트
- cleancode
- 클린코드
- 그리디알고리즘
- Baekjoon
- JPA
- 알고리즘
- API
- 데이터베이스
- 그리디
- 애자일기법
- 엘라스틱서치
- Today
- Total
튼튼발자 개발 성장기🏋️
AI 코드리뷰 자동화 (vLLM + Gitlab + Google Chat) 본문
DMR: Don't Merge without Review
프로젝트 개요
지난 약 8년간 백엔드 개발자로서 현업에서 재직하며 업무를 볼 때 사바사, 팀바팀이 많지만, 결코 개발만을 하지 않았습니다. 생각해보면 기획 내용과 요구사항 분석부터 웹 디자인, 개발 설계, 테스트 코드 작성, Sanity Test, 모니터링, 문서 최신화까지 개발 외 적인 업무도 다량 존재했습니다. 이 중에서도 바로 코드리뷰는 필수적이면서도 적지 않은 시간을 소요하는 작업입니다. 이 코드리뷰를 어떻게 하면 자동화할 수 있을까 고민을 해보았습니다. ai를 활용한다 쳐도 line by line 레벨에서의 리뷰밖에 할 수 없다고 생각했습니다. 즉, 사이드 이펙트와 관련된 이슈나 버그를 리뷰할 수 없다는 치명적인 단점이 존재했습니다. (ai가 각 프로젝트 전체를 학습하고 있지 않는다는 가정 하에.) 그렇다고 프로젝트를 학습 시키자니, 프로젝트가 10개면 10개의 ai가 있어야 하는 셈이 될 거라는 생각에 고민에 빠졌습니다. 그러던 중 아이디어가 떠올랐습니다. "변경된 파일들에 대해서 의존하고 있는 모든 코드의 스니펫을 이용한다면?"이라는 생각을 하게 되었습니다.
이제부터 단순히 Line By Line 코드리뷰가 아닌, 프로젝트 전체적인 관점에서의 코드리뷰 자동화 프로젝트(DMR)를 소개하고자 합니다.
단, 아무리 AI 코드리뷰 자동화 프로젝트라도 분명히 할루시네이션 또는 놓치는 부분이 생길 수 있기 때문에 사람이 직접 코드리뷰를 하는 행위는 동일하게 가져가야합니다.
그래도.. 부담이 덜 된다는 장점이...(?)
DMR (Don't Merge without Review)는 Spring AI를 활용한 AI 기반 코드 리뷰 자동화 시스템입니다. GitLab Webhook을 통해 Merge Request 이벤트를 수신하고, vLLM 또는 OpenAI 호환 API를 사용하여 코드를 분석한 후, 라인별로 상세한 리뷰 댓글을 GitLab에 자동으로 작성합니다.
프로젝트 목적
- 개발 생산성 향상: 반복적이고 기계적인 코드 리뷰를 자동화
- 코드 품질 개선: AI가 잠재적 버그, 성능 이슈, 보안 취약점 등을 사전에 발견
- 의존성 분석: 변경된 코드가 다른 파일에 미치는 영향을 자동으로 분석
- 빠른 피드백: Merge Request 생성 즉시 자동 리뷰 시작
주요 기능
1. 라인별 상세 리뷰
- 변경된 각 코드 라인에 대해 구체적인 리뷰 댓글 작성
- 심각도 레벨별 분류 (CRITICAL, WARNING, SUGGESTION, INFO)
- GitLab Discussion 형태로 직접 코드 라인에 댓글 생성

2. 의존성 분석
- 변경된 파일에서 함수, 클래스, 인터페이스 등의 심볼 추출
- GitLab Code Search API를 통해 심볼의 사용처 검색
- Kotlin 및 Java 코드 지원
3. 파일간 영향도 분석 (사이드이펙트)
- 변경이 다른 파일에 미치는 영향도 평가 (CRITICAL, HIGH, MEDIUM, LOW)
- Breaking Change 자동 감지
- 영향을 받는 파일 및 사용 횟수 통계
4. 프롬프팅 전략
- 의존성 분석 결과를 포함한 컨텍스트 기반의 프롬프팅
- 실제 코드 스니펫을 포함하여 AI가 구체적으로 분석 가능
- 구조화된 JSON 응답 형식으로 파싱 용이성 향상
5. Google Chat 알림
- 리뷰 완료 시 Google Chat Webhook으로 알림 전송
- 리뷰 결과 요약 (댓글 수, 변경 파일 수, 코드 변경량 등)
- 오류 발생 시 즉시 알림

6. 견고한 에러 처리
- Spring Retry를 활용한 자동 재시도 메커니즘
- 불완전한 JSON 응답 자동 수정 및 파싱
- 비동기 처리로 Webhook 응답 속도 보장
기술 스택
Core Framework
- Spring Boot 3.5.7
- Kotlin 2.1.0
- Java 21
Spring AI Integration
Spring AI 사용 방식
이 프로젝트는 Spring AI 1.0.0-M5를 사용하여 AI 모델과 통합합니다.
// build.gradle.kts
extra["springAiVersion"] = "1.0.0-M5"
dependencies {
implementation("org.springframework.ai:spring-ai-openai-spring-boot-starter")
}
dependencyManagement {
imports {
mavenBom("org.springframework.ai:spring-ai-bom:${property("springAiVersion")}")
}
}
Spring AI 주요 컴포넌트
- OpenAiApi: OpenAI 호환 API와 통신하는 핵심 클라이언트
- vLLM, OpenAI, Azure OpenAI 등 OpenAI 호환 API 지원
- RestClient 및 WebClient 기반 HTTP 통신
- 커스텀 타임아웃 및 연결 설정 지원 (vLLM 서버의 경우 상태에 따라 간혹 타임아웃이 발생하긴 함...)
- ChatClient: Spring AI의 고수준 채팅 인터페이스
- Fluent API를 통한 직관적인 프롬프트 작성
- System/User 메시지 구분 지원
- 옵션 기반 모델 파라미터 설정
- OpenAiChatOptions: AI 모델 실행 옵션 설정
- temperature, max-tokens, top-p 등 파라미터 제어
vLLM/OpenAI 호환 API 통합 방식
@Configuration
class SpringAiConfig {
@Bean
@ConditionalOnMissingBean
fun openAiApi(
@Value("\${spring.ai.openai.base-url}") baseUrl: String,
@Value("\${spring.ai.openai.api-key}") apiKey: String,
@Value("\${spring.ai.openai.timeout.connect:10}") connectTimeout: Long,
@Value("\${spring.ai.openai.timeout.read:300}") readTimeout: Long
): OpenAiApi {
// RestClient 설정 (동기 처리)
val requestFactory = SimpleClientHttpRequestFactory().apply {
setConnectTimeout(Duration.ofSeconds(connectTimeout))
setReadTimeout(Duration.ofSeconds(readTimeout))
}
val restClientBuilder = RestClient.builder()
.baseUrl(baseUrl)
.defaultHeader("Authorization", "Bearer $apiKey")
.requestFactory(requestFactory)
// WebClient 설정 (비동기 처리)
val httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (connectTimeout * 1000).toInt())
.doOnConnected { conn ->
conn.addHandlerLast(ReadTimeoutHandler(readTimeout, TimeUnit.SECONDS))
conn.addHandlerLast(WriteTimeoutHandler(readTimeout, TimeUnit.SECONDS))
}
.responseTimeout(Duration.ofSeconds(readTimeout))
val webClientBuilder = WebClient.builder()
.baseUrl(baseUrl)
.defaultHeader("Authorization", "Bearer $apiKey")
.clientConnector(ReactorClientHttpConnector(httpClient))
// OpenAI 호환 API 생성
return OpenAiApi(baseUrl, apiKey, restClientBuilder, webClientBuilder)
}
@Bean
fun chatClient(chatModel: ChatModel, defaultChatOptions: OpenAiChatOptions): ChatClient {
return ChatClient.builder(chatModel)
.defaultOptions(defaultChatOptions)
.build()
}
@Bean
fun defaultChatOptions(
@Value("\${spring.ai.openai.chat.options.temperature:0.3}") temperature: Double,
@Value("\${spring.ai.openai.chat.options.max-tokens:4000}") maxTokens: Int,
@Value("\${spring.ai.openai.chat.options.top-p:0.95}") topP: Double
): OpenAiChatOptions {
return OpenAiChatOptions.builder()
.temperature(temperature)
.maxTokens(maxTokens)
.topP(topP)
.build()
}
}
핵심 포인트:
- Spring AI의
OpenAiApi는 OpenAI 호환 API를 추상화하여 vLLM, Ollama 등 다양한 LLM 서버와 호환 base-url만 변경하면 다른 AI 제공자로 쉽게 전환 가능- 동기(RestClient) 및 비동기(WebClient) 통신 모두 지원
- 타임아웃, 재시도 등 프로덕션 환경에 적합한 견고한 설정
AI 호출 예시
@Service
class LineByLineReviewService(
private val chatClient: ChatClient,
private val lineReviewChatOptions: OpenAiChatOptions
) {
private suspend fun callAiForLineReview(prompt: String): String {
return chatClient.prompt()
.system("You are a code review assistant. You MUST respond ONLY with valid JSON.")
.user(prompt)
.options(lineReviewChatOptions)
.call()
.content() ?: ""
}
}
비동기 처리
- Kotlin Coroutines: 효율적인 비동기 처리
- WebFlux: 논블로킹 HTTP 클라이언트
- Reactor Netty: 고성능 네트워크 통신
기타 라이브러리
- Jackson: JSON 직렬화/역직렬화
- Spring Retry: 자동 재시도 메커니즘
- Kotlin Logging: 구조화된 로깅
- Micrometer Prometheus: 메트릭 및 모니터링
아키텍처
전체 구조
┌─────────────┐ Webhook ┌──────────────────┐
│ GitLab │ ───────────────────> │ DMR Application │
│ │ │ │
│ Merge │ │ ┌────────────┐ │
│ Request │ │ │ Controller │ │
└─────────────┘ │ └────────────┘ │
│ ↓ │
┌─────────────┐ │ ┌────────────┐ │
│ vLLM/ │ <────────────────┐───│ │ Service │ │
│ OpenAI API │ AI Request │ │ │ Layer │ │
└─────────────┘ │ │ └────────────┘ │
│ │ ↓ │
┌─────────────┐ │ │ ┌────────────┐ │
│ Google Chat │ <────────────────┘ │ │ Analyzer │ │
│ Webhook │ Notification │ │ Layer │ │
└─────────────┘ │ └────────────┘ │
│ ↓ │
│ ┌────────────┐ │
│ │ GitLab API │ │
│ │ Client │ │
│ └────────────┘ │
└──────────────────┘
↓
┌──────────────────┐
│ GitLab API │
│ (Search, Comment)│
└──────────────────┘
계층별 구조
┌─────────────────────────────────────────────────────────┐
│ Controller Layer │
│ - GitLabWebhookController: Webhook 수신 및 검증 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Service Layer │
│ - CodeReviewService: 전체 리뷰 프로세스 조율 │
│ - LineByLineReviewService: 라인별 리뷰 수행 │
│ - ContextBuilderService: 리뷰 컨텍스트 구성 │
│ - GoogleChatNotifier: 알림 전송 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Analyzer Layer │
│ - DependencyAnalyzer: 심볼 추출 및 의존성 검색 │
│ - CrossFileImpactAnalyzer: 파일간 영향도 분석 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Client Layer │
│ - GitLabApiClient: GitLab API 호출 │
│ - GoogleChatClient: Google Chat Webhook 호출 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Parser Layer │
│ - DiffParser: Git Diff 파싱 │
│ - JsonResponseParser: AI 응답 JSON 파싱 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Prompt Layer │
│ - PromptTemplateService: AI 프롬프트 생성 │
└─────────────────────────────────────────────────────────┘
주요 컴포넌트
Controller Layer
GitLabWebhookController
GitLab Webhook 이벤트를 수신하고 검증합니다.
역할:
- GitLab에서 전송되는 Merge Request 이벤트 수신
- Webhook Secret Token 검증
- 이벤트 타입 검증 (Merge Request 이벤트만 처리)
- 비동기 리뷰 프로세스 시작
@RestController
@RequestMapping("/api/webhook")
class GitLabWebhookController(
private val codeReviewService: CodeReviewService,
private val gitLabProperties: GitLabProperties
) {
@PostMapping("/gitlab")
fun handleGitLabWebhook(
@Valid @RequestBody event: MergeRequestEvent,
@RequestHeader("X-Gitlab-Event", required = false) gitlabEvent: String?,
@RequestHeader("X-Gitlab-Token", required = false) gitlabToken: String?
): ResponseEntity<WebhookResponse> {
validateWebhookToken(gitlabToken)
validateEventType(gitlabEvent, event.objectKind)
codeReviewService.processWebhookEvent(event)
return ResponseEntity.ok(
WebhookResponse(
status = "accepted",
message = "Webhook received and processing started"
)
)
}
}
Service Layer
CodeReviewService
전체 코드 리뷰 프로세스의 비즈니스 로직을 담당하는 코어 서비스입니다.
역할:
- Webhook 이벤트 액션 결정 (open, update, reopen 등)
- 비동기 코루틴 스코프에서 리뷰 실행
- 드래프트/WIP MR 필터링
- 리뷰 컨텍스트 구성
- 라인별 리뷰 수행
- 요약 댓글 작성
- Google Chat 알림 전송
- 에러 처리 및 알림
suspend fun performCodeReview(projectId: Long, mrIid: Long, mrUrl: String) {
// 1. MR 상세 정보 조회
val mr = gitLabApiClient.getMergeRequest(projectId, mrIid)
// 드래프트 MR 스킵
if (mr.draft || mr.workInProgress) return
// 2. 리뷰 컨텍스트 설정
val context = contextBuilderService.buildContext(projectId, mr)
if (context.files.isEmpty()) return
// 3. 라인별 리뷰 수행
val lineCommentCount = lineByLineReviewService.performLineByLineReview(
projectId, mrIid, context
)
// 4. 요약 댓글 작성
gitLabApiClient.postMergeRequestNote(projectId, mrIid, summaryComment)
// 5. Google Chat 알림
googleChatNotifier.notifyLineByLineReview(context, lineCommentCount, mrUrl)
}
LineByLineReviewService
라인별 상세 리뷰를 수행하는 서비스입니다.
역할:
- 의존성 분석 수행 (DependencyAnalyzer)
- 파일간 영향도 분석 (CrossFileImpactAnalyzer)
- 분석 결과를 포함한 고급 프롬프트 생성
- Spring AI ChatClient를 통한 AI 호출
- AI 응답 JSON 파싱
- GitLab Discussion API를 통한 라인별 댓글 작성
suspend fun performLineByLineReview(
projectId: Long,
mrIid: Long,
context: ReviewContext
): Int {
// Step 1: 의존성 분석
val dependencies = dependencyAnalyzer.analyze(
projectId, context.files, context.targetBranch
)
// Step 2: 파일간 영향도 분석
val crossFileAnalysis = crossFileImpactAnalyzer.analyze(
context.files, dependencies
)
// Step 3: 프롬프트 생성
val prompt = buildEnhancedPrompt(context, dependencies, crossFileAnalysis)
// Step 4: AI 호출
val aiResponse = callAiForLineReview(prompt)
// Step 5: 응답 파싱
val lineComments = parseLineComments(aiResponse)
// Step 6: GitLab 댓글 작성
val postedCount = postLineComments(
projectId, mrIid, lineComments, diffRefs, context
)
return postedCount
}
ContextBuilderService
리뷰에 필요한 컨텍스트를 구성합니다.
역할:
- MR 변경사항 조회
- 파일 필터링 (삭제된 파일, 제외 확장자, 제외 경로, 큰 파일 등)
- 코드 변경 통계 계산 (추가/삭제 라인 수)
- ReviewContext 객체 생성
GoogleChatNotifier
Google Chat으로 알림을 전송합니다.
역할:
- 리뷰 완료 알림 전송
- 오류 발생 알림 전송
- 구조화된 카드 형식 메시지 생성
Analyzer Layer
DependencyAnalyzer
변경된 파일에서 심볼을 추출하고 의존성을 분석합니다.
역할:
- Git Diff에서 추가된 라인 추출
- Kotlin/Java 심볼 파싱 (함수, 클래스, 인터페이스, 프로퍼티 등)
- GitLab Code Search API를 통한 심볼 사용처 검색
- 의존성 정보 수집 및 반환
지원하는 심볼:
- Kotlin: fun, class, data class, interface, object, val/var, const val
- Java: method, class, interface, enum, field
// Kotlin 함수 정의 감지
val functionRegex = """(public |private |protected |internal )?(suspend )?(inline )?(fun )\s+(\w+)\s*(<[^>]+>)?\s*\(""".toRegex()
// GitLab Code Search로 사용처 검색
val searchResults = gitLabApiClient.searchCode(
projectId = projectId,
query = symbol.name,
ref = targetBranch
)
CrossFileImpactAnalyzer
파일간 영향도를 분석하고 Breaking Change를 감지합니다.
역할:
- 변경된 파일별 의존성 그룹핑
- 영향받는 파일 수 및 사용 횟수 계산
- 영향도 레벨 결정 (CRITICAL, HIGH, MEDIUM, LOW)
- Breaking Change 감지 (시그니처 변경, 삭제 등)
- 분석 결과 요약 생성
영향도 레벨 기준:
- CRITICAL: 영향받는 파일 10개 이상 또는 사용 20회 이상
- HIGH: 영향받는 파일 5개 이상 또는 사용 10회 이상
- MEDIUM: 영향받는 파일 2개 이상 또는 사용 5회 이상
- LOW: 그 외
Client Layer
GitLabApiClient
GitLab API와 통신하는 클라이언트입니다.
제공 메서드:
getMergeRequest(): MR 상세 정보 조회getMergeRequestChanges(): MR 변경사항 조회postMergeRequestNote(): MR 일반 댓글 작성createDiscussion(): 라인별 Discussion 생성searchCode(): 코드 검색 (의존성 분석에 사용)
특징:
@Retryable애노테이션을 통한 자동 재시도- WebClient 기반 비동기 통신
- 16MB 버퍼 크기 설정 (큰 diff 대응)
GoogleChatClient
Google Chat Webhook으로 메시지를 전송합니다.
Parser Layer
DiffParser
Git Unified Diff 형식을 파싱합니다.
역할:
- Diff 헤더 파싱 (
@@ -old_start,old_count +new_start,new_count @@) - 추가/삭제/컨텍스트 라인 분류
- 라인 번호 매핑 생성
- 추가된 라인만 추출하는 유틸리티 제공
val addedLines = diffParser.getAddedLines(file.diff)
// DiffLineMapping(newLine=42, oldLine=null, content="fun newFunction() {", type=ADDITION)
JsonResponseParser
AI 응답 JSON을 파싱하고 불완전한 JSON을 복구합니다.
역할:
- 코드 펜스(```json)에서 JSON 추출
- 중괄호 매칭을 통한 JSON 추출
- 불완전한 JSON 자동 수정 (중괄호/대괄호 닫기, 문자열 종료 등)
- LineComment 객체로 변환
견고한 파싱:
- JSON EOF 에러 자동 복구
- 불완전한 문자열 종료 처리
- 구조 분석 및 자동 완성
Prompt Layer
PromptTemplateService
AI 프롬프트를 생성합니다.
역할:
- 구조화된 프롬프트 템플릿 생성
- MR 정보 포함
- 의존성 분석 결과 포함 (실제 코드 스니펫)
- 파일간 영향도 분석 결과 포함
- 변경된 파일 diff 포함
- JSON 응답 형식 명시
- 리뷰 가이드라인 제공
프롬프트 구조:
1. 헤더: 역할 및 JSON 응답 강조
2. MR 정보: 프로젝트, 브랜치, 작성자 등
3. 의존성 분석 결과: 심볼 사용처 코드 스니펫
4. 파일간 영향 분석: Breaking Change, 영향도
5. 변경된 파일: Diff 내용
6. 응답 형식: JSON 스키마 및 예시
7. 리뷰 가이드라인: 구체적인 지침
Configuration
SpringAiConfig
Spring AI 관련 빈을 설정합니다.
제공 빈:
openAiApi(): OpenAI 호환 API 클라이언트chatClient(): ChatClient 인스턴스defaultChatOptions(): 기본 ChatOptionslineReviewChatOptions(): 라인 리뷰용 ChatOptions
WebClientConfig
WebClient 빈을 설정합니다.
제공 빈:
gitLabWebClient(): GitLab API용 WebClientgoogleChatWebClient(): Google Chat용 WebClient
특징:
- Netty 기반 커넥션 풀
- 타임아웃 설정 (connect, read, write)
- 자동 재시도 활성화 (
@EnableRetry)
동작 방식
1. GitLab Webhook 수신
GitLab MR 이벤트 발생
↓
GitLabWebhookController.handleGitLabWebhook()
↓
Webhook Token 검증
↓
이벤트 타입 검증 (Merge Request인지 확인)
↓
CodeReviewService.processWebhookEvent()
↓
액션 결정 (open, update, reopen 등)
↓
async 코루틴 시작 (즉시 200 OK 응답)
2. 코드 리뷰 프로세스
CodeReviewService.performCodeReview()
↓
[1단계] GitLab API로 MR 상세 조회
- 드래프트/WIP 필터링
↓
[2단계] 리뷰 컨텍스트 구성
- MR 변경사항 조회
- 파일 필터링 (제외 규칙 적용)
- 코드 변경 통계 계산
↓
[3단계] 라인별 리뷰 수행
- LineByLineReviewService.performLineByLineReview()
↓
[4단계] 요약 댓글 작성
- GitLabApiClient.postMergeRequestNote()
↓
[5단계] Google Chat 알림
- GoogleChatNotifier.notifyLineByLineReview()
3. 라인별 리뷰 상세 프로세스
LineByLineReviewService.performLineByLineReview()
↓
[Step 1] 의존성 분석
┌─────────────────────────────────────────┐
│ DependencyAnalyzer.analyze() │
│ - 변경된 파일 diff 파싱 │
│ - 추가된 라인에서 심볼 추출 │
│ (함수, 클래스, 인터페이스 등) │
│ - GitLab Code Search로 심볼 사용처 검색 │
│ - 의존성 정보 수집 │
└─────────────────────────────────────────┘
↓
[Step 2] 파일간 영향도 분석
┌─────────────────────────────────────────┐
│ CrossFileImpactAnalyzer.analyze() │
│ - 파일별 의존성 그룹핑 │
│ - 영향받는 파일 수 계산 │
│ - 영향도 레벨 결정 │
│ - Breaking Change 감지 │
│ - 분석 결과 요약 생성 │
└─────────────────────────────────────────┘
↓
[Step 3] 고급 프롬프팅
┌─────────────────────────────────────────┐
│ PromptTemplateService │
│ - MR 정보 추가 │
│ - 의존성 분석 결과 추가 │
│ (실제 코드 스니펫 포함) │
│ - 파일간 영향도 분석 결과 추가 │
│ - 변경된 파일 diff 추가 │
│ - JSON 응답 형식 명시 │
│ - 리뷰 가이드라인 추가 │
└─────────────────────────────────────────┘
↓
[Step 4] AI 호출
┌─────────────────────────────────────────┐
│ ChatClient.prompt() │
│ .system("You are a code reviewer...") │
│ .user(prompt) │
│ .options(lineReviewChatOptions) │
│ .call() │
│ .content() │
└─────────────────────────────────────────┘
↓
[Step 5] 응답 파싱
┌─────────────────────────────────────────┐
│ JsonResponseParser.parseLineComments() │
│ - 코드 펜스 또는 중괄호에서 JSON 추출 │
│ - 불완전한 JSON 자동 복구 │
│ - LineComment 객체 리스트로 변환 │
└─────────────────────────────────────────┘
↓
[Step 6] GitLab 댓글 작성
┌─────────────────────────────────────────┐
│ postLineComments() │
│ - 파일별 댓글 그룹핑 │
│ - DiscussionPosition 생성 │
│ - GitLabApiClient.createDiscussion() │
│ (각 라인에 댓글 작성) │
└─────────────────────────────────────────┘
4. 의존성 분석 예시
시나리오: UserService.kt에서 getUser() 함수의 시그니처가 변경됨
// 변경 전
fun getUser(id: Long): User
// 변경 후
fun getUser(id: Long, includeDeleted: Boolean = false): User
분석 과정:
- 심볼 추출:
getUser함수가 변경됨을 감지 - 사용처 검색: GitLab Code Search로
getUser검색OrderService.kt:45에서 사용PaymentService.kt:128에서 사용
- 영향도 분석: 2개 파일에 영향, MEDIUM 레벨
- AI 리뷰:
- OrderService.kt:45에 댓글: "새로운
includeDeleted파라미터가 추가되었으나 기본값이 있으므로 호환성 유지됨. 다만 삭제된 유저를 포함해야 하는지 검토가 필요합니다." - PaymentService.kt:128에 댓글: "결제 처리 시 삭제된 유저는 처리하지 않아야 하므로 기본값(
false) 사용이 적절합니다."
- OrderService.kt:45에 댓글: "새로운
5. AI 프롬프팅 전략
이 프로젝트는 고급 프롬프팅 전략을 사용하여 AI가 보다 구체적이고 정확한 리뷰를 제공하도록 합니다.
핵심 전략
- 컨텍스트 기반 프롬프팅
- 단순히 diff만 보여주는 것이 아니라, 변경이 미치는 영향을 함께 제공
- 의존성 분석 결과를 실제 코드 스니펫과 함께 제공
- 구조화된 프롬프트
- 명확한 섹션 구분 (MR 정보, 의존성 분석, 영향 분석, 변경 파일)
- 각 섹션의 목적과 중요도 명시
- 명시적 지침
- AI가 정확히 무엇을 해야 하는지 명확히 지시
- JSON 형식 강조 및 예시 제공
- 리뷰 가이드라인 명시 (간결함, 구체성, 한국어)
- 실제 코드 포함
- 의존성 분석 시 실제 사용처 코드를 포함
- AI가 추상적으로 말하지 않고 구체적인 파일명과 라인 번호 언급 가능
- Breaking Change 강조
- 파일간 영향 분석 결과에서 Breaking Change를 별도로 표시
- AI가 중요한 이슈에 집중하도록 유도
설정 및 환경변수
application.yml 주요 설정
spring:
application:
name: dmr
ai:
openai:
# vLLM 서버 주소 (OpenAI 호환 API)
base-url: ${VLLM_BASE_URL}
# API 키 (vLLM은 보통 EMPTY로 설정)
api-key: ${VLLM_API_KEY:EMPTY}
timeout:
connect: ${VLLM_CONNECT_TIMEOUT:10}
read: ${VLLM_READ_TIMEOUT:300}
chat:
options:
# 사용할 모델명
model: ${VLLM_MODEL:openai/gpt-oss-20b}
# 낮은 temperature로 일관된 응답 유도
temperature: 0.3
max-tokens: 4000
top-p: 0.95
gitlab:
# GitLab 서버 주소
url: ${GITLAB_URL}
# GitLab Personal Access Token (api, read_api 권한 필요)
token: ${GITLAB_TOKEN}
webhook:
# Webhook Secret Token (GitLab Webhook 설정 시 동일한 값 사용)
secret-token: ${GITLAB_WEBHOOK_SECRET}
api:
timeout: 30s
max-retries: 3
retry-delay: 1s
google:
chat:
# Google Chat Webhook URL
webhook-url: ${GOOGLE_CHAT_WEBHOOK_URL}
# Google Chat 알림 활성화 여부
enabled: ${GOOGLE_CHAT_ENABLED:true}
timeout: 10s
code-review:
# 최대 리뷰 파일 수
max-files: 50
# 최대 파일 크기 (바이트)
max-file-size: 1048576 # 1MB
# 라인별 리뷰 활성화
line-by-line-enabled: true
ai:
# 라인 리뷰용 AI 옵션 (더 큰 토큰)
temperature: 0.3
max-tokens: 12000
top-p: 0.95
# 제외할 파일 확장자
excluded-extensions:
- .gitattributes
- .gitignore
- .mod
- .env.example
- .bat
- .md
# 제외할 경로
excluded-paths:
- gradle/
- build/
- .kotlin/
- .gradle/
- .idea/
- .claude/
# 영향도 분석 설정
analysis:
side-effects:
severity-threshold: medium
patterns:
database:
- "INSERT INTO"
- "UPDATE "
- "DELETE FROM"
- "ALTER TABLE"
- "DROP TABLE"
- "TRUNCATE"
api:
- "RestTemplate"
- "WebClient"
- "HttpClient"
- "@FeignClient"
file-io:
- "FileWriter"
- "FileOutputStream"
- "Files.write"
- "Files.delete"
state:
- "static "
- "companion object"
- "object "
server:
port: ${SERVER_PORT:8080}
shutdown: graceful
logging:
level:
root: INFO
com.example.dmr: DEBUG
필수 환경변수
| 환경변수 | 설명 | 예시 |
|---|---|---|
VLLM_BASE_URL |
vLLM 서버 주소 (OpenAI 호환) | http://your-vllm-server:8000 |
VLLM_API_KEY |
vLLM API 키 (보통 EMPTY) | EMPTY |
VLLM_MODEL |
사용할 AI 모델명 | openai/gpt-oss-20b |
GITLAB_URL |
GitLab 서버 URL | https://your.gitlab.com |
GITLAB_TOKEN |
GitLab Personal Access Token | glpat-xxxxx |
GITLAB_WEBHOOK_SECRET |
GitLab Webhook Secret Token | your-secret-token |
GOOGLE_CHAT_WEBHOOK_URL |
Google Chat Webhook URL | https://chat.googleapis.com/v1/spaces/... |
GOOGLE_CHAT_ENABLED |
Google Chat 알림 활성화 | true |
SERVER_PORT |
서버 포트 | 8080 |
GitLab Personal Access Token 생성
- GitLab > Settings > Access Tokens
- Token name:
DMR Code Review Bot - Scopes:
api,read_api,read_repository - Create personal access token
- 생성된 토큰을
GITLAB_TOKEN환경변수에 설정
실행 방법
사전 요구사항
- Java 21 이상
- vLLM 서버 또는 OpenAI 호환 API 서버 (Ollama, Azure OpenAI 등)
- GitLab 인스턴스 (Webhook 설정 가능)
- (선택) Google Chat Workspace (알림 수신용)
빌드
# Gradle Wrapper 실행 권한 부여
chmod +x gradlew
# 빌드
./gradlew build
# 테스트 스킵하고 빌드
./gradlew build -x test
로컬 실행
# 환경변수 설정
export VLLM_BASE_URL=http://your-vllm-server:8000
export VLLM_API_KEY=EMPTY
export VLLM_MODEL=openai/gpt-oss-20b
export GITLAB_URL=https://your.gitlab.com
export GITLAB_TOKEN=glpat-xxxxxxxxxxxxx
export GITLAB_WEBHOOK_SECRET=your-webhook-secret
export GOOGLE_CHAT_WEBHOOK_URL=https://chat.googleapis.com/v1/spaces/xxx
export GOOGLE_CHAT_ENABLED=true
export SERVER_PORT=8080
# 실행
./gradlew bootRun
Docker 실행
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY build/libs/dmr-0.0.1-SNAPSHOT.jar app.jar
ENV JAVA_OPTS="-Xms512m -Xmx2g"
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
# Docker 이미지 빌드
./gradlew build
docker build -t dmr:latest .
# Docker 컨테이너 실행
docker run -d \
--name dmr \
-p 8080:8080 \
-e VLLM_BASE_URL=http://your-vllm-server:8000 \
-e VLLM_API_KEY=EMPTY \
-e VLLM_MODEL=openai/gpt-oss-20b \
-e GITLAB_URL=https://your.gitlab.com \
-e GITLAB_TOKEN=glpat-xxxxxxxxxxxxx \
-e GITLAB_WEBHOOK_SECRET=your-webhook-secret \
-e GOOGLE_CHAT_WEBHOOK_URL=https://chat.googleapis.com/v1/spaces/xxx \
-e GOOGLE_CHAT_ENABLED=true \
dmr:latest
GitLab Webhook 설정
- DMR 서버가 외부에서 접근 가능한지 확인
- 공개 IP 또는 도메인 필요
- 포트 포워딩 설정 (예: 8080)
- GitLab 프로젝트 설정
- GitLab 프로젝트 > Settings > Webhooks
- URL:
http://your-dmr-server:8080/api/webhook/gitlab - Secret Token:
GITLAB_WEBHOOK_SECRET와 동일한 값 입력 - Trigger:
Merge request events체크 - SSL verification: 환경에 맞게 설정
- Add webhook
- Webhook 테스트
- Webhook 설정 페이지에서 "Test" > "Merge Request events"
- Recent events에서 응답 확인
- 200 OK 응답이면 성공
- MR 생성하여 테스트
- 새로운 브랜치 생성 및 코드 변경
- Merge Request 생성
- DMR 서버 로그 확인
- GitLab MR 페이지에서 AI 댓글 확인
특징
1. 라인별 상세 리뷰
전통적인 MR 레벨 요약이 아닌, 각 코드 라인에 직접 댓글을 작성합니다.
장점:
- 개발자가 정확히 어느 라인의 문제인지 즉시 파악
- GitLab Discussion 형태로 대화 가능
- 심각도 레벨(CRITICAL, WARNING, SUGGESTION, INFO)로 우선순위 판단 용이
예시:
UserService.kt, Line 45:
🔴 CRITICAL: includeDeleted 파라미터가 추가되었으나 OrderService.kt:128에서
이전 시그니처로 호출하고 있어 컴파일 오류가 발생할 수 있습니다.
2. 의존성 분석 기능
변경된 코드가 다른 파일에서 어떻게 사용되는지 자동으로 분석합니다.
기능:
- Git Diff에서 변경된 함수, 클래스 등의 심볼 추출
- GitLab Code Search API를 통한 심볼 사용처 검색
- Kotlin 및 Java 코드 지원
- 실제 코드 스니펫을 AI 프롬프트에 포함
효과:
- 단순히 변경된 파일만 보는 것이 아니라, 영향받는 모든 파일 고려
- Breaking Change 조기 발견
- 리팩토링 시 누락된 수정 지적
3. 파일간 영향도 분석
변경이 얼마나 광범위하게 영향을 미치는지 정량적으로 평가합니다.
분석 항목:
- 영향받는 파일 수
- 심볼 사용 횟수
- Public/Private 심볼 구분
- 영향도 레벨 (CRITICAL, HIGH, MEDIUM, LOW)
- Breaking Change 감지
활용:
- 리뷰 우선순위 결정
- 리스크 평가
- 테스트 범위 결정
4. 고급 프롬프팅 전략
단순 diff 제공을 넘어, AI가 깊이 있는 분석을 할 수 있도록 풍부한 컨텍스트를 제공합니다.
프롬프트 구성:
- MR 메타데이터 (제목, 설명, 브랜치 등)
- 의존성 분석 결과 (실제 코드 스니펫 포함)
- 파일간 영향도 분석 결과
- 변경된 파일 diff
- JSON 응답 형식 명시
- 리뷰 가이드라인
효과:
- AI가 추상적이지 않고 구체적인 파일명과 라인 번호 언급
- 단순 문법 체크를 넘어 로직 및 아키텍처 리뷰
- 일관된 응답 형식으로 파싱 용이
5. Google Chat 알림
리뷰 완료 또는 오류 발생 시 Google Chat으로 즉시 알림을 받습니다.
알림 내용:
- MR 제목 및 작성자
- 브랜치 정보
- 라인별 댓글 수
- 변경된 파일 수
- 코드 변경량 (+/-)
- MR 링크 (새 창으로 열기 버튼)
장점:
- 팀원들이 실시간으로 리뷰 완료 인지
- 오류 발생 시 즉시 대응 가능
- Google Chat과 연동되어 기존 커뮤니케이션 채널 활용
6. 견고한 에러 처리
프로덕션 환경에서 안정적으로 동작하도록 다양한 에러 처리 메커니즘을 갖추고 있습니다.
기능:
- Spring Retry를 통한 자동 재시도 (지수 백오프)
- 불완전한 JSON 응답 자동 복구
- 비동기 처리로 Webhook 응답 속도 보장 (200 OK 즉시 반환)
- 예외 발생 시 Google Chat 알림
- 상세한 로깅 (DEBUG 레벨)
7. 확장성 및 유지보수성
Spring Boot 및 Kotlin의 장점을 활용하여 확장 가능하고 유지보수가 쉬운 구조입니다.
특징:
- 계층별 명확한 역할 분리 (Controller, Service, Analyzer, Client, Parser)
- 의존성 주입을 통한 느슨한 결합
- Kotlin Coroutines를 통한 효율적인 비동기 처리
- ConfigurationProperties를 통한 타입 안전한 설정
- 테스트 용이한 구조
API 엔드포인트
Webhook Endpoint
POST /api/webhook/gitlab
GitLab Webhook 이벤트를 수신합니다.
Headers:
Content-Type: application/json
X-Gitlab-Event: Merge Request Hook
X-Gitlab-Token: your-webhook-secret-token
Request Body:
{
"object_kind": "merge_request",
"user": {
"id": 123,
"name": "John Doe",
"username": "johndoe",
"email": "john@example.com"
},
"project": {
"id": 456,
"name": "My Project",
"web_url": "https://gitlab.example.com/group/project",
"path_with_namespace": "group/project"
},
"object_attributes": {
"id": 789,
"iid": 42,
"title": "Add new feature",
"description": "This MR adds...",
"source_branch": "feature/new-feature",
"target_branch": "main",
"state": "opened",
"action": "open",
"draft": false,
"work_in_progress": false
}
}
Response:
{
"status": "accepted",
"message": "Webhook received and processing started"
}
Status Codes:
200 OK: Webhook 수신 성공, 백그라운드 처리 시작400 Bad Request: 유효하지 않은 이벤트 타입401 Unauthorized: 유효하지 않은 Webhook 토큰500 Internal Server Error: 서버 오류
운영 관련
AI 모델 파라미터 튜닝
spring:
ai:
openai:
chat:
options:
# temperature: 0 (일관성) ~ 1 (창의성)
temperature: 0.3
# max-tokens: 응답 길이 제한
max-tokens: 12000
# top-p: 샘플링 확률 (0.95 권장)
top-p: 0.95
권장 설정:
- temperature: 0.2~0.4 (코드 리뷰는 일관성이 중요)
- max-tokens: 8000~15000 (라인별 리뷰는 긴 응답 필요)
- top-p: 0.9~0.95 (균형)
의존성 분석 제외 설정
너무 많은 심볼이 검색되어 성능 문제가 있다면:
// DependencyAnalyzer.kt
private fun shouldSkipSymbol(symbol: Symbol): Boolean {
// Private 심볼은 스킵
if (!symbol.isPublic) return true
// 짧은 이름 (2자 이하)는 오탐 가능성 높음
if (symbol.name.length <= 2) return true
// 특정 타입만 분석
if (symbol.type !in listOf(SymbolType.FUNCTION, SymbolType.CLASS)) return true
return false
}
GitLab Code Search 최적화
GitLab Code Search는 API 호출 제한이 있나? 그렇다면 병렬 처리보다 순차 처리 (+ sleep)
// DependencyAnalyzer.kt
for (symbol in allSymbols) {
val searchResults = gitLabApiClient.searchCode(...)
// 각 심볼별로 순차 검색
}'프로젝트 > 토이프로젝트' 카테고리의 다른 글
| [공모전] 1위의 빛나는 Butler (0) | 2025.10.16 |
|---|---|
| 푸르지오 스마트홈 가전제어를 언어로 손쉽게 (ios) (2) | 2025.01.07 |
| #4. 코인 자동 매매 프로그램 만들기 (1) | 2022.02.28 |
| #3. 코인 자동 매매 프로그램 만들기 (1) | 2022.02.07 |
| #2. 코인 자동 매매 프로그램 만들기 (0) | 2022.02.07 |