NYPC를 참여할때 항상 설레고 행복했던 나는 우리 학교에서도 그러한 행사를 진행하고 싶었다. 지속적으로 이야기를 꺼낸 결과 우리 정보 동아리에서는 크리스마스 코딩 파티를 개최하게 되었고, 나는 온라인 저지와 일부 문제 출제를 담당하게 되었다. 나는 NYPC의 우리 학교 버전을 만들고 싶었다. 이를 위해선 기존의 Online Judge 를 그대로 활용하기에는 문제가 있었다. 평소 입부시험을 치르거나 학교 소규모 대회를 운영할 때에는 QuingdaoU 의 OnlineJudge 2.0이나 HUSTOJ를 주로 사용하였다. 둘 다 비슷한 UI에 비슷한 기능을 제공한다. 단순한 문제 등록, 채점, 대회 개최용으로는 손색이 없는 오픈소스 Online Judge 이지만 NYPC 분위기랑은 잘 어울리지 않는 딱딱한 구조를 갖추고 있었다. 특히 서브테스크와 부분점수가 없다는 것이 큰 문제가 되었다. 이를 해결하기 위해 처음부터 기능을 뜯어 고쳐야 했지만 Online Judge 2.0은 Next.js 로 제작되었고 나는 이를 써 본 적이 없다. 대회 개최까지 한달도 남지 않은 시점에서 이를 배우고 고치기에는 시간이 촉박했다. 그래서 차라리 내가 평소에 주로 다룬 React로 아예 처음부터 Online Judge 를 만들어 보기로 하였다.
많은 인원이 참가하는 대회인만큼, 공정성을 유지하기 위해 무엇보다 채점서버의 안정성이 중요했다. 하지만 학교에 있는 서버는 조그마한 노트북 하나 뿐이었다. 그렇다고 서버를 호스팅 하기에는 비용과 구축이 어렵다는 문제가 있었다. 어떻게 하면 채점 서버의 안정성을 높일 수 있을까 고민하다 나온것이 “채점 큐” 서버와 “채점” 서버의 분리였다. “채점 큐” 서버는 채점 해야하는 문제와 소스코드들을 저장하고 있다. “채점” 서버는 이 큐 에서 하나씩 빼와 채점하고, 그 결과를 “채점 결과” 서버로 넘겨준다. 따라서 채점서버가 전혀 작동을 하지 않거나 심지어 포맷되더라도 채점해야하는 소스코드들은 채점 큐 서버에서 사라지지만 않는다면 언제든 다시 채점 할 수 있다. 그래서 나는 채점 큐 서버에는 Google의 Firestore 를 사용하고, 채점 서버는 Python 으로 직접 제작하고 구축하였다. 아무리 노트북 서버가 고장나거나 제 기능을 못하더라도 Google의 서버가 멈추지 않는 이상 제출 기록은 사라지지 않는다. 이렇게 서버를 구축했을때의 또 다른 장점은 프론트앤드가 채점서버와 직접적으로 통신을 할 필요가 없다는 것이다. 이는 DDOS 공격이나 대역폭 관리의 어려움 처럼 소규모 서버에서 발생 할 수 있는 문제의 대부분을 없애준다. 또한 이미 제대로 잘 구축되어있는 Google Firebase의 라이브러리를 활용 할 수 있어 개발 소요 시간도 단축된다.

그림1. 채점 서버의 쿼리 구상도
채점 서버는 온라인 저지의 심장과도 같은 부분이다. 아무리 사이트를 잘 만들어도 채점이 안되면 Judge 가 아니다. 그래서 제일 중요한 채점 서버를 먼저 개발하기로 하였다. 채점 서버는 Python 으로 개발하기로 하였다. Online Judge의 채점 과정은 우리가 컴퓨터에서 컴파일을 하고 작동시키는 것과 동일하지만 이를 자동화 시키고 입력과 출력은 테스트케이스가 작성된 텍스트 파일로 한다는 차이점이 있다. 텍스트 파일을 사용한다는 것이 생소하게 들리고 어렵게 느껴질 수 있지만 사실 아래에 있는 2줄의 명령어가 이를 수행해준다.
g++ main.cpp -o main
./main < input.txt
첫번째 줄은 main.cpp를 컴파일 해 주고, 두번째 줄은 input.txt를 입력으로 주어 컴파일된 프로그램을 실행시킨다. 위처럼 코드 상에서 파일로 입출력을 받지 않아도 CLI를 파일로 대체 할 수 있다. 따라서 우리가 채점 서버에서 구현해야 하는 기능은 아래와 같다.
- 채점 큐에서 채점해야 하는 소스코드와 문제 정보를 받아온다
- 소스코드를 파일에 작성하고 컴파일 한다
- 컴파일된 파일을 문제에서 주어진 제한 시간과 메모리 내에서 실행한다
- 문제의 테스트케이스에서 주어진 입력과 출력이 모두 동일한지 확인한다
- 채점 결과를 채점 결과 서버에 업로드한다
그런데 3번에서 “주어진 제한 시간과 메모리 내에서 실행” 은 어떻게 할 수 있을까. 우선 시간제한은 subprocess 를 활용하였다. 명령어를 subprocess 내에서 timeout 를 설정하고 실행해 프로그램이 timeout 를 벗어나면 자동적으로 시간초과를 반환하도록 하였다. 그럼 메모리 제한은 어떻게 구현하였을까? 사실 Riemann OJ 의 제일 처음 버전에는 한가지의 비밀이 있다. 메모리 제한이 있는 척 했을뿐 사실은 제한을 걸어두지 않았다. 애초에 출제된 문제들에서 과도하게 메모리를 사용하는 풀이가 나오기 힘들었기 때문에 우선은 시간 초과만 잡도록 하였다. 다행히 실제 대회에서 메모리 초과로 인한 채점 서버의 마비는 발생하지 않았다. 하지만 메모리 제한이 없는 상황에서 제대로된 Online Judge 로 확장시켜 나가기는 불가능하다. 그래서 바로 다음 버전부터 이를 추가하기로 계획하였다. 추가되면 바로 어떻게 해결하였는지를 올리도록 하겠다.
코드를 올릴까 했지만 너무나도 읽기 어렵게 작성한 관계로 클린 코드로 바꾸어서 메모리 제한 문제를 해결한 후 나중에 같이 올리도록 하겠다. 다음 글은 React 로 Online Judge 의 웹사이트를 제작한 과정에 대해 올릴 예정이다. 그 후에는 최대한 빠른 시일 내에 코드를 다듬고 UI를 개편해 RiemannOJ의 URL과 함께 올리도록 하겠다.