소개
안녕하세요! ThinkTank 프로젝트 백엔드 팀장 박창민입니다.
채점 서버를 구현하면서 발생한 트러블슈팅을 포스트에 작성하겠습니다.
2024.04.28 - [분류 전체보기] - [ThinkTank] 채점서버 (1) - 도커로 사용자 코드 실행시키기
해당 게시글은 위와 이어집니다.
문제 상황
이전 글에서 사용자 코드를 도커 컨테이너 안에서 채점을 진행할 때 N 개의 테스트케이스 만큼 도커 컨테이너가 생성되고 삭제되는 문제상황이 발생했습니다.
성능뿐 아니라 1초에 50명 정도만 동시에 채점을 요청해도, InterruptedException이 발생하는 것을 부하 테스트를 통하여 확인했습니다.
또한, 무한 반복문에 대해서도 도커 컨테이너가 계속 실행 중이기 때문에 계속 채점이 진행되는 상황 또한 발생했습니다.
왜 테스트케이스 N개당 N개의 도커 컨테이너가 생성될까?
이유는 생각보다 간단했습니다.
해당 플로우를 보시면 자바의 경우 사용자의 코드를 받아서 컴파일시키는 부분과, 실행시키는 부분을 도커 컨테이너 안에서 모두 이루어집니다.
컴파일이 완료가 된다면 컴파일된 파일을 생성하도록 했는데 이 컴파일 된 파일을 실행하면서 테스트케이스에 입력 값을 대입합니다.
예를 들어서 설명해 보겠습니다.
문제: 입출력 예제 example이 공백을 기준으로 매개변수로 주어질 때, A + B를 리턴해 주세요.
<테스트케이스>
Example | result |
1 2 | 3 |
2 4 | 6 |
5 6 | 11 |
<사용자 코드>
class ThinkTank {
public static String solution(String str) {
String[] repo = str.split(" ");
int a = Integer.parseInt(repo[0]);
int b = Integer.parseInt(repo[1]);
return a + b;
}
}
사용자가 이렇게 제출하게 되면 ThinkTank 서버에서는 사용자 코드를 메인에서 실행시키고 Example을 입력 값으로 넣어서 나온 출력값이 Result와 같은지 비교해서 성공 or. 실패 여부를 보여줍니다.
결국 해당 예시에서는 Example이 총 3개가 있는데 저희 서버에서는 총 3번 실행시켜서 Example을 넣는 방식입니다.
도커 컨테이너의 특성상 한 번 실행이 되고 종료가 된다면 재시작을 한다거나 하지 않는 이상 멈추게 되기 때문에, N 개의 테스트케이스만큼 컨테이너가 생성되고 삭제되었습니다.
또한 저의 경우는 --rm 명령어를 작성하여 컨테이너를 삭제해주었습니다.
삭제를 시킨 이유
코드를 채점하는 서버를 만들면서, 가장 중요하게 생각했던 부분은 안전하게 채점하는 부분이었습니다.
이를 위해 독립된 실행 환경인 도커 컨테이너를 활용했지만, 모든 사용자의 코드를 하나의 컨테이너 내에서 채점할 경우 발생할 수 있는 보안 문제를 해결해야 한다고 판단했습니다.
격리 부족
한 사용자의 코드가 다른 사용자의 코드 또는 시스템에 영향을 미칠 위험이 있습니다.
데이터 노출 위험
한 사용자가 악의적인 코드로 다른 사용자의 데이터에 접근하거나 시스템의 민감한 정보를 노출할 가능성이 있습니다.
이러한 이유로, 문제별로 각각의 도커 컨테이너를 생성하여 코드를 채점하고, 채점 후 해당 컨테이너를 삭제하는 방식이 더 안전하다고 판단했습니다.
해결방법
하나의 컨테이너에서 모든 테스트케이스를 실행시키기
따라서 제가 생각한 해결 방법은 하나의 실행문 안에서 테스트케이스 수만큼 반복하는 구조를 갖추는 것이었습니다.
저희 서비스에서 도커 컨테이너가 실행된다는 것은 메인 함수가 시작된다는 뜻입니다. 여기서 입력값을 대입하고, 이를 통해 도출된 출력값이 정답과 일치하는지를 확인합니다.
정답을 검증하는 출력문이 나오면 메인 함수는 종료되기 때문에, 종료를 막기 위해 별도의 코드가 필요하다고 판단했습니다.
public static final String JAVA_TEMPLATE =
"import java.io.*;\n" +
"import java.util.*;\n" +
"import java.util.stream.*;\n" +
"import java.util.concurrent.*;\n" +
"public class Main {\n" +
" public static void main(String[] args) throws IOException {\n" +
" ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();\n" +
" executor.schedule(() -> System.exit(0), 15, TimeUnit.SECONDS);\n" +
" BufferedReader br = new BufferedReader(new InputStreamReader(System.in));" +
" for(int zxc = 0; zxc < %d; zxc++) {\n" +
" String qwer = br.readLine(); \n" +
" System.out.println(solution(qwer));\n" +
" }\n" +
" }\n" +
" %s\n" +
"}\n";
템플릿의 흐름입니다.
1. 마지막 %s에 사용자 코드를 삽입합니다.
2. 사용자 코드의 메소드는 solution으로 정의되어 있으므로, 매개변수에 데이터베이스에에 저장되어 있는 example을 넣어줍니다.
3. 코드 실행 결과로 생성된 출력문과 데이터베이스에 저장된 정답(result)을 비교하여 일치 여부를 확인합니다.
4. 이 반복문은 테스트케이스 수만큼, 즉 %d 횟수만큼 실행되므로 도커 컨테이너가 실행 중인 동안은 종료되지 않으며 채점 프로세스를 유지할 수 있습니다.
레퍼런스가 부족한 상황에서 채점 서버를 구현하면서, 채점에 대한 고려사항과 리소스 낭비 방지 방법 등 여러 가지 중요한 배움을 얻을 수 있었습니다.
설계를 지속적으로 발전시키며 해결 방안을 생각해낸 덕분에 문제 상황을 해결할 수 있었지만, 여전히 제가 미처 고려하지 못한 문제가 있을 수 있습니다. 앞으로 더 나은 설계를 고민하며 보완해 나가야 할 것 같습니다.
'구름톤 트레이닝 풀스택 회고' 카테고리의 다른 글
[D'art-gallery] 선착순 쿠폰(2) - 글로벌 캐싱 도입 (0) | 2024.06.18 |
---|---|
[D'art-gallery] 선착순 쿠폰(1) - 대용량 트래픽, 동시성 이슈 (0) | 2024.06.11 |
[ThinkTank] 채점서버 (1) - 도커로 사용자 코드 실행시키기 (0) | 2024.04.28 |
⛅️[구름톤 트레이닝 풀스택 6회차] - 16주 차 회고⛅️ - [WEB IDE 프로젝트] (0) | 2024.04.20 |
⛅️[구름톤 트레이닝 풀스택 6회차] - 13주 차 회고⛅️ (0) | 2024.03.30 |