2. 온라인 코딩 테스트 시스템 Backend 만들기
이 프로젝트의 중심은 backend이다. 내가 이 프로젝트를 진행하기 위한 목적은 과거 아쉬웠던 경험을 기억하며 다시 한번 되돌아 보는 기회를 가지는것과 함께 인프라적으로 학습했던 Docker와 backend 중심의 개발을 학습해보기 위해서이다. 프로젝트의 중심인 Backend에 대하여 정리 해보겠다.
기능과 기본 구조
onCoTe-backend의 핵심 기능은 제출과 실행의 관리이다. frontend에서 코드를 작성하여 제출을 하면 backend에서는 제출된 코드를 파일 데이터로 가공 후 저장을 위한 storage 서버로 전달을 하고 저장된 파일을 기반으로 언어 컨테이너를 실행 시켜 코드의 결과값을 도출한다.
모듈 통합과
/** exam.controller.ts / submit */
const saveResult = await this.examService.submit(body.lang, body.code)
const {id, file, ext, msg} = saveResult
const executeResult = await this.containerService.runContainer(`container_${ext}:latest`, fileName)/** container.provider.ts */
import * as Docker from 'dockerode';
export const ContainerProvider = {
provide: Docker,
// Docker 클래스의 새 인스턴스를 생성
// socketPath는 도커 데몬과 통신하기 위한 유닉스 소켓의 경로를 지정
useFactory: () => new Docker({ socketPath: '/var/run/docker.sock' })
}코드 제출 (exam.module)
- 클라이언트로부터 받은 소스코드를 다른 서버(storage server)에 업로드
/** exam.service.ts */
async submit(lang, code){
// 언어별 확장자 설정
const fileExtension = '언어별 확장자 설정'
// storage 저장 파일 이름
const fileName = '제출자 ID_제출 시간'
// 파일 데이터 생성 (FormData 생성)
const formData = new FormData()
formData.append('file', new Blob([code], {type: 'text/plain'}), fileName)
// HTTP PORT 요청 전송
const res = await firstValueFrom(this.httpService.post('uploadURL', formData, {
headers: {
'content-Type': 'multipart/form-data'
}
}))
// 성공 응답 처리
const {id, fileName, ext} = res.data
return {
id: id,
fileName: fileName,
ext: ext,
msg: 'upload success'
}
}컨테이너 관리 (container.module)
- 주어진 도커 이미지를 사용하여 컨테이너를 생성 및 실행하고, 실행 결과를 반환
/** container.service.ts */
async runContainer(image, fileName) {
let output = ''
// 컨테이너의 표준 출력과 표준 에러를 수신하여 output 변수에 저장
const outputStream = new Writable({
write(chunk, encoding, callback) {
output += chunk.toString()
callback()
}
})
// 컨테이너 생성 옵션 설정
const options = { // docker run --rm -v vol-onCoTe-storage:/exam container_lang:latest [fileName]
// 생성할 컨테이너 도커 이미지
Image: image,
// 컨테이너가 시작 될때 실행할 명령어 / 실행할 파일명 전달
Cmd: [fileName],
HostConfig: {
// 컨테이너의 실행이 완료되면 자동으로 컨테이너 삭제
AutoRemove: true,
// 저장된 storage 서버의 파일에 접근하기 위한 볼륨 마운트
Binds: [
"vol-onCoTe-storage:/exam"
]
}
}
// 정의된 옵션으로 새로운 컨테이너 생성
const container = await this.docker.createContainer(options)
// 컨테이너의 입출력 스트림 연결
const stream = await container.attach({
stream: true,
stdout: true,
stderr: true
})
// 도커 스트림에서 오는 데이터를 outputStream 전송
container.modem.demuxStream(stream, outputStream, outputStream)
// 컨테이너 시작과 컨테이너 내부의 프로세스가 종료될 때까지 대기
await container.start()
await container.wait()
// 결과 반환
return {
id: container.id,
msg: output
}
}