← 프로젝트 목록

Backend Project · 01

CodeQuest

운영 중인 코딩 교육 플랫폼 고도화 · 백엔드 단독 담당 · Gradle 멀티모듈 · AI·SW중심대학사업단/안랩블록체인컴퍼니(ABC) 산학협력 · 호서대학교 컴퓨터공학부 수업 실사용

Java 21Spring Boot 3.4Spring MVCGradle 멀티모듈Spring Cloud GatewayKafkaKEDAJPA/HibernateRedisMySQLKubernetesArgo CDPrometheus / GrafanaLoki
Overview

AI·SW중심대학사업단·안랩블록체인컴퍼니(ABC) 산학협력 프로젝트로, 호서대학교 컴퓨터공학부의 자료구조·알고리즘 등 정규 수업에서 학생들이 실제로 사용 중인 코딩 교육 플랫폼이다. 코스 → 퀘스트 → 문제 계층으로 학습 과정을 구성하고, 학생이 제출한 코드를 언어별 워커가 자동 채점해 결과를 실시간으로 돌려준다. Gradle 멀티모듈 구조로 Gateway·Auth·Course·Submit 서비스를 분리하고, 공통 도메인·보안·인프라(JWT·Redis·Kafka)는 common 모듈로 공유한다.

Problem

인수 시점의 서비스는 여러 모듈이 하나의 JAR로 묶여 한 모듈만 고쳐도 전체를 다시 빌드·배포해야 했고, 인증·코스·채점이 한 코드베이스에 섞여 책임 경계가 흐려져 있었다. 채점은 동기 처리라 실행 시간만큼 요청이 블로킹됐고, 채점 결과 회신만 HTTP로 남아 서버 장애 시 결과가 유실될 수 있었다. 문제 공유도 강좌 단위여서 개별 문제를 재사용하기 어려웠다.

My Role & Technical Decisions

연구실 선배로부터 데모 단계 프로젝트를 인수해 운영 중인 서비스의 고도화를 백엔드 단독으로 담당했다(프론트엔드는 별도 담당자). Gradle 멀티모듈(common + Gateway·Auth·Course·Submit), Kafka 기반 채점 디스패치·결과 회신, SSE, JWT, KEDA 오토스케일링과 Kubernetes 배포를 설계·구현했다. 이후 GitHub Actions의 직접 배포를 Argo CD app-of-apps·ApplicationSet 기반 GitOps로 전환하고, Prometheus 메트릭과 Loki 로그를 Grafana 대시보드·Slack 알림으로 연결했다.

Architecture
CodeQuest 시스템 아키텍처요청 처리, GitOps 배포, 메트릭·로그 관측을 분리해 각 흐름을 한 방향으로 읽을 수 있게 구성했습니다.

01 · 학습·채점 요청

사용자 요청부터 비동기 채점 결과 전달까지

  1. 01Web Client문제 풀이 · SSE
  2. HTTPS
  3. 02API GatewayJWT · 라우팅
  4. REST
  5. 03Domain ServicesAuth · Course · Submit
  6. event
  7. 04Kafka채점 이벤트 큐
  8. consume
  9. 05Judge WorkersKEDA 자동 확장

02 · 데이터 계층

영속 데이터와 단기 상태의 역할을 분리

  1. 01Domain Services트랜잭션 처리
  2. JPA
  3. 02MySQL사용자 · 과제 · 결과
  4. cache
  5. 03Redis세션 · 캐시

03 · GitOps 배포

이미지 빌드와 배포 선언을 분리한 자동화 흐름

  1. 01Git Push애플리케이션 변경
  2. trigger
  3. 02GitHub Actions테스트 · 이미지 빌드
  4. push image
  5. 03Container Registry버전 이미지 보관
  6. manifest SHA
  7. 04Argo CDGit 상태 동기화
  8. sync
  9. 05Kubernetes서비스 · 워커 운영

04 · 메트릭·로그 관측

수집과 조회 방향을 구분해 운영자가 장애 원인을 추적

  1. 01Services · Workersmetrics · JSON logs
  2. scrape / ship
  3. 02Prometheus · LokiServiceMonitor · 로그 저장
  4. query
  5. 03GrafanaPromQL · LogQL
  6. notify
  7. 04Slack Alert운영 알림
  • 사용자·클라이언트
  • 애플리케이션
  • 데이터
  • 비동기 처리
  • 배포
  • 관측
  • 보안·검증
  • 외부 시스템
Key API Endpoints
MethodPathDescription
POST/api/auth/**로그인·토큰 발급·재발급 (Auth 모듈)
GET/api/course/**코스·퀘스트·문제 조회 (Course 모듈)
POST/api/submit/**코드 제출 → Kafka 비동기 채점 (Submit 모듈)
GET/api/grade/**채점 결과 조회
GET/ws/**제출 결과 실시간 스트림 (SSE)
Troubleshooting & Evidence

하나의 모듈만 변경해도 전체를 다시 빌드·배포해야 함↗

원인: 단일 빌드·배포 단위에서는 작은 변경에도 전체가 재빌드·재배포되어 배포 시간이 늘고 무관한 서비스까지 재기동됨

해결: 모듈별 Dockerfile + CI/CD(GitHub Actions) path 필터로 변경된 모듈만 선택적으로 빌드·이미지화·배포 (공통 common이 바뀐 경우에만 의존 모듈로 전파)

배운 점: 모듈 경계를 CI/CD와 연계하면 변경 영향 범위를 빌드·배포 단위까지 좁힐 수 있다

채점은 끝났는데 점수가 반영되지 않고 결과가 사라짐↗

원인: 제출 디스패치는 Kafka인데 결과 회신만 HTTP POST(3회 retry + API 키)로 남아 통신 방식이 혼재했다. 서버가 내려가 있으면 retry 3회가 모두 실패한 뒤 결과가 영구 유실되고, 워커가 서버 주소와 API 키를 알아야 해 결합·장애 전파도 함께 생겼다

해결: 결과 회신을 grading-result 토픽 produce/consume로 전환. 워커 5종의 retry 로직·API 키·콜백 엔드포인트와 서버의 ApiKeyFilter를 제거해 워커가 서버의 존재를 몰라도 동작하게 했다. 브로커가 메시지를 보관하므로 서버가 늦게 떠도 재처리된다

배운 점: 한 파이프라인에 통신 방식이 섞이면 약한 쪽이 전체의 신뢰도를 결정한다 — 디스패치만 비동기화하고 회신을 동기로 두면 '채점은 됐는데 점수가 없는' 상태가 남는다 (결과 유실 0건 보장, 코드 +58/−222줄)

강좌 단위 공유라 잘 만든 문제 하나를 다른 교수와 나눌 수 없음↗

원인: 문제 은행이 강좌에 종속돼 공유의 최소 단위가 강좌였다 — 문제 하나를 주려면 강좌를 통째로 열어야 했다

해결: 공유 단위를 문제로 낮춰 교수 간 개별 공유·해제 API를 두고(공유 대상·가능 사용자 조회 포함), 전체 공개 목록을 분리해 기본 제공 문제를 쌓을 수 있게 했다

배운 점: 재사용성은 기능을 더해서가 아니라 공유의 최소 단위를 낮춰서 생긴다

제출이 특정 시점에 몰리면 채점 대기열이 쌓여 응답이 지연↗

원인: CPU·메모리 기반 오토스케일링은 이미 발생한 자원 소모만 반영해, 아직 처리되지 않은 채점 수요(대기열)를 제때 감지하지 못함

해결: KEDA로 채점 워커 오토스케일링의 제어지표를 CPU·메모리에서 Kafka Consumer Lag(미처리 메시지 수)로 바꿨다. 언어별 ScaledObject를 consumerGroup·topic 단위로 분리하고(min1/max10, 15초 폴링, cooldown 60s), 파드당 허용 대기 메시지 수(lagThreshold)를 언어별로 차등했다 — JVM 콜드스타트를 제거해 케이스당 60ms 수준이 된 C·Java는 3, 처리가 더 걸리는 Python·C++·C# 는 5. 빠른 언어는 파드 한 대가 대기열을 빨리 소진하므로 더 일찍 확장시키고, 느린 언어는 소진이 더뎌 과확장을 피하도록 완화한 판단이다

배운 점: 오토스케일링은 자원 사용률보다 '미처리 작업량(Consumer Lag)'이 부하를 더 정확히 반영한다. 같은 큐라도 파드 한 대가 대기열을 소진하는 속도가 워크로드(언어)마다 달라, 임계값을 하나로 덮으면 과·소 확장이 함께 생긴다

WebFlux+JPA 조합에서 제출 경로 응답이 느림↗

원인: 논블로킹 WebFlux 위에서 블로킹 JPA를 호출해 이벤트 루프 스레드가 묶이는 블로킹 병목이 발생

해결: 제출 경로를 Spring MVC로 전환해 블로킹 작업을 전용 스레드풀에서 처리하도록 정리

배운 점: WebFlux는 호출 경로 전체가 논블로킹일 때만 이득 — 블로킹 JPA와 섞으면 오히려 병목이다 (JMeter 동시 500 기준 제출 p95 2,320ms→271ms, 자체 목표 응답시간 300ms 위반율 77.5%→4.5%. 단 이 수치는 MVC 전환과 HikariCP 풀·CPU 사이징이 함께 적용된 결과다)

인증 트래픽이 몰릴 때 auth 파드가 병목이 되어 응답이 지연↗

원인: 노드 자원은 남는데도 auth 파드가 limits.cpu=2·replicas=1에 고정 — bcrypt가 CPU 바운드라 동시 요청이 늘면 Tomcat 스레드가 큐잉됨. 자원 부족이 아니라 파드에 걸린 상한이 병목이었고, 수평 확장의 진짜 천장은 MySQL max_connections였음

해결: 스레드풀 증설 대신 코어를 늘리는 수직 확장(auth CPU 2→4)에 KEDA 수평 확장(min2/max4)·gateway 이중화를 더하고, HikariCP 풀을 줄여 (replica 합산)×풀 < max_connections로 커넥션 예산을 맞춤. KEDA CPU Utilization이 limits가 아닌 requests 기준이라 조기 스케일되던 함정은 AverageValue(절대 CPU)로 정정

배운 점: '느리다'를 자원 부족으로 단정하면 안 된다 — 노드는 남는데 파드 limits/replicas 상한이 진짜 병목이었다. CPU 바운드 병목엔 스레드풀이 아니라 코어가 답이고, 수평 확장은 앱보다 공유 자원(DB 커넥션)에서 먼저 막힌다

GitHub Actions가 빌드와 배포를 모두 담당해 클러스터 상태와 Git 선언이 어긋날 수 있음

원인: CI가 kubectl로 직접 배포하면 파이프라인 실행 결과가 실제 상태의 기준이 되고, 수동 변경·실패한 재실행·구버전 워크플로가 배포 상태를 되돌릴 위험이 있었다

해결: GitHub Actions는 변경 모듈의 이미지를 빌드하고 매니페스트의 이미지 SHA만 갱신하도록 축소했다. 배포는 Argo CD root Application(app-of-apps)과 서비스별 ApplicationSet이 담당하며 automated prune·selfHeal로 Git 선언 상태를 지속적으로 복구한다. Secret은 Sealed Secrets로 Git 관리한다

배운 점: CI는 산출물을 만들고 GitOps 컨트롤러는 원하는 상태를 조정하도록 책임을 나누면 배포 이력과 실제 클러스터 상태의 기준을 Git으로 단일화할 수 있다

서버와 채점 워커의 메트릭·로그가 흩어져 장애 원인과 처리 흐름을 함께 보기 어려움

원인: 서비스별 로그를 개별 파드에서 확인해야 했고, Argo CD 동기화 상태·서비스 메트릭·채점 결과를 한 화면에서 연결해 볼 관측 계층이 없었다

해결: kube-prometheus-stack의 Prometheus·ServiceMonitor로 서비스와 Argo CD 메트릭을 수집하고, Loki에 구조화 로그를 보관했다. Grafana에 서비스·레벨·trace_id·submission_id 필터와 언어별 채점 처리량·테스트케이스 결과 패널을 구성하고 로그 알림을 Slack에 연결했다

배운 점: 관측은 도구 설치보다 동일한 요청을 메트릭·로그·배포 상태 사이에서 추적할 수 있는 공통 식별자와 대시보드 구조가 핵심이다

성적 조회에서 N+1 쿼리가 폭증↗

원인: 학생×퀘스트×문제 3중 루프에서 제출을 건별 조회 — 학생 30·퀘스트 5·문제 10 기준 요청당 최대 1,500회 SELECT (Hibernate SQL 로그로 확인)

해결: IN + JOIN FETCH 기반 단일 쿼리로 개선

배운 점: 쿼리 수는 코드 줄 수가 아니라 루프 깊이에 비례한다 — 3중 루프 안의 건별 조회 한 줄이 요청당 1,500 SELECT가 됐다. 목록 조회는 중첩이 생기는 순간 코드를 보기 전에 쿼리 수를 먼저 세야 한다 (1,500→1, Submit 목록 21→1)

Metrics
2,320ms→271ms
제출 p95 · JMeter 동시 500 (MVC 전환+풀·CPU 사이징)
54%→0%
채점 케이스 중 1s 이상 비중 (케이스별 JVM→단일 JVM+CDS, 평균 0.84s→0.06s)
25,242ms→159ms
로그인 p50 · JMeter 동시 500 (파드 상한 해소)
1,500→1
성적 조회 SELECT (N+1 제거)
Infra & Deploy

Gradle 멀티모듈(common·gateway·auth·course·submit)을 모듈별 Dockerfile로 이미지화해 Kubernetes에 배포한다. GitHub Actions는 변경 모듈의 이미지를 빌드하고 배포 SHA를 Git에 반영하며, Argo CD root Application과 ApplicationSet이 서비스별 동기화·롤백·prune·selfHeal을 담당한다. 언어별 채점 워커는 KEDA가 Kafka Consumer Lag를 기준으로 오토스케일링한다. 관측 환경은 kube-prometheus-stack(Prometheus·Grafana), Loki와 서비스·Argo CD ServiceMonitor로 구성했고, Grafana 대시보드와 Slack 로그 알림을 운영한다.

Next Step

프로젝트에 대해 더 이야기해 보세요.

연락하기