개발자라면, 무에서 유를 창조해야 합니다. 물론 금수저라면 유에서 유를 창조해도 되겠죠.
하지만 저는 흙수저이기 때문에 창조해내야만 합니다. 오늘은 오픈소스 LLM을 사용한 흙수저용 AI 코드리뷰어를 만들어보겠습니다.
해내기만 한다면 로컬 LLM을 통해 보안성과 엄청난 비용절감이라는 두 마리 토끼를 잡을 수 있습니다.
개요
얼마 전, 개인 github에 코드래빗을 무료로 적용해봤습니다.
코드래빗은 코드의 변경점(diff_text
)을 확인한 뒤 AI모델을 통해 코드를 리뷰해주고, 사용자와 대화형으로 피드백을 해주는 좋은 놈이었습니다.
그러나...
- 클라우드 기반의 코드래빗이 회사 보안 정책상 사내 서버에 설치된 GitLab과 연동하기 어렵다.
- 코드래빗이나 Claude, GPT API는 돈이 든다.
결국, 로컬에 설치 가능한 AI 리뷰어가 필요했고, 저는 이 문제를 0원으로 해결해보기로 했습니다.
결과물
결과물은 다음과 같이 나왔습니다. 코드래빗 보다는 다소 부족하지만, 동료의 코드를 이해하기 전에 살펴볼 정도의 퀄리티는 나오는 것 같습니다.
필요한 거
어떻게 동작시키지?
먼저 (사내)서버에 설치된 GitLab에 리뷰어 레포지토리(mr-reviewer
) 와 리뷰를 받을 레포지토리(mmis
) 이렇게 2개의 레포를 준비합니다.
그리고 Ollama를 같은 서버에 설치하고, API 엔드포인트를 확인합니다.
mr-reviwer
에는 리뷰를 진행할 python 코드를 넣고, Ollama와 리뷰 받을 레포지토리를 연결합니다.
해당 python 코드를 systemd로 실행해서 1분에 한 번씩 열려있는 Merge Request에 대한 리뷰를 진행하도록 자동화합니다.
이제 만들어봅시다!!
1. 리뷰를 받을 Gitlab 레포지토리의 token 발급
[Gitlab] - [Settings] - [Access Tokens]에서 Add new token
을 클릭합니다.
api, read_api, read_repository를 체크한 뒤 생성해줍니다.
생성된 값은 따로 잘 복사해둡니다. 이후 python에서 GitLab 연결에 필요합니다.
2. Ollama 설치 및 원하는 LLM 준비(pull)
리눅스 서버라면
curl -fsSL https://ollama.com/install.sh | sh
을 통해 손쉽게 Ollama를 설치할 수 있습니다.
Open-webui를 이용해 ChatGPT처럼 만들 수도 있죠.
여기서 /admin/settings
를 url 뒤에 입력하면 설정 페이지로 이동하는데, [설정] - [모델] - [(우측상단) 다운로드 버튼]을 누르면 다음과 같이 원하는 모델을 받을 수 있습니다.
다운로드 받을 수 있는 LLM 목록을 확인해서
ollama run 모델명
을 입력하면 모델을 받을 수 있습니다.
GUI상에서는
모델명
만 입력하고 다운로드 버튼을 누르면 됩니다.
3. 리뷰봇 레포지토리 준비
이제 마지막으로 리뷰를 해줄 봇 레포지토리를 준비합니다.
서버 내 GitLab에 레포지토리를 하나 생성해줍니다. 저는 mr-reviewer
라는 이름으로 생성했습니다.
생성된 레포지토리 내에는 간단하게 mr_reviewer.py
파일과 requirements.txt
파일을 준비합니다.
openai
requests
mr_reviewer.py
파일에는 다음과 같은 내용이 필요합니다.
- 환경변수(GitLab과 Ollama)
GITLAB_URL = "http://101.1.1.109" # 자신의 서버 주소(예시)
GITLAB_TOKEN = "glpat--UMWI92xkJSkmMC" # 자신의 Gitlab token(예시) - 위에서 발급한 토큰
LLM_URL = "http://101.1.10.109:11434/v1/" # Ollama 주소(예시)
LLM_MODEL = "qwen3:32b-q8_0" # 사용할 LLM 모델(예시) - 위에서 받은 모델
- 리뷰받을 레포지토리의 project_id
curl --header "PRIVATE-TOKEN: <your-token>" \ "http://10.2.10.5/api/v4/projects/mmis%2Fmmis-2025"
위 명령어를 입력하면 {id:"1", ...}
과 같은 형식으로 프로젝트의 id를 받을 수 있습니다. 이걸 python 코드에 입력해서 연결하도록 할 겁니다.
- 프롬프트
코드리뷰를 위해서는 diff_text
를 확인하도록 설정하고, 코드리뷰의 일관성을 위한 프롬프트를 추가합니다.
system
프롬프트는 항상 적용될 프롬프트이고, user
프롬프트는 코드리뷰를 위한 프롬프트를 작성합니다.
저는 다음과 같이 작성했습니다.
messages = [
{"role": "system", "content": "You are a senior backend/frontend developer with 20 years of experience in Vue.js 3 and Java Spring. You MUST conduct the code review in Korean language ONLY. DO NOT respond in English under any circumstances."},
{"role": "user", "content": f"{diff_text} 의 변경사항을 확인해서 코드리뷰를 작성하세요.\n다음과 같은 형식을 반드시 따르세요:\n\n## 1. 요약\n[전체 코드 변경의 목적과 내용을 1-2문장으로 요약]\n\n## 2. 주요변경\n\n\n| 파일명 | 변경 내용 요약 | 문제점 (있는 경우) |\n|--------|--------------|------------------|\n[표 형식으로 주요 코드 변경사항을 나열하고, 하나의 파일에 여러 내용이 있는 경우, 여러 내용을 list로 표현]\n\n## 3. 피드백\n- 오타 및 실수\n- 함수 인자 오류\n- 디버깅 코드 포함 여부\n- 팀 컨벤션 위반\n- 보안 이슈\n- 성능 이슈\n- 코드 스타일 개선사항\n- 제안사항. \n [만약 Feedback의 list 중 제안할 사항이 없는 list는 생략, 추가적인 내용을 넣고 싶으면 list로 만들어서 추가.] \n 영어로 된 파일명(예 - index.ts, Header.vue 등), 타입(예 - string, any 등), 코드(예 - return null 등)은 백틱(`)으로 감싸는 Markdown형태로 작성할 것."}
]
전체 코드는 다음과 같습니다.
import requests
import time
from openai import OpenAI
# 기본 설정
GITLAB_URL = "http://101.1.1.109" # 자신의 서버 주소(예시)
GITLAB_TOKEN = "glpat--UMWI92xkJSkmMC" # 자신의 Gitlab token(예시)
LLM_URL = "http://101.1.10.109:11434/v1/" # Ollama 주소(예시)
LLM_MODEL = "qwen3:32b-q8_0" # 사용할 LLM 모델(예시)
# OpenAI 호환 클라이언트
client = OpenAI(
base_url=LLM_URL,
api_key="apikey" # 형식상 필요한 파라미터
)
# 중복 리뷰 방지용
seen_mr = set()
def fetch_open_mrs():
headers = {"PRIVATE-TOKEN": GITLAB_TOKEN}
project_id = "1" # 자신의 프로젝트 ID(예시)
url = f"{GITLAB_URL}/api/v4/projects/{project_id}/merge_requests?state=opened"
res = requests.get(url, headers=headers)
res.raise_for_status()
return res.json()
def get_mr_diff(project_id, mr_iid):
headers = {"PRIVATE-TOKEN": GITLAB_TOKEN}
res = requests.get(f"{GITLAB_URL}/api/v4/projects/{project_id}/merge_requests/{mr_iid}/changes", headers=headers)
res.raise_for_status()
return res.json()["changes"]
def post_mr_comment(project_id, mr_iid, comment):
headers = {"PRIVATE-TOKEN": GITLAB_TOKEN}
data = {"body": comment}
res = requests.post(f"{GITLAB_URL}/api/v4/projects/{project_id}/merge_requests/{mr_iid}/notes", headers=headers, data=data)
res.raise_for_status()
# 기존 댓글에 AI가 이미 리뷰한 내용이 있는지 확인
def comment_already_exists(project_id, mr_iid):
headers = {"PRIVATE-TOKEN": GITLAB_TOKEN}
url = f"{GITLAB_URL}/api/v4/projects/{project_id}/merge_requests/{mr_iid}/notes"
res = requests.get(url, headers=headers)
res.raise_for_status()
notes = res.json()
for note in notes:
if "AI 코드리뷰:" in note["body"]:
return True
return False
# AI 식별 태그를 포함하여 요약 생성
def summarize_diff(diff_text):
try:
messages = [
{"role": "system", "content": "You are a senior backend/frontend developer with 20 years of experience in Vue.js 3 and Java Spring. You MUST conduct the code review in Korean language ONLY. DO NOT respond in English under any circumstances."},
{"role": "user", "content": f"{diff_text} 의 변경사항을 확인해서 코드리뷰를 작성하세요.\n다음과 같은 형식을 반드시 따르세요:\n\n## 1. 요약\n[전체 코드 변경의 목적과 내용을 1-2문장으로 요약]\n\n## 2. 주요변경\n\n\n| 파일명 | 변경 내용 요약 | 문제점 (있는 경우) |\n|--------|--------------|------------------|\n[표 형식으로 주요 코드 변경사항을 나열하고, 하나의 파일에 여러 내용이 있는 경우, 여러 내용을 list로 표현]\n\n## 3. 피드백\n- 오타 및 실수\n- 함수 인자 오류\n- 디버깅 코드 포함 여부\n- 팀 컨벤션 위반\n- 보안 이슈\n- 성능 이슈\n- 코드 스타일 개선사항\n- 제안사항. \n [만약 Feedback의 list 중 제안할 사항이 없는 list는 생략, 추가적인 내용을 넣고 싶으면 list로 만들어서 추가.] \n 영어로 된 파일명(예 - index.ts, Header.vue 등), 타입(예 - string, any 등), 코드(예 - return null 등)은 백틱(`)으로 감싸는 Markdown형태로 작성할 것."}
]
response = client.chat.completions.create(
model=LLM_MODEL,
messages=messages,
temperature=0.1,
max_tokens=2000,
top_p=0.1,
frequency_penalty=0.5,
presence_penalty=0.5
)
return "AI 코드리뷰:\n" + response.choices[0].message.content
except Exception as e:
print(f"❌ AI 리뷰 생성 중 에러 발생: {str(e)}")
return None
def process_merge_requests():
try:
mrs = fetch_open_mrs()
if not mrs:
print("📭 처리할 MR이 없습니다")
return
print(f"📋 총 {len(mrs)}개의 MR 처리 시작")
for mr in mrs:
key = f"{mr['project_id']}:{mr['iid']}:{mr['updated_at']}"
# AI 댓글이 있는지 먼저 확인
has_ai_comment = comment_already_exists(mr["project_id"], mr["iid"])
# 이미 처리됐고 AI 댓글도 있는 경우만 스킵
if key in seen_mr and has_ai_comment:
print(f"⏭️ 이미 처리된 MR: {mr['title']}")
continue
# AI 댓글이 없는 경우 seen_mr에서 제거하여 재처리 가능하게 함
if not has_ai_comment and key in seen_mr:
seen_mr.remove(key)
print(f"🔄 댓글이 삭제된 MR 재처리: {mr['title']}")
# 처리 시작하면 seen_mr에 추가
seen_mr.add(key)
print(f"📝 처리 중: {mr['title']}")
# 이미 AI 댓글이 있는 경우 스킵하고 다음 MR로 진행
if has_ai_comment:
print("⚠️ 이미 AI 코드리뷰가 작성됨, 다음 MR로 진행")
continue
try:
changes = get_mr_diff(mr["project_id"], mr["iid"])
diff_text = "\n".join([c["diff"] for c in changes])
if not diff_text.strip():
print("⚠️ 변경사항 없음, 다음 MR로 진행")
continue
summary = summarize_diff(diff_text)
if summary is None:
print("❌ AI 리뷰 생성 실패, 다음 MR로 진행")
continue
post_mr_comment(mr["project_id"], mr["iid"], summary)
print("✅ 댓글 등록 완료")
except Exception as e:
print(f"❌ MR 처리 중 에러 발생: {mr['title']} - {str(e)}")
continue
print("✨ 모든 MR 처리 완료")
except Exception as e:
print(f"❌ MR 목록 처리 중 에러 발생: {str(e)}")
if __name__ == "__main__":
print("🚀 GitLab Merge Request 자동 리뷰 시작")
while True:
try:
process_merge_requests()
except Exception as e:
print(f"❌ 전체 프로세스 에러 발생: {str(e)}")
print("⏳ 1분 대기 중...")
time.sleep(60) # 1분 간격
주요 로직은 다음과 같습니다.
- 프로젝트 ID와 토큰을 통해 열려있는 MR을 가져옵니다.
- 가져온 MR의 변경사항을 가져옵니다.
- 변경사항을 통해 코드리뷰 댓글을 작성합니다.
- 1분 뒤 폴링할 때, 이미 'AI 코드리뷰'가 포함된 댓글이 추가된 MR이면 스킵합니다.
- 도중에 관리자가 AI코드리뷰를 제거하면 재생성합니다.
4. 서버에서 GitLab clone
이제 서버에서 GitLab clone을 해봅시다. 저는 /opt
디렉토리에 clone을 진행했습니다.
sudo git clone http://101.1.1.109/mr-reviewer.git
그리고 먼저 requirements.txt를 통해 필요한 패키지를 설치합니다. (python 3 이상 필요)
sudo pip install -r requirements.txt
그리고 mr_reviewer.py를 실행해봅시다.
sudo python mr_reviewer.py
그런데 이렇게 하면, 단 한번만 실행되고 종료됩니다. 이를 해결하기 위해 systemd를 설정해봅시다.
5. systemd 설정
systemd를 설정하면 다음과 같은 기대효과를 얻을 수 있습니다.
- 서버 부팅 시 자동 실행
- 꺼져도 자동 재시작
- 로그 확인 가능
- 경로 확인 (mr_reviewer.py의 경로와 python 경로)
which python3
- 서비스 유닛 파일 생성
sudo nano /etc/systemd/system/gitlab-mr-reviewer.service
내용은 아래와 같이 작성합니다. 표시된 내용에서는 위에서 확인한 경로를 넣으면 됩니다.
[Unit]
Description=GitLab MR Reviewer Service
After=network.target
[Service]
ExecStart={파이썬경로} {mr_reviewer.py경로}
WorkingDirectory={mr_reviewer.py 경로의 바로 위 디렉토리 경로}
Restart=always
User={youruser}
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
- 퍼미션 점검
sudo chmod +x /opt/mr-reviewer/mr_reviewer.py
chmod: changing permissions of '/opt/mr-reviewer/mr_reviewer.py': Operation not permitted
- 현재 권한 확인
ls -l /opt/mr-reviewer/mr_reviewer.py
-rw-r--r-- 1 root root 3085 Apr 30 07:00 /opt/mr-reviewer/mr_reviewer.py
- 권한부여
sudo chmod +x /opt/mr-reviewer/mr_reviewer.py
- systemd 등록 및 실행 (순서대로 실행)
sudo systemctl daemon-reload
sudo systemctl enable gitlab-mr-reviewer.service
sudo systemctl start gitlab-mr-reviewer.service
- 상태확인
sudo systemctl status gitlab-mr-reviewer.service
정상이라면
● gitlab-mr-reviewer.service - GitLab MR Reviewer Service
Loaded: loaded (/etc/systemd/system/gitlab-mr-reviewer.service; enabled)
Active: active (running) ...
이런 식으로 나옵니다.
- 로그 확인
journalctl -u gitlab-mr-reviewer.service -f
- 재시작
sudo systemctl restart gitlab-mr-reviewer.service
이렇게 하면 서버 부팅 시 자동으로 실행되고, 꺼져도 자동으로 재시작됩니다.
결과
- deepseek-coder-33b : 한국어로 작성 시, 가끔 문법에 안 맞거나 한문이 섞여서 나오는 경우가 있음. 영어로 작성 시 가장 작성이 잘 됨. 보안상 문제될 수 있음.
- qwen3-32b : 영문과 한국어가 모두 표출됨. 퀄리티는 나름 준수함.
- exaone3.5-32b : LG LLM답게 한국어 작성이 가장 잘 됨. 퀄리티는 조금 낮음.
이 외에도 llama, phi4, mistral 등 써봤는데, deepseek-coder-33b나 exaone3.5-32b가 가장 좋았습니다. 회사 프로젝트에는 보안을 위해 deepseek보다는 exaone을 사용하려고 합니다. 초기 목표(동료의 코드를 리뷰하기 전 확인 용도)로는 크게 성능이 떨어지지 않습니다.
인사이트
- 오픈소스 LLM의 실용성
- 오픈소스 LLM으로 충분히 실무에서 활용 가능한 AI시스템을 구축할 수 있을 것 같습니다. 작년에 오픈소스 LLM을 사용했을 때 퀄리티를 생각하면, 많이 폼이 올라온 것 같습니다.
- 대형 diff의 경우 LLM이 프롬프트를 일부 무시하거나, 중요한 파일만 요약하는 경향이 있었습니다. 향후에는 다음과 같은 방식으로 개선할 수 있을 것 같습니다:
diff_text
전처리: auto-format, test 파일 제외, 주석 제거 등으로 요약 길이 조절- 파일별 리뷰 분리 처리: 한 MR에 여러 번의 LLM 호출
- LLM 입력 제한 대응: 토큰 수 계산 → 초과 시 분할 처리
- 이를 위해 파인튜닝 및
diff_text
를 줄이는 방법을 찾아봐야 할 것 같습니다.
- 시스템 자동화와 유지관리 전략
systemd
를 활용해 자동 실행 및 복구 기능을 추가함으로써, 신뢰성을 확보하고 로그 기반 모니터링을 통해 문제를 쉽게 찾을 수 있습니다.Webhook
을 통해 실시간 처리를 하고 싶었습니다. 하지만 내부망에서 Webhook 수신 서버를 열기위한 별도 설정, 실패 시 재시도 처리 어려움 등으로 인해 폴링이 더 안정적인 운영 전략이라고 생각했습니다. 1분 간격으로 폴링하는 것이 서버에 큰 부담이 되지도 않구요.
- 개인적 인사이트
- 남는 서버, 오픈소스, 그리고 코드 몇 줄로 실제 업무를 자동화하고, 반복적인 피드백을 줄이는 시스템을 만들었다는 점에서 굉장히 뿌듯합니다.
- 사내 유일의 Front-End 엔지니어로서, 여러 팀원의 코드를 리뷰하고 피드백하는 것이 쉽지 않았습니다. 이번 프로젝트를 통해 코드 리뷰를 자동화하고, 피드백을 줄이는 시스템을 만들었습니다. 적어도 코드 리뷰 전, 팀원의 코드를 이해하는 데 좀 더 효율적이 되었습니다.
- 추후 프롬프트 엔지니어링을 기반으로 커스터마이징이 가능하다는 점에서 회사 문화에 최적화된 AI 서비스를 만들 수 있지 않을까도 생각합니다.