Skip to content

Commit b18f2a1

Browse files
authored
Gemini 특정 모델을 사용할 수 없는 경우, 다른 모델을 사용하도록 구현
Gemini 특정 모델을 사용할 수 없는 경우, 다른 모델을 사용하도록 수정
2 parents ef630d6 + 4f5f457 commit b18f2a1

13 files changed

Lines changed: 346 additions & 274 deletions

File tree

src/main/kotlin/com/project/codereview/client/github/GithubDiffUtils.kt

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,13 @@ object GithubDiffUtils {
1414
val side: String, // "RIGHT" | "LEFT"
1515
val snippet: String
1616
) {
17-
fun toReviewType(): ReviewType.ByMultiline =
18-
ReviewType.ByMultiline(
19-
path = path,
20-
line = endLine,
21-
side = side,
22-
start_line = startLine,
23-
start_side = side
24-
)
17+
fun toReviewType() = ReviewType.ByMultiline(
18+
path = path,
19+
line = endLine,
20+
side = side,
21+
start_line = startLine,
22+
start_side = side
23+
)
2524
}
2625

2726
// 파일 단위 컨텍스트(파일별로 묶기)

src/main/kotlin/com/project/codereview/client/google/GoogleGeminiClient.kt

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
package com.project.codereview.client.google
22

33
import com.google.genai.Client
4-
import com.google.genai.types.Content
5-
import com.google.genai.types.GenerateContentConfig
6-
import com.google.genai.types.Part
7-
import com.google.genai.types.ThinkingConfig
8-
import com.project.codereview.client.util.MODEL
4+
import com.google.genai.types.*
5+
import com.project.codereview.client.util.GeminiTextModel
96
import com.project.codereview.client.util.ReviewLanguage
107
import kotlinx.coroutines.Dispatchers
118
import kotlinx.coroutines.withContext
129
import org.slf4j.LoggerFactory
1310
import org.springframework.beans.factory.annotation.Value
1411
import org.springframework.stereotype.Component
1512
import java.util.concurrent.ConcurrentHashMap
13+
import kotlin.jvm.optionals.getOrNull
1614

1715
@Component
1816
class GoogleGeminiClient(
@@ -46,6 +44,7 @@ class GoogleGeminiClient(
4644
suspend fun chat(
4745
filePath: String,
4846
prompt: String,
47+
model: GeminiTextModel,
4948
instruction: Content = ReviewLanguage.fromExtension(filePath).let { language ->
5049
instructionMap[language] ?: instructionMap[ReviewLanguage.KT]!!
5150
},
@@ -54,7 +53,7 @@ class GoogleGeminiClient(
5453
try {
5554
logger.info("[Gemini] request started = {}", filePath)
5655
client.models.generateContentStream(
57-
MODEL,
56+
model.modelName,
5857
prompt,
5958
GenerateContentConfig.builder()
6059
.systemInstruction(instruction)

src/main/kotlin/com/project/codereview/client/util/ReviewLanguage.kt

Lines changed: 1 addition & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -32,112 +32,4 @@ enum class ReviewLanguage(
3232
} ?: KT
3333
}
3434
}
35-
}
36-
37-
const val REJECT_REVIEW = "REJECT_REVIEW"
38-
39-
private const val SYSTEM_PROMPT_JS_TS = """
40-
41-
"""
42-
43-
private const val SYSTEM_PROMPT_COMMON = """
44-
## 이름과 역할
45-
46-
- 너의 이름은 Review Bot.
47-
- 한국의 코틀린/스프링 생태계를 잘 아는 CTO 동료처럼, 간결하고 실무 친화적으로 말한다.
48-
- 입력은 Pull Request의 Git diff 결과이며, 독자는 주니어 개발자다.
49-
50-
## 응답 원칙
51-
52-
삭제된 파일이거나, 리뷰가 필요 없는 단순한 코드인 경우, ${REJECT_REVIEW}를 응답한다.
53-
그렇지 않을 경우, 다음 원칙에 따라 **출력 형식**에 맞게 응답한다.
54-
55-
- 모든 지적에는 중요도(High/Medium/Low)와 영향 범주(안정성/성능/가독성/보안/호환)를 명시한다.
56-
- 최소 3개 이상의 범주(언어·스타일, API·설계, 동시성/코루틴, 성능/컬렉션, I/O·경계, 로깅·보안, 테스트)를 동시에 다룬다.
57-
- 실제로 바꿔야 할 코드만 30줄 이내 스니펫으로 제시하고, 적용될 파일/라인 범위를 함께 적는다.
58-
59-
## 길이·톤 제약
60-
61-
- 전체 350~900자 권장(스니펫 제외), 항목별 6줄 이내.
62-
- 과장·군더더기 배제, 동료에게 직접 적용 가능한 실무 톤 유지.
63-
64-
## 스타일 가이드 선택
65-
66-
- JetBrains Kotlin 컨벤션을 기본으로 가정한다.
67-
68-
## 리뷰 체크리스트(범용)
69-
70-
1) 언어·스타일
71-
- 불변(val) 우선, 의미 있는 이름, 가시성 최소화(public 지양, internal/private 선호).
72-
- 널 안정성(안전 호출/엘비스, require/check, 널 가능 타입 축소).
73-
- 타입 설계: value/inline class, typealias, sealed 계층으로 도메인 명확화.
74-
- 스코프/확장 함수(let/run/apply/also/with) 남용 방지.
75-
76-
2) API·설계
77-
- 단일 책임, 레이어 경계, 의존 역전.
78-
- 오류 모델 일관성(예외 vs Result vs sealed error).
79-
- equals/hashCode/copy 의미·불변성 보장.
80-
- 공개 API 변경 시 호환성 영향 표기(바이너리/소스/직렬화 스키마).
81-
82-
3) 동시성/코루틴
83-
- 구조화된 동시성(스코프, 취소 전파, timeout) 준수, GlobalScope 지양.
84-
- Dispatcher 선택과 blocking 호출 유입 점검(withContext(Dispatchers.IO) vs 비차단).
85-
- Flow/Channel의 backpressure, 오류·취소 전파, 리소스 해제 확인.
86-
87-
4) 성능/컬렉션
88-
- 불필요한 객체/복사 제거, sequence/lazy 적정 사용.
89-
- 알고리즘 복잡도, 배치/캐시 전략, boxing·toList() 남용 점검.
90-
91-
5) I/O·경계
92-
- 네트워크/DB/파일 타임아웃, 재시도, 연결/자원 해제.
93-
- 직렬화/역직렬화 안전성, 입력 검증, 경계값 처리.
94-
95-
6) 로깅·보안·관측
96-
- 파라미터화 로깅 사용, 로그 레벨 일관성, MDC/추적ID.
97-
- 비밀/PII 로그 금지, 예외 메시지에 민감정보 포함 금지.
98-
- 메트릭·트레이싱 노출로 관측 가능성 확보.
99-
100-
7) 테스트
101-
- 단위/통합/계약/경계/동시성/직렬화 라운드트립.
102-
- 순수 함수에는 property-based test를 최소 1개 요구.
103-
- Given-When-Then, 결정적 실행, 명확한 네이밍.
104-
105-
## 출력 형식
106-
107-
인사 한 줄
108-
(예: 안녕하세요. ~에 대한 PR 잘 보았습니다.)
109-
110-
### 좋은 점
111-
112-
- 1~3개. 구체적 변화와 팀 가치(가독성/안정성/성능)를 연결해 말한다.
113-
114-
### 개선 및 제안
115-
116-
- 아래 서브포맷으로 항목을 작성한다. 각 항목은 다른 범주를 우선적으로 커버한다.
117-
- 설명: 무엇이 왜 문제인지, 맥락을 2~4문장으로
118-
- AS-IS: 필요한 부분만 코드 스니펫(≤30줄)
119-
- TO-BE: 즉시 적용 가능한 대안 스니펫(≤30줄)
120-
121-
간단한 마무리 인사 한 줄
122-
(예: 수고하셨습니다 👍)
123-
"""
124-
125-
const val SUMMARY_PROMPT = """
126-
## 이름과 역할
127-
128-
- 너의 이름은 Review Bot.
129-
- 너는 다재다능한 CTO이고, 간결하고 실무 친화적인 말투로 작업 결과물에 대해서 요약을 해야한다.
130-
- 입력은 Pull Request의 Git diff 결과이며, 독자는 주니어 개발자다.
131-
132-
## 응답 원칙
133-
134-
- 이 Pull Request에 어떤 변경 사항이 있는지 정리한다.
135-
- 절대로 향후 방향성에 대한 내용은 작성하지 않고, 단순히 코드 변경 사항을 요약한다.
136-
137-
## 출력 형식
138-
139-
인사 한 줄
140-
(예: 안녕하세요. 코드 변경 사항에 대해서 요약해드리겠습니다.)
141-
142-
주요 변경 사항 요약
143-
"""
35+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.project.codereview.client.util
2+
3+
enum class GeminiTextModel(
4+
val modelName: String,
5+
val maxRpm: Int,
6+
val maxTpm: Int,
7+
val maxRpd: Int
8+
) {
9+
GEMINI_2_5_FLASH(
10+
modelName = "gemini-2.5-flash",
11+
maxRpm = 10,
12+
maxTpm = 250_000,
13+
maxRpd = 250
14+
),
15+
16+
GEMINI_2_5_FLASH_LITE(
17+
modelName = "gemini-2.5-flash-lite",
18+
maxRpm = 15,
19+
maxTpm = 250_000,
20+
maxRpd = 1_000
21+
),
22+
23+
GEMINI_2_5_PRO(
24+
modelName = "gemini-2.5-pro",
25+
maxRpm = 2,
26+
maxTpm = 125_000,
27+
maxRpd = 50
28+
),
29+
30+
GEMINI_2_0_FLASH(
31+
modelName = "gemini-2.0-flash",
32+
maxRpm = 15,
33+
maxTpm = 1_000_000,
34+
maxRpd = 200
35+
),
36+
37+
GEMINI_2_0_FLASH_LITE(
38+
modelName = "gemini-2.0-flash-lite",
39+
maxRpm = 30,
40+
maxTpm = 1_000_000,
41+
maxRpd = 200
42+
);
43+
44+
fun toRateLimit(): RateLimit = RateLimit(
45+
rpm = maxRpm,
46+
tpm = maxTpm,
47+
rpd = maxRpd
48+
)
49+
}
50+
51+
data class RateLimit(
52+
val rpm: Int,
53+
val tpm: Int,
54+
val rpd: Int
55+
)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.project.codereview.client.util
2+
3+
const val REJECT_REVIEW = "REJECT_REVIEW"
4+
5+
const val SYSTEM_PROMPT_COMMON = """
6+
## 이름과 역할
7+
8+
- 너의 이름은 Review Bot.
9+
- 한국의 코틀린/스프링 생태계를 잘 아는 CTO 동료처럼, 간결하고 실무 친화적으로 말한다.
10+
- 입력은 Pull Request의 Git diff 결과이며, 독자는 주니어 개발자다.
11+
12+
## 응답 원칙
13+
14+
삭제된 파일이거나, 리뷰가 필요 없는 단순한 코드인 경우, ${REJECT_REVIEW}를 응답한다.
15+
그렇지 않을 경우, 다음 원칙에 따라 **출력 형식**에 맞게 응답한다.
16+
17+
- 모든 지적에는 중요도(High/Medium/Low)와 영향 범주(안정성/성능/가독성/보안/호환)를 명시한다.
18+
- 최소 3개 이상의 범주(언어·스타일, API·설계, 동시성/코루틴, 성능/컬렉션, I/O·경계, 로깅·보안, 테스트)를 동시에 다룬다.
19+
- 실제로 바꿔야 할 코드만 5줄 이내 스니펫으로 제시한다.
20+
21+
## 출력 형식
22+
23+
인사 한 줄
24+
(예: 안녕하세요. ~에 대한 PR 잘 보았습니다.)
25+
26+
### 좋은 점
27+
28+
- 1~3개. 구체적 변화와 팀 가치(가독성/안정성/성능)를 연결해 말한다.
29+
30+
### 개선 및 제안
31+
32+
- 아래 서브포맷으로 항목을 작성한다. 각 항목은 다른 범주를 우선적으로 커버한다.
33+
- 설명: 무엇이 왜 문제인지, 맥락을 2~4문장으로
34+
- AS-IS: 필요한 부분만 코드 스니펫(≤30줄)
35+
- TO-BE: 즉시 적용 가능한 대안 스니펫(≤30줄)
36+
37+
간단한 마무리 인사 한 줄
38+
(예: 수고하셨습니다 👍)
39+
"""
40+
41+
const val SUMMARY_PROMPT = """
42+
## 이름과 역할
43+
44+
- 너의 이름은 Review Bot.
45+
- 너는 다재다능한 CTO이고, 간결하고 실무 친화적인 말투로 작업 결과물에 대해서 요약을 해야한다.
46+
- 입력은 Pull Request의 Git diff 결과이며, 독자는 주니어 개발자다.
47+
48+
## 응답 원칙
49+
50+
- 이 Pull Request에 어떤 변경 사항이 있는지 정리한다.
51+
- 절대로 향후 방향성에 대한 내용은 작성하지 않고, 단순히 코드 변경 사항을 요약한다.
52+
53+
## 출력 형식
54+
55+
인사 한 줄
56+
(예: 안녕하세요. 코드 변경 사항에 대해서 요약해드리겠습니다.)
57+
58+
주요 변경 사항 요약
59+
"""

src/main/kotlin/com/project/codereview/client/util/ReviewUtils.kt

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
package com.project.codereview.core.controller
22

3-
import com.project.codereview.core.dto.GithubActionType
43
import com.project.codereview.core.dto.parsePayload
5-
import com.project.codereview.core.service.CodeReviewService
4+
import com.project.codereview.core.service.PullRequestEventEntry
65
import com.project.codereview.core.util.GithubSignature
76
import org.slf4j.LoggerFactory
87
import org.springframework.beans.factory.annotation.Value
@@ -16,52 +15,32 @@ import org.springframework.web.bind.annotation.RestController
1615
@RestController
1716
class CodeReviewController(
1817
@param:Value("\${app.github.webhook.secret-key}") private val secret: String,
19-
private val codeReviewService: CodeReviewService
18+
private val entry: PullRequestEventEntry
2019
) {
21-
private val logger = LoggerFactory.getLogger(CodeReviewController::class.java)
20+
private val log = LoggerFactory.getLogger(CodeReviewController::class.java)
2221

2322
@PostMapping("/api/code/review")
24-
suspend fun net(
23+
suspend fun handleWebhook(
2524
@RequestHeader("X-GitHub-Event") event: String,
2625
@RequestHeader("X-Hub-Signature-256", required = false) sig256: String?,
2726
@RequestBody rawBody: ByteArray
2827
): ResponseEntity<String> {
29-
if (sig256 == null) {
30-
logger.warn("Missing signature header for GitHub webhook")
31-
return fail("Missing signature")
32-
}
33-
// 서명 검증
34-
if (!GithubSignature.isValid(sig256, secret, rawBody)) {
35-
logger.warn("Invalid signature detected from GitHub webhook")
36-
return fail("Invalid signature")
37-
}
28+
if (sig256.isNullOrBlank()) return fail("Missing signature")
29+
if (!GithubSignature.isValid(sig256, secret, rawBody)) return fail("Invalid signature")
3830

3931
val payload = try {
4032
parsePayload(rawBody)
4133
} catch (e: Exception) {
42-
return fail("Invalid payload : ${e.message}", HttpStatus.NOT_ACCEPTABLE)
34+
return fail("Invalid payload: ${e.message}", HttpStatus.NOT_ACCEPTABLE)
4335
}
4436

45-
val action = GithubActionType(payload.action)
46-
logger.info("payload = {}, action = {}", payload, action)
37+
entry.handle(event, payload)
4738

48-
// PR 생성 시에만 리뷰 진행
49-
when (event) {
50-
"pull_request" -> when (GithubActionType(payload.action)) {
51-
GithubActionType.OPENED,
52-
GithubActionType.REOPENED -> {
53-
codeReviewService.review(payload)
54-
}
55-
else -> logger.info("Ignored pull_request event: ${payload.action}")
56-
}
57-
else -> logger.debug("Unhandled GitHub event: $event")
58-
}
59-
60-
return ResponseEntity.ok("Request completed")
39+
return ResponseEntity.ok("Accepted")
6140
}
6241

63-
private fun fail(
64-
message: String,
65-
status: HttpStatus = HttpStatus.UNAUTHORIZED
66-
) = ResponseEntity.status(status).body(message)
42+
private fun fail(message: String, status: HttpStatus = HttpStatus.UNAUTHORIZED): ResponseEntity<String> {
43+
log.error("[Api request fail] reason = {}", message)
44+
return ResponseEntity.status(status).body(message)
45+
}
6746
}

0 commit comments

Comments
 (0)