코딩 테스트 채점 서버 만들기

fasongsong·2024년 6월 27일
1

실습

목록 보기
3/4

📌 프로젝트 구조


- src
  - main
    - java
      - com
        - example
          - judge
            - JudgeApplication.java
            - controller
              - JudgeController.java
            - service
              - JudgeService.java
            - model
              - Submission.java
            - util
              - CodeExecutor.java
    - resources/static
      - input.txt
      - output.txt



📌 조건


  1. 입력 형태
{ user : "파송송", answer : "some awesome code" }
  • 사용자의 데이터는 Request Body에 담겨 있으며, JSON 형식이다.
  1. answer => Soultion.java 형태로 저장

  2. 정답 여부 판별과 채점 모듈 구현

  3. 시간을 초과한 경우에는 작업을 중단

  4. 상황에 맞는 상태 코드 리턴 (컴파일 에러 등..)

  5. 정답 데이터를 제출하고 원하는 리턴이 나오는지 검증



📌 구현 코드


[CodingtestGradeApplication]

package com.example.judge;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CodingtestGradeApplication {

	public static void main(String[] args) {
		SpringApplication.run(CodingtestGradeApplication.class, args);
	}

}

[JudgeController]

package com.example.judge.controller;

import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.example.judge.model.Submission;
import com.example.judge.service.JudgeService;

@RestController
public class JudgeController {
	
	@Autowired
	private JudgeService judgeService;
	
	
	@PostMapping("/submit")
	public ResponseEntity<?> submitCode(@RequestBody Submission submission) throws IOException{
		System.out.println(submission.getAnswer());
		String result = judgeService.judge(submission);
		return ResponseEntity.ok(result);
	}
	
}

[Submission]

package com.example.judge.model;

public class Submission {

	private String user;
	private String answer;

	// getters and setters
	public String getUser() {
		return user;
	}

	public void setUser(String user) {
		this.user = user;
	}

	public String getAnswer() {
		return answer;
	}

	public void setAnswer(String answer) {
		this.answer = answer;
	}

}

[JudgeService]

package com.example.judge.service;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

import org.springframework.stereotype.Service;

import com.example.judge.model.Submission;
import com.example.judge.util.CodeExecutor;

@Service
public class JudgeService {
	
	public String judge(Submission submission) throws IOException {
		String code = submission.getAnswer();
		File file = new File("Solution.java");
		String expectedOutputPath = "src/main/resources/static/output.txt";
		
		try(FileWriter writer = new FileWriter(file)){
			writer.write(code);
		}catch(IOException e ) {
			e.printStackTrace();
			return "파일 쓰기 오류";
		}
		
		String result = CodeExecutor.compileAndRun(file);
		
		// 예상 출력과 실제 출력을 비교
        String expectedOutput = new String(java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(expectedOutputPath))).trim();
        if (result.equals(expectedOutput)) {
            return "정답입니다.";
        } else {
            return "틀렸습니다. 예상 출력: " + expectedOutput + " 실제 출력: " + result;
        }
		
	}
	
}

[CodeExecutor 채점 클래스]

package com.example.judge.util;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.concurrent.*;

public class CodeExecutor {

	public static String compileAndRun(File file) {
		try {
			// 외부 프로세스 실행
			// - Runtime.getRuntime().exec(command) 메서드는 주어진 명령어 command를 실행시키기 위한 외부 프로세스를
			// 생성.
			// command는 실행하려는 외부 프로그램의 명령어와 옵션을 포함한 문자열. 예를 들어, "javac Solution.java"와 같이 자바
			// 컴파일러를 실행하는 명령어.
			// - 여기서는 javac 명령어를 실행시키기 위해 사용.
			// - file.getName()은 file 객체에서 파일의 이름을 가져오는 메서드.
			// - 따라서 위 코드에서는 Solution.java 파일을 컴파일하는 명령어가 실행.
			Process compileProcess = Runtime.getRuntime().exec("javac " + file.getName());
			String inputPath = "src/main/resources/static/input.txt";
			// 외부 프로세스가 종료될 때까지 현재 스레드를 차단하고 기다린다
			compileProcess.waitFor();

			// compileProcess.exitValue() 메서드는 외부 프로세스의 종료 코드(exit code)를 반환
			// 일반적으로 0은 성공을 나타내고, 그 외의 값은 실패를 나타냄
			if (compileProcess.exitValue() != 0) {
				return "컴파일 오류";
			}

			// 비동기 실행
			Callable<String> task = () -> {
				try {
					Process runProcess = Runtime.getRuntime().exec("java " + file.getName().replace(".java", ""));
					// input.txt의 내용을 실행 프로세스에 전달
					BufferedWriter stdInput = new BufferedWriter(new OutputStreamWriter(runProcess.getOutputStream()));
					BufferedReader inputFile = new BufferedReader(new FileReader(inputPath));
					String inputLine;
					while ((inputLine = inputFile.readLine()) != null) {
						stdInput.write(inputLine);
						stdInput.newLine();
					}
					stdInput.close();

					BufferedReader stdOutput = new BufferedReader(new InputStreamReader(runProcess.getInputStream()));
					BufferedReader stdError = new BufferedReader(new InputStreamReader(runProcess.getErrorStream()));
					String s;
					StringBuilder output = new StringBuilder();
					while ((s = stdOutput.readLine()) != null) {
						output.append(s).append("\n");
					}
					while ((s = stdError.readLine()) != null) {
						output.append(s).append("\n");
					}

					runProcess.waitFor();
					if (runProcess.exitValue() != 0) {
						return "런타임 오류: " + output.toString();
					}

					return output.toString().trim();

				} catch (Exception e) {
					e.printStackTrace();
					return "예외 발생: " + e.getMessage();
				}
			};

			// Executors 클래스를 사용하여 ExecutorService 인스턴스 생성
			ExecutorService executor = Executors.newSingleThreadExecutor();
			Future<String> future = executor.submit(task);
			String result;
			try {
				result = future.get(5, TimeUnit.SECONDS); // 5초로 제한 시간 설정
			} catch (TimeoutException e) {
				future.cancel(true);
				result = "시간 초과";
			} finally {
				executor.shutdown();
			}

			return result;
		} catch (Exception e) {
			e.printStackTrace();
			return "예외 발생: " + e.getMessage();
		}
	}
}


📌 Postman test


테스트를 위해 단순한 answer를 넣었다

[정답인 경우]

[오답인 경우]



📌 추가할 사항


  • 격리된 컨테이너로 도커 활용
  • 보안적인 측면에서 실행되면 안되는 코드 생각
  • 무한 루프를 도는 경우 투입한 자원을 어떻게 회수할 것인지 고민
  • 동시에 많은 유저들이 채점을 신청할 경우 처리할 수 있는 구조를 고민
  • API 서버에서 Thread를 직접 실행하는 방법보다 별도의 채점 모듈을 만들어서 API 서버에서는 그 모듈을 호출하고, 모듈에서 Thread를 만들어 실행한 뒤 제한 시간을 초과하면 모듈에서 Thread를 중단하는 방식으로 구현


📌 참고 자료


profile
파송송의 개발 기록

0개의 댓글