tools & libs/ETC

Gitlab actions를 이용한 algorithm MR 피드백 처리

  • -

이번 포스트에서는 github에 자바 알고리즘 문제에 대한 mr 이 전달된 경우 자동으로 AI가 피드백을 달아주도록 파이프라인을 구축해보자.

🚀 개요

  • 목표: Merge Request(MR)가 생성되면 AI가 자동으로 코드를 읽고 분석 댓글 작성
  • 사용 기술: GitLab CI/CD, Python, Google Gemini 2.5 Flash API
  • 환경: Synology NAS (Self-hosted GitLab Runner)

 

준비물

 

Google Gemini API 키 발급

Google AI Studio에서 API 키를 발급받는다. Gemini 2.5 Flash 모델 정도 쓰면 매우 강력한 추론 성능을 무료(Free Tier 기준)로 사용할 수 있다.

Get API Key로 키 발급 받기

 

GitLab Access Token 발급 받기

MR에 분석 결과를 comment로 달기 위해서는 권한이 필요하다. 

GitLab의 preferences > Personal access tokens를 이용해서 token을 생성한다.

정확히 필요한 권한은 read_repository, write_repository일것 같은데 혹시 몰라서 다 줌 ㅠㅠ

 

NAS에 Runner 배치하기

 

Runner 구성

Runner는 GitLab의 작업을 실제로 처리할 도구로 NAS의 Docker(Container Manager)환경으로 구축한다.  Docker를 구성할 때 시스템 변수를 써야 하므로 docker composer로 구성하는 것이 정신 건강에 좋다.

Synology의 Container Manager > Project > 생성을 선택한다.

프로젝트를 이용한 docker compose 설정

경로는 설정 파일이 저장될 경로이다. [경로설정]버튼을 클릭해서 구성해준다.

다음으로 원본 항목을 [docker-compose.yml 만들기]를 선택하고 다음 내용을 붙여 넣는다.

version: '3.8'
services:
  runner:
    image: gitlab/gitlab-runner:latest
    container_name: gitlab-runner
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./config:/etc/gitlab-runner

다음으로 나오는 [웹 포털 설정]은 신경쓰지 말고 그냥 다음으로 이동해서 마무리 한다.

 

컨테이너 재시작

설정을 마쳤으면 container를 재시작 해준다.

 

Gitlab CI/CD 설정

 

Runner 등록

이제 gitlab의 project에 위에서 생성한 Runner를 등록해보자.

Settings > CI/CD > Runners에서 [Create project runner]를 선택한다.

runner 생성

다음 화면에서 Tags에 이름을 입력해주고 [Run untagged jobs]에 체크하자. 안그러면 태그가 안맞는 경우 처리가 pending 된다.

[create runner]를 선택하면 사용할 runner에서 해야할 일이 나온다.

runner에서 할 일

일단 platform에서는 linux이다.(synology가 linux!)

다음으로 Step 1에 있는 스크립트를 복사해서 synology에서 실행해준다. 권한 때문에 앞단에 sudo docker exec -it 가 추가되었다.

 

# 시놀로지 터미널에서 입력
sudo docker exec -it gitlab-runner gitlab-runner register \
  --url https://lab.myserver.com \
  --token [아까_발급받은_토큰]

 

처음에 비밀번호를 물어보는 부분이 지나면 interactive하게 몇 가지 설정을 하게 된다. 중요한 부분 executor에 docker,  docker image에 python:3.9-slim 부분이다.

Enter the GitLab instance URL (for example, https://gitlab.com/):
[https://lab.myserver.com]: 
Verifying runner... is valid

Enter a name for the runner. This is stored only in the local config.toml file:
[9d605761ba1f]: 

Enter an executor: ssh, parallels, docker-windows, docker+machine, docker-autoscaler, instance, custom, shell, virtualbox, docker, kubernetes:
docker

Enter the default Docker image (for example, ruby:3.3):
python:3.9-slim

Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!
 
Configuration (with the authentication token) was saved in "/etc/gitlab-runner/config.toml"

마지막으로 Enable for this project를 이용해서 runner를 동작시킨다.

 

키 등록

GitLab의 프로젝트로 이동 Settings > CI/CD > Variables > Project Variables에 다음과 같이 2개의 키를 등록한다.

GEMINI_API_KEY와 GITLAB_TOKEN 키 등록

이 값들은 준비물 쪽에서 생성한 값들이다.

 

프로젝트

 

코드 리뷰어 스크립트 작성

실제 코드를 리뷰할 프로그램을 만들어준다. 간단히 뭔가를 할때는 파이썬이 최고! 프로젝트 root에 code_reviewer.py 파일을 작성해주자.

import os
import requests
import google.generativeai as genai

def get_full_file_content(project_id, file_path, ref, token, gitlab_url):
    """GitLab API를 통해 파일의 전체 원본 소스코드를 가져옴"""
    # 파일 경로의 /를 %2F로 인코딩
    encoded_path = file_path.replace("/", "%2F")
    url = f"{gitlab_url}/api/v4/projects/{project_id}/repository/files/{encoded_path}/raw?ref={ref}"
    headers = {"PRIVATE-TOKEN": token}
    
    try:
        res = requests.get(url, headers=headers, timeout=30)
        res.raise_for_status()
        return res.text
    except Exception as e:
        print(f"   ❌ 파일 내용 추출 실패 ({file_path}): {e}")
        return None

def get_java_changes():
    print("--- [1단계] GitLab MR에서 전체 자바 소스코드 추출 ---")
    token = os.getenv("GITLAB_TOKEN")
    project_id = os.getenv("CI_PROJECT_ID")
    mr_iid = os.getenv("CI_MERGE_REQUEST_IID")
    gitlab_url = os.getenv("CI_SERVER_URL")
    # 학생이 작업 중인 소스 브랜치 명
    source_branch = os.getenv("CI_MERGE_REQUEST_SOURCE_BRANCH_NAME")

    # MR의 변경 파일 목록 가져오기
    url = f"{gitlab_url}/api/v4/projects/{project_id}/merge_requests/{mr_iid}/changes"
    headers = {"PRIVATE-TOKEN": token}
    
    try:
        response = requests.get(url, headers=headers, timeout=30)
        response.raise_for_status()
        res = response.json()
        
        full_code_data = ""
        changes = res.get('changes', [])
        print(f"✅ 총 {len(changes)}개의 변경 파일을 확인했습니다.")
        
        found_java = False
        for change in changes:
            file_path = change['new_path']
            # 자바 파일이고 삭제되지 않은 경우만 전체 내용 가져오기
            if file_path.endswith('.java') and not change.get('deleted_file'):
                found_java = True
                print(f"   📂 전체 분석 대상: {file_path}")
                
                # 원본 소스코드 전체 가져오기
                content = get_full_file_content(project_id, file_path, source_branch, token, gitlab_url)
                if content:
                    full_code_data += f"\n[FILE START: {file_path}]\n"
                    full_code_data += content
                    full_code_data += f"\n[FILE END: {file_path}]\n"
        
        return full_code_data if found_java else None
    except Exception as e:
        print(f"❌ GitLab API 오류: {e}")
        return None

def get_gemini_feedback(all_code):
    print("--- [2단계] Gemini 2.5 Flash 모델로 전체 코드 리뷰 생성 ---")
    api_key = os.getenv("GEMINI_API_KEY")
    genai.configure(api_key=api_key)

    # 사용자 환경에서 확인된 gemini-2.5-flash 모델을 최우선 사용
    try:
        # 모델 목록을 다시 확인하여 2.5 버전 선택
        models = [m.name for m in genai.list_models()]
        if 'models/gemini-2.5-flash' in models:
            model_name = 'gemini-2.5-flash'
        elif 'models/gemini-2.0-flash' in models:
            model_name = 'gemini-2.0-flash'
        else:
            model_name = 'gemini-1.5-flash'
            
        print(f"🚀 활성화된 모델: {model_name}")
        model = genai.GenerativeModel(model_name)
    except Exception as e:
        print(f"⚠️ 모델 선택 중 오류, 기본값 시도: {e}")
        model = genai.GenerativeModel('gemini-2.5-flash')

    prompt = f"""
    너는 구글 최고의 자바 소프트웨어 엔지니어이자 알고리즘 코치야.
    학생이 제출한 다음 자바 소스 코드 '전체'를 읽고 심도 있게 리뷰해줘.
    
    리뷰 지침:
    1. 코드의 전체적인 설계와 알고리즘 선택이 적절한지 평가해줘.
    2. 시간 복잡도($O(N)$ 표현)와 공간 복잡도를 정확히 분석해줘.
    3. 자바 1.8 기반으로 Java Collection Framework를 더 잘 활용할 수 있는 리팩토링 방안을 제시해줘.(절대 그 이상의 JDK 문법은 안돼!)
    4. 변수명, 주석, 가독성 등 클린 코드 관점에서의 피드백을 줘.
    5. 추가로 시도해볼 수 있는 새로운 방법이 있으면 알려줘.

    소스 코드 내용:
    {all_code}
    """
    
    try:
        response = model.generate_content(prompt)
        print("✅ Gemini 리뷰 생성 성공!")
        return response.text
    except Exception as e:
        print(f"❌ Gemini API 요청 중 오류: {e}")
        return None

def post_comment(feedback):
    print("--- [3단계] GitLab MR에 최종 피드백 등록 ---")
    token = os.getenv("GITLAB_TOKEN")
    project_id = os.getenv("CI_PROJECT_ID")
    mr_iid = os.getenv("CI_MERGE_REQUEST_IID")
    gitlab_url = os.getenv("CI_SERVER_URL")

    url = f"{gitlab_url}/api/v4/projects/{project_id}/merge_requests/{mr_iid}/notes"
    headers = {"PRIVATE-TOKEN": token}
    data = {"body": f"## 🤖 Gemini 2.5 AI의 전체 코드 심층 리뷰\n\n{feedback}"}
    
    try:
        res = requests.post(url, headers=headers, data=data, timeout=30)
        res.raise_for_status()
        print("✅ MR 댓글 등록 완료!")
    except Exception as e:
        print(f"❌ 댓글 등록 실패: {e}")

if __name__ == "__main__":
    print("🚀 Gemini 2.5 Flash 기반 AI 리뷰어 가동")
    java_content = get_java_changes()
    
    if java_content:
        feedback_text = get_gemini_feedback(java_content)
        if feedback_text:
            post_comment(feedback_text)
    else:
        print("⏭️ 분석할 자바 코드가 없어 종료합니다.")
    
    print("🏁 전체 작업 종료")

 

파이프라인 설정

다음으로 merge request가 왔을 때 위 스크립트를 수행하도록 파이프라인을 작성해주자. project root에 .gitlab-ci.yml 파일을 만들어주자. 이 파일은 GitLab 서버에서 ci를 위해 기본적으로 사용하는 config 파일 이름이다.

stages:
  - ai_review

gemini_code_review:
  stage: ai_review
  image: python:3.10-slim
  only:
    - merge_requests
  before_script:
    - pip install -q -U requests google-generativeai
  script:
    - python code_reviewer.py

 

코드리뷰어 스크립트 변수화

코드리뷰어 스크립트가 공개되는 부분이 지저분하다면 변수화 할 수도 있다. 

일반적인 변수와는 조금 다른데 일단 Type은 File, Visibility는 Visible을 선택해준다. 그리고 Value 항목에 파일의 내용을 작성해주자.

이제 스크립트 파일을 부를 때 변수를 이용하면 된다.

stages:
  - ai_review

gemini_code_review:
  stage: ai_review
  image: python:3.10-slim
  only:
    - merge_requests
  before_script:
    - pip install -q -U requests google-generativeai
  script:
    - python $REVIEW_SCRIPT

 

'tools & libs > ETC' 카테고리의 다른 글

SSH 키 생성  (0) 2026.01.14
[크롬개발자도구] 필터  (10) 2025.07.10
[synology] VPN 설치  (1) 2025.07.07
[synology] HTTPS 설정  (0) 2025.07.06
[oneNote]지우개, 브러쉬 단축키 조합  (0) 2022.09.05
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.