들어가며
이번에 개발을 시작한 알고리즘 저지 플랫폼 Solve Me Up(이하 SMU)의 아키텍쳐 설계 과정을 기록해보고자 합니다. 더 나아가 SMU에서는 왜 생산자/소비자 패턴 및 메시지 기반 아키텍쳐를 설계했는지를 다룹니다.
채점 서버의 특수성
사용자의 코드를 직접 받아서 채점하는 백준, 리트코드와 같은 서비스는 일반적인 CRUD API와는 조금 다른점이 있습니다.
- 빈번한 채점요청
- 사용자의 코드를 직접 실행하여 안전성을 보장 해야함
- 리소스 소비가 큼
- 사용자의 코드 실행 시간 예측 불가
따라서 사용자의 코드를 직접 실행 시키는 부분(채점 서버)를 어떻게 설계할 것인지 중요했습니다.
코드 실행 환경 선택
래퍼런스들을 참고하여 팀원들과 논의한 결과, 세 가지 방식이 후보로 있었습니다.
1. API 서버에서 실행
가장 단순한 방식입니다. Spring Boot API 서버가 제출 요청을 받으면 같은 프로세스 내에서 코드를 컴파일하고 실행합니다.
User → API Server (커뮤니티 API, 문제 API, 컴파일, 실행, 채점) → Response
이 방법은 채점 작업이 자원을 점유하면서 다른 API 요청까지 느려질 수 있는 가능성이 있습니다. 또한 사용자의 코드가 호스트에 접근할 수 있어 악의적인 코드를 방어하지 못한다면 서버 전체가 죽을 수 있습니다.
2. VM에서의 격리
채점 요청이 오면 별도 VM을 띄워서 코드를 실행하는 방법입니다.
User → API Server → VM (compile + execute) → API Server → Response
OS 수준으로 격리가 되지만 채점 요청이 들어올 때마다 VM별로 전체 OS를 올려야 하므로 리소스 오버헤드가 큽니다. 격리 수준이 가장 높지만 채점 요청이 빈번하게 이루어지므로 적합하지 않습니다.
3. Docker 컨테이너 격리
사용자의 코드를 VM 대신 Docker 컨테이너로 격리시켜서 채점하는 방법입니다.
User → API Server → Docker Container (compile + execute) → API Server → Response
컨테이너의 CPU, 메모리, 네트워크를 제한할 수 있어 안전하고 사용한 컨테이너를 바로 내려주기만 하면 리소스가 정리되기 떄문에 가장 합리적인 방법이라고 생각하여 채택했습니다.

아직 남아있는 문제
Docker 컨테이너 안에서 코드를 실행하는 것이 가장 합리적인 격리 방식이라고 판단했지만, 이것만으로는 API 서버가 느려지는 문제를 해결할 수 없습니다.
채점은 가벼운 요청이 아닙니다. Docker 데몬을 호출해서 컨테이너를 띄우고, 채점이 끝날 때까지 기다린 뒤 결과를 받아 응답하는 구조이기 때문에 API 서버의 쓰레드 하나가 그 시간 동안 점유됩니다. 동시 제출이 몰리면 문제 조회나 로그인 같은 가벼운 요청까지 대기하게 되는 문제가 여전히 남아 있습니다.
채점 서버의 분리 — Core API + 채점 Worker
이 문제를 해결하려면 채점 작업 자체를 API 서버에서 분리해야 합니다. API 서버는 제출 요청을 받아 DB에 기록하고, 실제 채점은 별도의 Worker 서버가 담당하는 구조입니다.
Core API는 제출 요청을 받으면 Submission을 PENDING 상태로 저장하고 즉시 응답을 돌려줍니다. 채점이 몇 초가 걸리든 API 서버는 관여하지 않습니다. Worker가 채점을 완료하면 결과를 Core API에 돌려주고, Core API는 이를 DB에 반영합니다.
Core와 Worker의 연결 — 메시지 큐 도입
Core와 Worker를 단순 HTTP 요청으로 연결하면 다음과 같은 문제가 발생합니다.
- 쓰레드 블로킹 — 채점이 끝날 때까지 HTTP 커넥션을 유지해야 하므로 Core API의 쓰레드가 다시 점유됩니다.
- 장애 시 요청 유실 — Worker가 채점 중에 죽으면 해당 요청은 그대로 사라집니다.
이 문제를 해결하기 위해 메시지 큐를 도입하고, 생산자/소비자 패턴으로 Core API와 채점 Worker를 연결하기로 결정했습니다.
메시지 큐 기반 생산자/소비자 패턴
ore API와 Worker 사이에 RabbitMQ를 두고, 양방향으로 메시지를 주고받는 구조입니다.

이 과정에서 Core API는 채점 요청 큐에 메시지를 넣는 순간 채점 작업에서 분리되고 메시지를 발행하고 바로 다음 요청을 처리할 수 있어 쓰레드가 블로킹되지 않습니다.
효과
1. API 서버 성능과 채점 부하의 분리
Core API는 메시지를 큐에 넣기만 하면 되기 때문에 제출이 몰려도 문제 조회, 로그인, 커뮤니티 같은 다른 API의 응답 속도에 영향을 덜 주게 됩니다.
2. 장애 격리
Worker가 채점 도중에 죽어도 Core API는 동작합니다. 처리되지 못한 메시지는 큐에 남아있고, Worker가 복구되면 다시 소비합니다. HTTP 방법 이었다면 Worker가 죽는 순간 해당 요청은 유실되지만 메시지 큐에서는 Worker가 acknowledge를 보내기 전까지 메시지가 삭제되지 않기 때문에 유실을 방지할 수 있습니다.
3. 비동기 응답으로 사용자 경험 개선
비동기로 응답할 수 있어 사용자는 제출 즉시 “채점 중” 상태를 받고 이후 클라이언트가 결과를 폴링하거나 SSE 등으로 결과 알림을 받을 수 있게 되었습니다.