자체적으로 채점자동화를 위한 웹 서비스를 만든 후기를 작성해봅니다. 소규모 인원을 대상으로 GCP 서비스를 조합해서 편하게 채점할 수 있도록 만들었고 그 과정에서 개발, 배포, 서비스 운영에 대한 전반적인 내용을 다룹니다. 그 과정에서 일어난 시행 착오를 공유하여 독자들이 서버리스 자동화를 설계할 때 사전에 고려해야할 점 등을 예습하는 글이 되길 바랍니다.
1. 채점 서비스의 필요성
1.1. 기존 채점 프로세스의 문제점
- 문제1) 과제 제출 -> 피드백 까지의 시간 지연
기존에는 학생들이 과제를 제출하면 이를 받아 채점 튜터들이 채점하고 리뷰를 하는 구조로 진행 되어있습니다.
학생들의 답안 제출 -> 교육 시스템에 적재 -> 튜터에게 할당(10명 등) -> 각자의 로컬 컴퓨터에서 코드를 실행 & 리뷰를 작성
답안 제출로부터 학생들이 피드백을 받을 때까지 최소 1주 - 최대 2주의 시간이 소요되었습니다. 문제를 푸는데 이렇게 시간이 오래 걸린다면 코드를 작성할때의 기억들이 휘발되어 학습효과가 떨어집니다. 따라서 위 프로세스 중에 채점하는 시간을 줄이고자 했습니다.
- 문제2) 환경 의존성 문제
데이터 분석을 포함한 프로그래밍은 환경 설정에 따라서 그 결과값의 차이가 있을 수 있습니다. 예컨데 SQL로 데이터를 불러온다고 해도 각자 데이터를 불러오는 DDL/DML이 다르거나 환경이 다르면 다른 결과물을 산출할 수 있습니다. 과제를 풀 때나, 채점을 할 때나 각자의 로컬 환경에 MySQL-DBeaver로 설치를 하는 환경이 문제였습니다.
따라서, 위 문제를 해결하기 위해서 하나의 서비스를 만들게 되었습니다. 제출된 코드에 대한 결과는 채점 자동화는 물론, 코드에 대한 정성적 피드백도 Gemini로 받았으면 좋겠다고 생각했습니다.
2. 채점 시스템 설계
2.1. 기존 프로세스 개선
사실 서비스를 시작하기에 앞서 이 서비스를 전체적으로 알리고 프로세스를 개선하는 필요성이 있다고 생각 했습니다. 따라서 과제를 제출하는 과정부터 프로세스를 개선하기 위해서 교육업체 담당자에게 설명하기 시작했습니다.
보통 과제가 나가는 과정은 튜터의 과제 제작 -> 학생들이 풀이 -> 튜터들의 과제 채점 -> 결과 송부 의 형식으로 진행됩니다. 일관된 채점을 위해서는 먼저 과제 제작에 대한 설계도 필요했습니다. 그래서 과제를 제출할 시 몇가지 기준을 세웠습니다.
- 과제에 대한 의도, 목적 등을 작성할 것
- 과제에 채점 기준을 명시할 것
- 모범 답안으로 해설지를 작성할 것(정답 코드 포함)
이렇게 되면 일관된 과제 제출도 가능해지고 추후 채점을 Gemini에 요청할 때 Prompt 의 기반이 될 것이므로 이 형식을 같이 잡아나갔습니다.
2.2. 채점 서비스 설계
위 업무 프로세스 개선과 동시에 서비스 설계를 위한 서비스와 스택을 정리해나갔습니다. 가장 중점적인 것은 GCP의 on-demand 서비스인 Cloud run으로 서버리스 구조를 선택하게 되었습니다. 서버를 관리하고 올리기엔 관리포인트가 클 것이고 간단하게 만들 수 있을 것 같아 선택했습니다.(하지만 이 선택을 후회하게됩니다) 웹사이트는 Streamlit으로 정했고, . 또한 과제는 크게 SQL와 Python으로 나뉘는데 SQL의 경우 실제 MySQL 8.0 버전이 필요해서 GCP의 Cloud SQL을 사용하였습니다. 파이썬의 경우 내장함수으로 exec 를 이용해서 채점 기능을 구현했습니다.
- 기술 스택
- 서비스: Cloud run, Cloud SQL, Gemini(gemini-2.0-flash)
- 백엔드/프론트: Python, Steamlit(Python)
과제는 확장될 수 있지만 기본적으로 SQL, Python, Pandas에 대한 문제를 기본으로 결과를 만들어 갔습니다. 크게 기능은 모범 정답과 학생 정답을 채점하는 기능 1번과 정성 평가를 하는 기능2가 있습니다.
- 기능 1) Gemini를 이용한 정성평가( 코드의 효율성과 개선점을)을 함께 평가
- 사전에 선정해놓은 기준으로 Prompt engineering에 따라 점수를 측정
- 기능 2) 학생들의 답과 모범 정답 비교
- SQL) Cloud상의 SQL 서비스에 직접 쿼리를 날려 가져와 출력
- Python) exec 내장함수를 이용해서 테스트 케이스마다의 출력
- Pandas) 코드저장소의 csv파일을 불러와 데이터 가공한 결과를 출력
3. 핵심 기능 구현 상세
기능1) 정성 평가
- 구현 내용
정성 평가는 몇가지 기준에 따라 채점됩니다. 2.1. 기존 프로세스 변경 제안에서 말한 것처럼 과제에 대한 의도, 채점 기준, 모범 답안 등의 대한 정보와 사전 프롬프트를 받아 결과를 뱉어냅니다. 다음은 `prompt_builder.py` 의 결과물이며 Langchain의 from_template 메소드처럼 사전에 변경 가능한 구조로 설계하여 확장정을 높였습니다.
당신은 {category} 과제 채점 전문가입니다.
- 문제: {question}
- 모범답안: {model_answer}
- 학생답안: {student_answer}
- 평가기준: {evaluation_criteria}
- 쿼리 상태: {query_status}
{grading_criteria_str}
학생의 답안이 모범답안과 다르더라도, 논리적/문법적 오류가 없다면 높은 점수를 주세요.
아래 조건을 꼭 지켜서 평가해 주세요.
1. 각 평가기준(정확도, 가독성, 효율성, 분석력 등)에 대해 **5점 단위(0, 5, 10, ..., 100)로만** 부분 점수를 각각 매겨주세요. (예: 정확도 50점, 가독성 20점, 효율성 15점)
2. 각 기준별 점수의 합이 100점이 되도록 최종 점수도 함께 출력해 주세요.
3. 각 평가기준별로 '참고할 점'을 한두 문장으로 구체적으로 작성해 주세요. (예: 잘한 점, 부족한 점, 개선점 등)
4. 마지막 피드백은 평가자가 참고할 수 있도록, 학생에게 직접 전달하는 친근한 말투가 아니라 틀린 부분, 부족한 점, 개선점 등 객관적이고 구체적인 정보 위주로 작성해 주세요.
결과는 아래 형식으로 출력해 주세요.
[기준별 점수]
정확도: 45 / 50
가독성: 15 / 20
효율성: 10 / 15
분석력: 10 / 15
[기준별 참고사항]
정확도: WHERE 조건에서 'Attrited Customer' 필터가 누락됨.
가독성: 쿼리 구조는 명확함.
효율성: CTE 활용이 부족함.
[최종 점수] 80
[피드백] 전반적으로 쿼리 구조는 명확하나, 일부 필터 조건과 효율성에서 아쉬움
- 문제: 프롬프트에 대한 불확실성
결국 LLM에 응답한 결과는 매번 결과가 달라지는 문제가 있습니다. 특히 이따금 결과값이 json 형태로 뱉어내어서 기대한 형태의 출력이 안되는 경우도 많았는데, 추후에 Langhain 스택을 추가하여 StructuredOuputParser 사용하는게 좀 더 나을 것 같습니다.
기능2) 채점 기능 - SQL
- 구현 내용
SQL은 MySQL로 문제를 냈다면 해당 클라우드가 있어야합니다. 이를 위해 Cloud SQL에 인스턴스를 하나 파서 DDL을 이용하여 데이터를 삽입하였습니다. 문제는 공통된 DDL을 코드로 제시했음에도 불구하고 각자의 환경에서 알아서 데이터를 불러오다보니 미묘하게 결과 데이터가 다른 문제가 있었습니다. 예를 들어 정수/정수 나누기에 대한 결과값 계산 과정에서 decimal 사용 유무에 따라 결과 값이 달라서 결국 소수점 2째짜리 이내는 정답으로 처리하도록 임시 방편을 택했습니다.
- 문제: 비용 이슈
반면 cloud 스펙에 대한 이슈도 있었는데, 원본 데이터가 그렇게 크지 않지만 초기 스펙은 램 2기가에 2CPU를 기본값을 배정했는데 매일 5천원씩 너무 많이 과금이 되는 문제가 있었습니다. 샌드박스 옵션인데도 불구하고 과제에 적합한 사이즈는 아니 였으며, DB의 완정성을 보장하기 위한 백업이나 관리의 비용이 많이 나가는 것을 확인했습니다. 이를 확인하고 다운 그레이드 절차를 진행했습니다. 결과적으로 일 5000원 유지비 -> 850원 정도의 유지비로 절감할 수 있었습니다.
- 데이터 백업 기능 해제(운영 DB가 아니므로)
- CPU 2코어 수 -> 1 햐향 조정
- 메모리 2GB -> 600 MB 하향 조정
기능2) 채점 기능 - Python / Pandas
- 구현 내용
파이썬의 경우내장 함수 exec를 통해 채점을 구현했습니다. 문제를 dictionary 형식으로 저장하여 추후 불러와 사용했습니다. python의 결과는 모범정답의 key값에 따라 다양한 체크포인트를 거쳐 최종 정답으로 처리됩니다. 예컨데 문제1번이 단순하게 평균을 내는 함수라면 model_answer의 코드를 exec 내장함수에 넣고 test_cases의 input값을 전달인자로 실행하여 expected와 같은지 확인하면 됩니다.
QUESTIONS = {
"PYTHON_1": {
"title": "숫자 리스트의 평균 계산하기",
"content":
"""
- **배경**: 한 소매점에서 재고를 계산해야 합니다. 주어진 재고의 평균을 계산해보세요.
- **문제 의도**
- 리스트의 자료형을 이해
- 내장 함수의 활용
- **요구 사항**
- 함수명: `calculate_stock`
- 해당 함수는 리스트의 전달 인자를 받음
""",
"model_answer":
"""
def calculate_stock(numbers):
return sum(numbers) / len(numbers)
""",
"function_name": "calculate_stock",
"test_cases": [{"input": [10, 20, 30, 40, 50], "expected": 30.0}],
"evaluation_criteria": [
{"id": "P1", "description": "리스트의 합과 길이를 이용해 평균을 계산한다."},
{"id": "P2", "description": "내장 함수(sum, len) 사용"}
]
}
Pandas의 경우 자료형과 결과값 두개를 동시에 비교해야 하기때문에 "test_cases"에 자료형을 저장하는 "expected_type"과 "expected"를 동시에 넣어 채점하게 됩니다. passed 변수를 True로 초기화 해놓고 각 기준을 통과하지 못하면 False으로 Flag를 세워 최종적으로 True로 살아남는 코드를 정답으로 처리하였습니다.
"PYTHON_7": {
"title": "결측치 확인",
"content":
"""
- **배경:** 데이터를 불러 왔을 때 각 컬럼에 결측치 유무를 확인하는 것은 중요합니다. 컬럼의 결측치를 확인해보세요.
- **문제 의도**
- DataFrame의 함수를 활용
- **요구 사항**
- 함수명: `get_missing`
- 컬럼별 결측치 수를 예시 결과와 같이 출력
""",
"model_answer":
"""
def get_missing(df):
return df.isnull().sum()
""",
"function_name": "get_missing",
"test_cases": [ {"input": "df_sample",
"expected_type": 'Series',
"expected": {
"Route": 1,
"Total_Stops": 1,
"Airline": 0, "Date_of_Journey": 0, "Source": 0,
"Destination": 0, "Dep_Time": 0, "Arrival_Time": 0,
"Duration": 0, "Additional_Info": 0, "Price": 0
}}
],
"evaluation_criteria": [{"id": "P1", "description": "DataFrame의 isnull().sum()을 활용한다."}]
}
또한, 데이터를 불러와야하는 문제가 있었는데, 매번 문제마다 데이터를 불러오면 in-ouput관점에서 비효율적일 것 같아서 찾아보니 stremlit에서 캐시 기능을 구현할 수 있는 데코레이터를 발견했습니다. 이를 사용해서 효율적인 저장을 구현했습니다.
@st.cache_data
def load_sample_dataframe():
"""
이 함수는 처음 호출될 때 단 한 번만 실행되고 캐싱됩니다.
이후 호출에서는 캐시된 데이터를 반환합니다.
"""
# print('데이터로드 시작')
df = pd.read_csv('./data/7th/flight_data.csv', sep=';')
# print(df.head(3))
# print('데이터로드 종료')
return df
- 이슈: 데이터 로딩 문제
문제는 Pandas에 있었는데, 데이터 분석에 관련하기 때문에 외부에서 데이터를 불러와서 채점하게 됩니다. uci 레포나 github 등일수도있고 mysql 서버일 수 도 있습니다. 하지만 cloud service은 외부 데이터를 가져옴에 있어서 인증을 요구하다보니 이걸로 3일 정도 삽질을 했습니다. 사실 별거아닌 문제이지만, cloud run 서비스 특성상 에러를 찾기가 매우 불편합니다. 로그를 뒤져보면 되긴하지만 내가 실제로 서버를 띄우는 것 처럼 bash형태로 로그를 찾는게 아니다 보니 데이터 로딩에서 문제가 일어난다는 사실 자체를 찾는데도 오래 걸렸습니다. 사실 데이터가 7MB 이였어서 큰 문제는 없었지만, 대량의 데이터를 불러온다면 Cloud SQL 에 적재해서 가져오는 방향이 나을 것 같습니다.
4. 개발 과정의 교훈과 팁
- 각 디렉토리를 정확하게 명시
― core: 핵심 모듈
┕ service: 외부 호출되는 서비스( Ex Cloud SQL 등)
┕ streamlit_app: 프론트 엔드 streamlit 을 위한 디렉토리
┕ answer: 정답 저장
┕ data : 데이터 저장
하나의 디렉토리는 하나의 기능 혹은 구현하려고자하는 결과물이 명확하게 정의되면 정리하기가 좋습니다. 혼자 일하던 여럿이 일하던 명확한 디렉토리 설계와 README.md 작성은 업무 시작 시 로딩 속도를 줄여줍니다.
- 생성된 모듈의 import 하는 방법
# module import 문제 없음. 같은 경로
/ main.py
/ module.py
# module import 문제 없음. 같은 경로
┖ core
┖ ─ main.py
┖ dir
┖ ─ __init__.py
┖ ─ module.py
위처럼 모듈을 기능에 따라 나누다보면 결국 타 디렉토리에 모듈을 가져올 상황이 생깁니다. 같은 경로면 import module.py 를 스크립트 상단에 올리면되지만 디렉토리가 서로 다른 경우 문제가 생깁니다. 이럴때 `__init__.py` 라는 초기화 모듈을 해당 디렉토리에 넣어주면 경로를 쉽게 인식할 수 있게 해줍니다.
- Docker를 적극적으로 사용하자
streamlit 을 이용하면 localhost:8501 포트에 자동으로 실행되지만 여기서 재현성이 된다고 Cloud run에서 잘 작동하는 것 은 아닙니다. 기본적으로 로컬은 윈도우 환경이고 cloud 서비스는 linux 환경이기 때문입니다. 그래서 실제로 컨테이너를 띄워서 테스트해보는게 cloud run에 배포했을 때도 오류를 줄일 수 있는 좋은 테스트 환경입니다. Docker 짱장맨!
# base 디렉토리로 이동
cd base
# Docker 이미지 빌드 (Cloud Run 배포용과 동일)
docker build --platform linux/amd64 -t {서비스명} .
# 빌드된 이미지로 Docker 컨테이너 실행하기
docker run -d -p 8501:8080 -m 1g --name {개발 이미지명} {서비스명}
기타 유틸 파일 활용법
- .env: 환경 변수에 대한 정보를 담습니다. 보통 api나 db의 접속 정보 등을 가지고 있으나 credential이기 때문에 `.env .example` 파일을 github에 올리고 환경변수 명을 제외한 정보를 제거하여 형태만 보존합니다.
# .env.example
GEMINI_API_KEY=
SLACK_BOT_TOKEN=
- `.dockerignore`: 도커나이징 할때 굳이 필요 없는 파일을 저장합니다. `.gitignore`와 비슷한데 보통 python 가상환경에 대한 디렉토리나 cache 기타 파일들을 작성합니다. 이걸 명시하지 않으면 가상환경 디렉토리 내용이 통째로 docker화 되기 때문에 작업 효율이 늦어집니다.
# 파이썬 가상환경 폴더
.venv
# 파이썬 캐시 파일
__pycache__/
*.pyc
# Git 폴더
.git
# 기타 운영체제 파일
.DS_Store
- 배포 코드 .sh 화: 일련의 deploy를 위한 코드를 매번 치지말고 .sh 파일을 만들어서 실행시킵니다.
#!/bin/bash
# 스크립트 실행 중 오류 발생 시 즉시 중단
set -e
# 2. Docker 이미지 빌드 (linux/amd64 플랫폼)
echo "Building Docker image..."
docker build --platform linux/amd64 -t {서비스명} .
# 3. Artifact Registry에 맞게 이미지 태그 지정
echo "Tagging image for Artifact Registry..."
docker tag {서비스명} {이미지명}.pkg.dev/{프로젝트}/cloud-run-source-deploy/{서비스명}
# 4. Artifact Registry로 이미지 푸시
echo "Pushing image to Artifact Registry..."
docker push {이미지명}.pkg.dev/{프로젝트}/cloud-run-source-deploy/{서비스명}
# 5. Cloud Run에 배포 (us-central1)
echo "Deploying to Cloud Run..."
gcloud run deploy grading-app \
--image={이미지명}.pkg.dev/{프로젝트명}/cloud-run-source-deploy/{서비스명} \
--region=us-central1 \
--platform=managed \
--vpc-connector={vpc 커넥터} \
--vpc-egress=all \
--allow-unauthenticated \
--memory=2Gi \
--min-instances=1 \
--cpu=2
echo "Deployment complete! 🚀"
- requirements
requirement는 가상환경을 쓴다면 자주 사용하는데 이게 나중에 docker로 배포할때 설치할 라이브러리가 되기 때문에 필수적입니다.
#가상환경 만들기
py -3.12 -m venv {가상환경명}
'''
라이브러리 설치 ...
'''
#패키지 저장
pip freeze > -r requirements.txt
#설치
pip install -r requirments.txt
- mono repo전략
잠시 파이썬 채점자동화시 clodu run function을 고민했던 적이 있었습니다. 코드저장소 측면에서보면 하나의 리포지토리에 두 개이상의 배포 포인트가 있어야하는건데 이럴 땐 그냥 간단히 디렉토리를 나누면 됩니다.
5. 개발 후기
5.1. 그래서 cloud run 다음에도 쓸까?
상황에 따라 다를 것 같은데, 그래도 서버를 파는게 나을 것 같습니다. on-demand서비스라는건 비용 측면에서 좋지마 로그를 찾기가 너무 불편합니다. 데이터 로딩 과정에서 계속 에러가 났는데 확인하기 불가능한 경우가 많았습니다. 더욱이 로컬 Docker에서는 기능을 잘 하는 것이 배포하는 환경에서는 에러 재현이 안되니 정말 답답했습니다. 사실상 단일 스크립트로 구현해야하는 서비스의 경우에는 cloud run function을 사용하는게 나을 것 같고, 조금 복잡한 서비스에는 직접 GCE를 이용해서 구현하는게 디버깅 측면에서는 더 나을 것 같습니다.
5.2. 개선할 점
- 구조설계의 효율화
뒤돌아보니 개선할 몇가지가 보이긴 합니다. 일단 가장 큰 것은 함수의 파편화입니다. 일례로 gemini가 정성 평가하는 방식만 봐도 main -> local_grader -> generate_content -> prompt_builder 의 스택 구조가 필요 이상으로 만들어져 있다는 느낌이 들었습니다. 이러면 디버깅 관점에서도, 확장 측면에서도 파악이 어려워서, 다음에는 하나의 스크립트로 통합하고 prompt_builder 와 같은 지속적으로 변경하고 수정이 필요한 부분만 따로 분리하는 것이 좋을 것 같습니다. 또한, 함수형 스크립트보다는 문제의 기능을 통합하여 class 설계를 하는 것도 좋겠습니다.
- 비용 효율 관점
처음에는 비용을 크게 생각하지 않았는데, 매일 몇천원씩 과금되는 Cloud - SQL이 점차 비용이 많이 증대되는게 느껴졌습니다. 좀 더 저렴한 사설 cloud를 쓸 수 도 있겠지만, 관리포인트가 많아지고 난이도가 높아질 것 같아 최대한 GCP안에서 해결하려고 했습니다. 하지만 그 과정에서도 자원과 비용을 효율적으로 사용할 설계를 하는 것이 좋겠습니다. 항상 채점 서비스가 올라가야하는 것은 아니라 상시 올려져있는 cloud-sql와 같은 서비스도 on-off 자동화가 필요해보입니다.
- 채점 서비스 기능 추가
현재 채점 서비스는 따로 로깅이나 데이터 수집을 하고 있진 않습니다. Gemini를 단순히 사용하는 것에 넘어서 개선하는 결과를 만들고 싶은데 그러러면 로깅 시스템이 있어야해서 설계해볼 것 같습니다. 또한, 현재는 정량적인 결과를 채점하고 있는데, 추후 통계 그래프나 시각화 이미지 등을 채점하는 기능을 추가 개발할 여지가 있습니다. 그리고 현재는 Prompt Engineering이 직접 api를 call 하고 있는데 langchain을 도입하여 좀 더 구조적인 설계를 해볼 수 있습니다.
5.3. 결과
결과적으로 총 2번의 과제에 지원 서비스를 무사히 마칠 수 있었고, 하나의 서비스를 개발해보는 좋은 경험이 되었습니다. 정량적으로는 기존에 1명에 채점이 20분 정도 걸리는 시간에서 10분정도로 빠르게 채점할 수 있었고, 개별적으로 튜터들이 시스템을 구축하여 채점하는 사전 준비 시간도 없앨 수 있었습니다. 사실 본 서비스는 누가 시킨것도 아니고 스스로 편하게 일해보자고 시작했지만 그래도 잘 해보자 나에게 남을 것이라는 마음으로 시작했는데, 결과가 나름 만족스럽게 나와서 좋았습니다. 다음에도 이렇게 스스로 추진하고 결과를 만드는 프로덕트를 하나씩 만들고 남겨보도록 하겠습니다.
6. 레퍼런스
https://github.com/llm-bot-sparta/llmbot/tree/main
GitHub - llm-bot-sparta/llmbot: 스코 채점&질문봇
스코 채점&질문봇. Contribute to llm-bot-sparta/llmbot development by creating an account on GitHub.
github.com
'Data Science > Engineering' 카테고리의 다른 글
클라우드별 서비스 무료 크레딧 정리, AWS GCP Azure NCP (0) | 2025.03.24 |
---|---|
슬랙 봇으로 커뮤너티 활성화하기(with CRM 메시지, 마인크래프트) (1) | 2025.02.02 |
[글또] 데이터 파이프라인 자동화 구축기 with 농수산물데이터, GCP, crontab (0) | 2024.04.13 |
GCP 셋업를 위한 기본 지식: 하드웨어와 OS 개념 (0) | 2024.04.13 |