TECHNICAL REPORT · CASE STUDY

교육용 온라인 저지와 안전한 AI 피드백 설계

자체 호스팅 Python 채점 시스템 · 막힌 부분 → 시도 → 결정 중심 기록 · 최종 갱신 2026-10-04

이 글의 사실성 기준. 아래 수치·동작은 이 시스템을 실제로 빌드하고 명령으로 실행해 확인한 값만 적었습니다. 추정이나 검증하지 못한 내용은 넣지 않았습니다. 환경은 NVIDIA DGX Spark(aarch64, Docker 사용 가능), 로컬 LLM은 같은 호스트에서 vLLM으로 서빙되는 Qwen 계열 모델입니다.

1. 문제 정의

고교 정보/인공지능 수업에서 파이썬을 처음 배우는 학생이 스스로 문제를 풀고 즉시 채점받을 수 있는 환경이 필요했습니다. 요구사항은 세 가지였습니다.

2. 아키텍처 결정 — 왜 별도 서비스인가

결정

기존에 운영 중인 교육용 LLM 플랫폼과 프로세스·포트·도메인을 완전히 분리했습니다. 저지는 독립 포트에서 FastAPI로 돌고, 앞단은 Cloudflare Tunnel이 담당합니다.

근거

저지는 설계상 신뢰할 수 없는 코드를 실행합니다. 같은 프로세스나 같은 서비스에 얹으면, 채점 중 사고가 수백 명이 쓰는 LLM 서비스까지 끌고 내려갈 위험이 있습니다. 분리하면 장애 범위가 저지 안에 갇힙니다.

3. 막힌 부분 — 신뢰할 수 없는 코드를 어떻게 실행할까

막힌 지점

가장 큰 난관은 "학생이 무엇을 제출할지 모른다"였습니다. 무한 루프, 메모리 폭탄, 포크 폭탄, 파일 시스템 쓰기, 외부 네트워크 접속, 다른 프로세스 엿보기 — 전부 가능성 있는 입력입니다. 단순히 subprocess로 파이썬을 실행하면 호스트가 그대로 노출됩니다.

시도한 방법

컨테이너 격리를 택하고, 보호 플래그를 하나씩 실제로 깨뜨려 보며 검증했습니다. "네트워크를 막았다고 주장"하는 것과 "소켓 연결이 실제로 실패하는 것을 확인"하는 것은 다릅니다. 아래는 적용하고 동작을 확인한 격리 계층입니다.

호스트 (DGX Spark) Docker 컨테이너 (python:3.12-slim, --rm) network none · read-only FS · tmpfs /box 16m memory 256m · cpus 0.5 · pids-limit 64 user 65534(nobody) · cap-drop ALL · no-new-privileges timeout -s KILL (컨테이너 내부 벽시계 종료) 2차 방어선: 호스트측 subprocess timeout 컨테이너 런타임이 오작동해도 호스트가 강제 종료
그림 1. 다층 격리. 각 플래그는 적용 후 동작을 개별 확인했다.
위협방어확인 방법
외부 네트워크 접속--network none소켓 연결 실패 확인
파일 시스템 변조--read-only + tmpfs /box/etc 쓰기 거부, /box만 쓰기 가능
메모리 폭탄--memory 256m (swap 동일)초과 시 OOM 종료
포크 폭탄--pids-limit 64프로세스 수 상한
권한 상승--user 65534 + cap-drop ALLUID 65534(nobody)로 실행 확인
무한 루프컨테이너 내 timeout -s KILL + 호스트 timeout강제 종료 확인

결정

제출 코드와 입력은 호스트 임시 디렉토리에 쓰고 :ro(읽기 전용)로 마운트, 표준출력만 수거해 기대값과 비교합니다. 컨테이너는 종료 시 삭제하고, 동시에 돌아가는 채점 수는 세마포어로 제한해 GPU·LLM 작업과 CPU를 다툼 없이 공유하도록 했습니다.

4. 막힌 부분 — AI 피드백이 정답을 알려주면 안 된다

막힌 지점

"AI가 틀린 코드를 봐주면 좋겠다"는 요구는 자연스럽지만, 그대로 두면 LLM이 정답 코드를 통째로 뱉어 학생이 베끼게 됩니다. 교육적으로는 최악입니다.

시도한 방법

실제 결과 (검증)

"두 정수를 더해야 하는데 input().split()으로 받은 문자열을 그대로 +로 이어 붙인" 전형적 버그 코드를 넣었더니, 모델은 정답 코드를 주지 않고 이렇게 답했습니다(실제 출력):

input().split()으로 받은 값은 숫자가 아니라 문자열(String)이에요. 문자열끼리 + 연산자를 쓰면 숫자 합이 아니라 앞뒤로 붙는 연결이 일어나죠. a와 b가 숫자로 계산되려면 어떤 변환을 해줘야 할까요? int() 함수를 활용해 수정해 보세요.

원인(문자열 연결)과 개념(형변환)을 짚고 질문으로 끝맺되, 완성된 정답은 주지 않았습니다. 의도한 교육적 동작과 일치합니다.

5. 데이터 — 처음 배우는 학생을 위한 그림 있는 문제

결정

설명을 글로만 쓰면 입문자가 막힙니다. 수직선, 리스트 인덱스 상자, 막대그래프, 조건 분기 순서도처럼 그림으로 설명하는 게 더 나은 주제에는 문제 설명 안에 인라인 SVG를 직접 그려 넣었습니다. 외부 이미지 파일이 아니라 텍스트로 된 벡터 그림이라, 서버에 바이너리를 두지 않고 버전 관리와 재현이 쉽습니다.

리스트 (인덱스는 0부터) 10 [0] 20 [1] 30 [2] 40 [3]
그림 2. 문제 설명에 삽입한 리스트 인덱스 도식(실제 문항에서 발췌).

검증

새로 만든 튜토리얼 문항의 참조 해답을 전부 자동 실행해 기대 출력과 비교했습니다. 아래는 측정된 수치입니다.

참조 해답을 샌드박스로 돌린 표본 채점에서도 전부 정답 판정(AC)이 나왔고, 로그인→문제 조회→오답 제출(AI 힌트 생성)→정답 제출의 전체 흐름을 통합 테스트로 확인했습니다.

6. 지속성 — 제출해도 코드가 사라지지 않게

막힌 지점 → 결정

초기 구현은 문제를 열 때마다 에디터를 템플릿으로 초기화해, 제출 후 코드가 날아갔습니다. 문제별 키로 브라우저 로컬 저장소에 코드를 보관하고, 편집할 때마다 저장·복원하도록 바꿨습니다. 이제 제출·새로고침과 무관하게 학생이 자기 코드를 계속 고칠 수 있습니다. 더불어 처음 배우는 학생을 위해 에디터에 자동완성(파이썬 키워드·내장 함수·코드 내 식별자), 괄호 자동 닫기, 괄호 짝 강조, 현재 줄 강조, 4칸 들여쓰기, 가벼운 문법 린트(괄호/따옴표 짝, 콜론 뒤 들여쓰기 누락), 단축키(자동완성·빠른 제출·주석 토글)를 넣었습니다.

7. 성능 메모 (실측)

로컬 LLM 힌트 생성 지연을 3회 측정했습니다: 약 4.3s · 5.2s · 6.7s. 채점은 샌드박스에서 동기 처리하되 이벤트 루프를 막지 않도록 스레드로 오프로드했고, AI 힌트는 오답일 때만 생성해 정답 제출 경로에는 추가 지연이 없습니다. 저지 핵심 백엔드는 약 1,000줄 규모의 파이썬 (게이트웨이·채점 엔진·AI 피드백·DB·인증)으로 유지했습니다.

8. 배운 점

부록 · 실행 환경

이 보고서를 작성·검증한 호스트(실측): HP ZGX Nano G1n AI Station, NVIDIA GB10(Grace-Blackwell) GPU, 약 121 GiB 통합 메모리, Ubuntu 24.04(aarch64). 이 한 대에서 교육용 LLM 채팅·온라인 저지·이 소개 사이트가 각각 분리된 포트로 동시에 돌아가며, 앞단은 Cloudflare Tunnel이 담당합니다.

참고: 운영 전체 구성은 이 Grace-Blackwell 스테이션을 포함한 여러 장비로 이루어져 있으나, 위 수치는 보고서 작성 시점에 이 호스트에서 직접 측정한 값만 적었습니다.

← 홈으로