AWS 기술 블로그

구름(goorm)의 Amazon Chime SDK를 활용한 대규모 코딩 테스트의 실시간 감독 서비스 운영 사례

<그림1. 구름DEVTH 서비스화면>

구름은 2013년 설립된 AI·SW 에듀테크 스타트업으로, ‘모두가 개발자가 된다’는 비전을 실현해나가고 있습니다. 누구나 양질의 SW 교육을 받고 성장할 수 있도록, 개발 환경 구성의 어려움을 해결하고 클라우드 자원을 활용해 자유롭게 소프트웨어 역량을 펼칠 수 있도록 관련 서비스를 제공하고 있습니다.

구름의 주요 서비스 중 하나인 구름DEVTH는 2016년에 출시되었으며, 실제 현업의 개발 환경과 유사한 환경에서 인재를 분석하고 역량을 평가할 수 있는 ‘비대면 평가 환경’을 제공합니다. 이 서비스는 LG, 현대자동차, 데이터진흥원 등 많은 기업과 기관에서 활용되고 있습니다.

비대면 역량 평가의 도전 과제

2016년 구름DEVTH 출시 직후 구름이 직면한 주요 과제는 다음과 같습니다.

  1. 시장의 거부감 극복: 비대면 평가라는 새로운 방식에 대한 시장의 두려움과 거부감 극복
  2. 공정성 확보: 현장 감독관 없이 어디서나 시험을 치를 수 있는 환경에서 부정 행위 차단과 평가의 공정성 담보
  3. 실시간 감독 요구: 코로나19로 늘어난 실시간 감독 요구 대응
  4. 대규모 동시 접속 처리: 취업 시즌 등 특정 기간에 대규모 인원이 동시에 접속하는 상황 대비
  5. 비용 효율성: 사용량에 따라 유연하게 대응할 수 있는 비용 구조 확립

기존 솔루션의 한계

구름DEVTH는 초기에 비대면 평가의 공정성을 확보하기 위해 사후 캡처 분석 방식을 사용했습니다. 이 방식은 평가 응시자의 화면을 주기적으로 캡쳐하고 평가 종료 후 이를 분석하여 부정 행위를 감독했습니다.

일정 시간 자리를 비우거나 특정 방향을 계속 응시하는 등의 의심스러운 행동 패턴을 찾아내 사후 제재하는 식이었습니다. 그러나 이러한 접근 방식은 감독 과정에서 많은 캡처 이미지를 일일이 확인해야 했기 때문에 많은 시간과 인력이 소요되었습니다. 캡처 이미지만으로는 정확한 부정 행위 여부를 판단하기 어려운 경우도 있었습니다. 특히 사후 제재 방식은 이미 평가가 끝난 후에 불이익을 주는 것이어서 논란의 여지가 있었습니다.

이를 해결하기 위해서는 실시간 감독 기능 구현이 필요했습니다. 특히 코로나19로 인해 비대면 평가가 일상이 되면서 실시간 감독 기능의 필요성은 더욱 커졌습니다.

Amazon Chime SDK 도입 배경

구름은 실시간 감독 기능에 대한 다양한 고객 요구를 수용할 수 있는 기술을 검토하기 시작했습니다. 당시 요구사항은 다음과 같습니다.

  1. 평가 하나당 1,000명 이상의 응시자 동시 감독
  2. 감독실당 10명 이상의 응시자 감독
  3. 응시자에 대한 실시간 제어(평가 일시정지, 재개 등)
  4. 웹캠으로 응시자 얼굴, 응시 화면, 모바일 카메라로 주위 환경까지 사각지대 없는 감시
  5. 평가 시간 동안 문제가 없는 서비스 안정성 확보
  6. 구축에 드는 시간과 비용 최소화

실시간 감독 기능의 핵심은 다양한 입력 소스(웹캠, 스마트폰 카메라, 화면 공유 등)에서 영상과 관련 데이터를 받아 감독자에게 실시간으로 전달하는 것이었습니다. 화상 회의 기술과 유사하지만, 평가당 1,000명 이상의 응시자를 수용할 수 있는 큰 규모라는 데 차이가 있었습니다.

또, 화상회의 관련 기술의 대부분은 하나의 미팅 룸에서 10명 이내의 인원만 이용 가능했지만, ‘실시간 감독’은 그 이상의 인원을 수용할 수 있어야 했습니다. 이러한 제약 외에도 구름의 모든 서비스가 AWS 인프라를 사용하며 AWS의 검증된 안전성과 사후 지원, 그리고 2020년 영상과 음성 서비스 모두를 Amazon Chime SDK로 전환한 슬랙(Slack)의 사례를 검토한 끝에, 최종적으로 Amazon Chime SDK를 도입키로 결정했습니다.

Amazon Chime SDK 소개

Amazon Chime SDK는 화상 회의에 필요한 여러 기능을 모아둔 소프트웨어 개발 키트(Software Development Kit)입니다. 특정 플랫폼, 운영체제, 프로그램 언어에서 실행되는 코드를 만들려면 디버거 컴파일러, 라이브러리가 필요한데 Amazon Chime SDK는 이를 한 데 모아서 제공하기 때문에 쉽고 빠르게 화상 회의 서비스를 만들 수 있습니다. 데스크톱뿐 아니라 iOS, 안드로이드 기기에서 화상 회의를 주최하거나, 미팅에 참여하고 채팅하거나, 화면을 공유할 수 있습니다. 특히 WebRTC로 직접 구성할 경우 서버 리소스 관리, 스케일링, 로드밸런싱 등을 직접 관리해야 하지만, Amazon Chime SDK는 ‘서버리스(Serverless)’ 서비스이기 때문에 AWS에서 인프라를 관리해준다는 장점이 있습니다.
Amazon Chime SDK 도입으로 구름DEVTH의 실시간 감독 프로토타입을 단 2주 만에 완성할 수 있었습니다.
개발 과정에서 경험했던 Chime SDK의 주요 장점은 아래와 같습니다.

  1. 편리한 모니터링: AWS ConsoleAmazon CloudWatch를 통해 서비스 상태를 쉽게 모니터링 및 관리
  2. 비용 효율성: 사용량 기반의 과금 체계로 사용량 변동이 큰 코딩 테스트에 유리
  3. 기술 스택 호환성: JavaScript SDK 지원으로 기존 기술 스택과 뛰어난 호환성
  4. 유연한 확장성: RPS(Requests Per Second) 조정 만으로 간편한 스케일 인/아웃
  5. 다양한 언어 지원: JavaScript 외에도 Python, PHP, .NET, Ruby, Java, Go, Node.js 등 다양한 프로그래밍 언어와 프레임워크 지원
  6. AWS 서비스 연동: AWS의 다른 SaaS 서비스와 연동한 통합 솔루션 구축 용이

Amazon Chime SDK 아키텍처

Amazon Chime SDK의 아키텍처는 다음과 같습니다.

<그림2. reference diagram : https://docs.aws.amazon.com/ko_kr/chime-sdk/latest/dg/chime-sdk-meetings-regions.html>

클라이언트 로 볼 수 있는 Customer Applications 영역은 모바일, 웹, 데스크톱 환경에서 실행 되며, 이 애플리케이션에는 Amazon Chime SDK가 통합되어 있어 실시간 음성, 비디오, 채팅 기능 등을 제공합니다. 또한 이용자가 화상 미팅에 참여하는 등의 작업도 처리합니다.
Customer Service는 고객 요청을 처리하는 서버 애플리케이션입니다. 여기에 있는 AWS SDK는 애플리케이션이 Amazon CloudWatch와 같은 다양한 AWS 서비스와 상호작용할 수 있게 돕는 라이브러리 세트입니다. 애플리케이션이 Amazon Chime의 기능을 호출하여 미팅을 시작하거나 관리하고, 미디어 스트림을 처리할 수 있게 돕습니다. IAM(Identity and Access Management)을 이용해 애플리케이션이 AWS 리소스에 접근하는 권한 제어도 해당 영역에서 담당하게 됩니다.
마지막으로 Control Plane RegionMedia Plane Region은 AWS Cloud에 위치한 Amazon Chime의 핵심 영역입니다. Control Plane은 Amazon Chime에서 미팅 관리 및 제어에 필요한 모든 작업을 담당하고 Media Plane은 실시간 미디어 스트림(오디오 및 비디오)을 처리하는 영역입니다. 여기서는 미팅 참여자 간의 오디오, 비디오, 화면 공유 등 실제 데이터가 전송되고 처리됩니다.

Amazon Chime SDK를 활용한 실시간 감독 기능 ‘옵저뷰’

<그림3. 구름DEVTH의 옵저뷰>

구름DEVTH의 실시간 감독 기능의 이름은 ‘옵저뷰(Obserview)’입니다. 감시자, 관찰자란 의미의 Observer에 보다를 뜻하는 View를 합성하여 지은 이름입니다.

옵저뷰는 하나의 화면에서 다수의 응시자를 실시간으로 감독할 수 있습니다. 웹캠, 스마트폰 카메라, 응시 화면 공유, 실시간 시험 로그, 채팅 등의 기능을 제공해 비대면 역량 평가 시 사각 지대 없이 응시자를 감독하고 평가할 수 있습니다.

응시자의 정면, 주위 환경, 응시 화면 외에도 응시자의 행동은 로그로 기록됩니다. 어떤 문제를 언제 풀고 있고, 정답을 맞혔는지 로그에서 확인할 수 있으며 부정 행위가 의심되면 메시지를 보내 주변 환경을 스마트폰 카메라로 비추게 할 수 있습니다. 또한 감독관은 평가를 일시 정지하거나 평가 시간을 연장할 수도 있습니다.

옵저뷰 아키텍처

<그림4. 옵져뷰 아키텍처>

서비스 영역의 API 서버와 웹캠 서버는 Amazon EC2에서 실행되며 API 서버는 Amazon Chime SDK와 API 통신을 담당합니다. 웹캠 서버는 응시자의 응시 화면, 웹캠, 스마트폰 카메라 영상을 캡쳐해 AWS Simple Storage Service(S3)에 저장합니다.

옵저뷰(Obserview)의 실시간 영상 공유 시스템 설명

핵심 플로우
– 응시자마다 하나의 meeting room이 생성됩니다.
– 응시자 본인의 Webcam, Mobile, Screen을 하나씩 Attendee로 초대합니다.
– 감독관은 본인 영상 송출 없이 Attendee로 초대됩니다.
– 감독관은 감독실에서 최대 16명의 응시자를 감독할 수 있습니다.

1. 응시자가 meeting room을 생성하고, 본인의 Webcam/Mobile/Screen을 송출하기 위한 연결

<그림 5. 환경설정 예시>

/** Code_WebRTCManager.js */

const observiewConnector = {};
// 응시자가 대기실에 접속시, chime 미팅룸에 대한 정보를 설정한다.

const setObserviewConnection = async () => {

        // meeting room에 대한 정보를 관리
        let meetData = await getMeetData();
        
        // webcam = 응시자 PC 웹캠
        // screen = 응시자 PC 화면
        // mobile = 응시자 모바일 화면
        const mediaDeviceTypes = ['webcam', 'screen', 'mobile']
        
        // meetData가 없으면 meeting 데이터 생성
        if (!meetData) {
            meetData = await setMeetData();
            ...
        }

        // 가져온 화면 공유 데이터에 따라서 video dom을 생성
        // 웹캠, 화면 공유 타입을 순회하면서 옵저뷰에 영상을 연결
        mediaDeviceTypes.forEach((mediaDeviceType)=>{
                // AWS Chime sdk를 이용하여 응시자 장치 연결을 관리하고 이벤트를 받는 곳
                // 응시자의 비디오 상태가 변할 때 옵저뷰에 알려주는 역할을 하고 영상을 연결하는 역할
                observiewConnector[mediaDeviceType] = getObserviewConnector({});
            
        })

    };
    
    /** 미팅 정보 가져오기 */
    const getMeetData = async () => {
            const { data } =
                await axios.get(
                    `/api/obserview/meetInfo?examId=<<EXAM_ID>>`
                );
            return data;
    };

Code_WebRTCManager.js 는 AWS Chime SDK를 사용하여 웹캠과 화면 공유 스트림을 관리하고, 이를 응시자(사용자)의 비디오 상태와 연결하는 구현부입니다. setObserviewConnector 함수는 웹캠 및 화면 공유와 같은 미디어 장치 데이터를 기반으로 비디오를 연결하고, 실시간 스트리밍을 처리합니다.

  1. meetData 가져오기:
    • getMeetData()를 호출하여 meetData를 가져옵니다. meetData는 미팅에 필요한 정보를 담고 있습니다.
  2. 비디오 DOM 생성:
    • mediaDeviceType에 대해 순회하며 응시자들의 장치 연결을 관리합니다. 이때 웹캠, 화면 공유 등의 다양한 미디어 장치가 존재할 수 있습니다.
  3. AWS Chime SDK를 사용한 장치 연결:
    • ObserviewConnector라는 연결 객체를 통해 AWS Chime SDK를 사용하여 응시자의 비디오 상태를 관리합니다. 이 클래스는 응시자의 비디오 상태가 변할 때 obserview에 이를 알려주고, 스트리밍을 적절히 연결합니다.
  4. 미디어 장치와의 연결:
    • mediaDeviceType 에 대해 ObserviewConnector 객체를 생성하고, 해당 장치 데이터를 meetData 에서 가져와 연결을 설정합니다.
    • 이 과정에서 시험 식별값 같은 데이터를 추가로 전달하여, 각 응시자의 상태를 특정 상황에 맞게 관리합니다.

2. meeting room 정보를 가져오는 구현부

/**    유저가 속한 chime meeting room을 가져온다.*/
router.get(
    '/meetInfo',
    async (req, res) => {
            ...
            
            // 해당 시험 기준으로 유저가 접속 가능한 meeting room이 존재하는지 질의
            const meetInfo = await observiewService.getMeetingInfo({
                examId,
                userId
            });
            
            // 만약 meeting room이 존재하면, 
            // socket을 통해 감독관에게 해당 응시자가 시험실에 접속했다고 통지
            if (meetInfo) {
                socketIo.send_room_message({
                        room: `supervisor-room-id`,
                        arguments: ['obserview-connection-start', userId],
                    });
            }
            res.json(meetInfo);
    }
);

Code_Server routes/api 코드는 router.get 메소드를 사용하여 특정 API 엔드포인트로 들어온 요청을 처리합니다. 실시간 연결을 위한 meetInfo(회의 정보)를 조회하고 응답하는 역할을 합니다.

  1. API 경로 정의:
    • router.get을 통해 /api/observiewConnection/meetInfo 경로로 들어오는 HTTP GET 요청을 처리합니다.
    • 비동기 함수(async)를 사용하여 비동기적으로 데이터를 처리합니다.
  2. 미팅 정보 조회:
    • observiewService.getMeetingInfo() 함수를 호출하여 meetInfo(미팅 정보)를 가져옵니다.
    • 이 함수는 examId, userId과 같은 데이터를 매개변수로 받습니다.
  3. 미팅 정보가 있는 경우:
    • 만약 meetInfo가 존재하면, socketIo.send_room_message()를 호출하여 해당 방(supervisor-room-id)에 메시지를 전송합니다.
      • 이 메시지는 obserview-connection-start이라는 이벤트와 함께 해당 userId를 전달합니다.
      • 이는 감독자(supervisor)에게 응시자가 대기실, 시험실에 연결되었음을 알리기 위한 메시지 입니다.
  4. 클라이언트 응답:
    • 마지막으로 res.json(meetInfo)를 통해 meetInfo 데이터를 클라이언트에게 JSON 형식으로 응답합니다.
/** attendee 초대와 동시에 meeting 생성 */
    async createMeetingWithAttendees({ examId, userId, deviceTypes = [] }) {

        // chime 내 유저 트래킹을 위한 id 생성
        const userObserviewId = generateChimeTrackingId(userId)

        // 해당 시험에 대한 감독관 목록 가져오기
        const supervisorIds =
            await userModel.getSupervisors({ examId }) || [];
        
        // 감독관 목록을 트래킹 가능한 id로 생성
        const supervisorExternalIds =
            supervisorIds
                .map(supervisorId => generateChimeTrackingId(supervisorId))
                .map(supervisorId => ({ExternalUserId: supervisorId}))
        
        // 멀티미디어별 추적시 사용할 id 생성 (응시자 당 최대 3개)
        const userExternalIdsWithDevices = 
                deviceTypes.map((deviceType) => { ExternalUserId: `${userObserviewId}/${deviceType}`})

        // chime에 meeting 생성
        const chimeMeetingAndAttendees = await chime
            .createMeetingWithAttendees({
                ClientRequestToken: `YOUR_ClientRequestToken`,
                MediaRegion: 'YOUR_MediaRegion',
                ExternalMeetingId: `YOUR_ExternalMeetingId`,
                Attendees: [
                    ...userExternalIdsWithDevices,
                    ...supervisorExternalIds,
                ],
            })
            .promise();

        // 생성된 미팅 정보
        const meeting = chimeMeetingAndAttendees.Meeting;

        // 초대된 미팅 참석자 정보 (미디어 별 응시자, 감독관)
        const attendees = chimeMeetingAndAttendees.Attendees || [];

        // DB에 생성된 미팅 정보 추가
        await observiewModel.addConnection({
            examId,
            userId,
            meetingId: meeting.MeetingId,
            info: meeting,
        });
        // DB에 생성된 미팅 참석자 정보 추가
        // 해당 정보를 통해, 감독실 등 접근시, 연결해야 할 대상을 찾아올 수 있다.
        await observiewModel.addAttendees(attendees)

        return {meeting, attendees};
    }

createMeetingWithAttendees 함수는 미팅 생성과 동시에 회의 참석자를 초대합니다. 응시자와 감독관을 미팅에 초대하여, 감독관이 응시자의 영상을 확인할 수 있도록 합니다.

  1. 유저 ID 포맷팅:
    • user id를 chime 내에서 식별 가능한 External Id 형식으로 변환합니다.
  2. 미팅 생성:
    • chime의 createMeetingWithAttendees 함수를 호출하여 meeting을 생성하고, attendee를 추가합니다.
  3. 생성된 미팅 정보를 DB에 저장
    • 감독관이 접속하였을시, 응시자와 연결된 meeting을 사용할 수 있도록 observiewModel.addConnection 를 통해 데이터 베이스상에 접속 정보를 저장합니다.

3. 감독관이 응시자의 화면을 송신받을 수 있도록 연결

<그림6. DEVTH 사용 화면 예시 (출처: https://devth.goorm.io/) 이미지 속 인물 및 이름은 예시를 위해 생성된 스톡 이미지입니다>

다음 코드들은 AWS Chime에 연결된 응시자들의 미팅 정보를 불러오고, 감독관을 해당 미팅에 참가시킵니다. 클라이언트와 서버 각각의 코드와 동작 흐름은 다음과 같습니다.


/**
    Grid 형태로 유저의 Video를 보여주는 컴포넌트
*/
function UserVideoCardGrid({ gridColumnSize, users }) {
    return (
            <CardGrid columnCount={gridColumnSize}>
                {users.map((user, index) => (
                    <UserVideoCard key={user.key} id={user.id}/>
                ))}
            </CardGrid>
    );
}

/**
    옵저뷰에서 응시자가 송출하는 영상을 보여주는 컴포넌트
*/
function UserVideoCard({ id }) {
    return (
        <div
            className={classnames(
                styles.LabelVideo
            )}
        >
            <video id={id} />
        </div>
    );
}
  • 감독실 화면:
    1. 감독관이 실시간 감독실에 들어오면, DB에 저장된 감독실의 응시자 meeting 정보를 불러옵니다
    2. UserVideoCard 컴포넌트는 유저별로 video element를 생성하고, id를 매핑하여 chime의 tile과 연결할 수 있게 합니다.
/**
    chime의 video stream을 연결해 주는 영역
*/
const connectVideo = ({
    user
}) => {
    
    const videoDomInfoList = user.videos.map((video) => ({
        userId: user.id,
        mediaDeviceType: video.type,
        domId: video.domId,
    }));
    const meetingKey = `YOUR_MeetingKey`;
    const meetData = meetingInfoMap.get(meetingKey);
    
    const originMeetingId =
        observiewConnectionMap.get(meetData.meeting.ExternalMeetingId)?.meetingId;
    
    const observiewConnector = getObserviewConnector({
        videoDoms: videoDomInfoList,
    });
    
    observiewConnectionMap.set(meetData.meeting.ExternalMeetingId, {
        userId: user.id,
        meetingId: meetData.meeting.MeetingId,
    })
};
  • connectVideo.js
    1. 감독관이 초대된 meeting에 있는 참석자들의 영상을 송신받기 위해, connector를 연결합니다.
    2. 생성된 video dom의 레퍼런스 배열을 obserview connector로 전달합니다.
/**
    getObserviewConnector 함수 중 일부
*/
import {
  DefaultDeviceController,
  DefaultMeetingSession,
  MeetingSessionConfiguration,
} from 'amazon-chime-sdk-js';

const API_ENDPOINT_URL = 'https://<<AWS_API_GW_ENDPOINT>>/api/meetingevents';

const qualitySet = {
  // 해상도
  'resolution': {
    width,
    height,
    frameRate,
    maxBandwidthKbps
  }
};

const getObserviewConnection = async ({
  meeting,
  attendee,
  logger,
  quality,
  videoStream,
  onChangeRemoteVideos,
  DEFAULT_INTERVAL_SEC
}) => {
  let meetingEvents = [];

  const deviceController = new DefaultDeviceController(logger);
  deviceController.chooseVideoInputQuality(
    qualitySet[quality].width,
    qualitySet[quality].height,
    qualitySet[quality].frameRate,
    qualitySet[quality].maxBandwidthKbps
  );
  const configuration = new MeetingSessionConfiguration(meeting, attendee);
  const meetingSession = new DefaultMeetingSession(
    configuration,
    logger,
    deviceController
  );

  const sendMeetingEvents = () => {
    fetch(API_ENDPOINT_URL, {
      method: 'POST',
      body: JSON.stringify(meetingEvents),
    });
  }

  if (videoStream) {
    await meetingSession.audioVideo.chooseVideoInputDevice(videoStream);
  }

  const observer = {
    audioVideoDidStop: (sessionStatus) => {
      const sessionStatusCode = sessionStatus.statusCode();
      // statusCode를 통한 예외처리 진행
    },
    eventDidReceive: (name, attributes) => {
      // 이벤트 수신시 진행할 로직
    },
  };

  if (onChangeRemoteVideos) {
    observer.remoteVideoSourcesDidChange = (remoteVideoSources) => {
      // 응시자 device 연결을 추적하기 위한 remote video length 추적
      onChangeRemoteVideos(remoteVideoSources?.length || 0);
    };
  }
  
  if (Array.isArray(videoDoms) && videoDoms.length > 0) {
    observer.videoTileDidUpdate = (tileState) => {
        // 실제 미팅에 참여한 유저가 dom이 있나 확인한다
        const videoDomInfo = videoDoms.find(
            (dom) =>
                tileState.boundExternalUserId === getUserExternalId(dom.userId, dom.mediaDeviceType)
        );

        const videoDom = document.getElementById(videoDomInfo.domId);
        if (videoDom) {
            meetingSession.audioVideo.bindVideoElement(
                tileState.tileId,
                videoDom
            );
        }

    };
}

  meetingSession.audioVideo.addObserver(observer);
  meetingSession.audioVideo.start();
  meetingSession.audioVideo.startLocalVideoTile();

  const interval = setInterval(() => {
    sendMeetingEvents();
  }, DEFAULT_INTERVAL_SEC);

  return {
    meetingSession,
    interval,
  };
};

export default getObserviewConnection;
  • observiewConnector.js
    1. obserview connector 내에서, 실질적인 chime 연결을 관리합니다.
    2. bindVideoElement 함수를 통해 응시자의 영상을 감독관이 볼 수 있도록 설정하고, 응시자의 송출 상황이 변경되었을때 업데이트 합니다.
// 서버에서 응시자가 옵저뷰에 접속 했다는 것을 감지하면, 해당 응시자와 미팅 연결
    socket.on('obserview-connection-start', (userId) => {
            fetchObserviewConnection({userId, examId});
    });
        
        
// 특정 응시자의 AWS chime 미팅 정보를 조회 하며 감독관을 참가자로 추가한다.
    export const fetchObserviewConnection = async ({ userId, examId }) => {
      const response = await axios.get('/api/manager/observiewConnection', 
              { params: { examId, userId } });
      return {
        params: { userId },
        data: response.data,
      };
    };
  • socketManager
    1. 만약 응시자가 대기실 연결을 성공하면, 해당 감독실에 들어있는 감독관들에게 알림이 전송됩니다.
    2. 알림을 수신한 감독관은, 감독실 정보를 갱신하여 새로운 유저가 접속한것을 인지합니다.

2,000명 규모의 대규모 코딩 테스트 사례

구름DEVTH의 서비스에서 가장 중요한 것은 중단 없이 역량 평가를 제시간에 안정적으로 진행하는 것입니다. 그다음으로 중요한 것이 ‘원활한 실시간 감독’ 서비스의 제공입니다. 이 두 가지 요소는 비대면 역량 평가의 신뢰성을 결정짓는 핵심 요소입니다.

2024년 6월, 구름DEVTH는 이러한 서비스의 안정성과 효율성을 검증할 수 있는 대규모 코딩 테스트를 진행했습니다. N사에서 실시한 테스트는 약 2,000명의 응시자와 100여 명의 감독관이 참여하는 대규모 행사였습니다.

대규모 이벤트 고려 사항 및 준비 과정

역량 평가가 시작되면 응시자가 몰립니다. 그러한 최악의 상황에서도 구름DEVTH는 중단 없이 서비스되어야 합니다. 만약 서비스에 장애가 발생하면 특정 응시자에게 시스템 오류가 발생하거나, 시스템 속도가 느려져 시험의 공정성이 훼손될 수 있습니다. 피해는 응시자에 그치지 않고, 고객사에게까지 큰 피해를 끼치게 됩니다.
따라서 구름은 코딩 테스트와 같은 역량 평가 전, 다음과 같은 준비 과정을 거쳐 준비했습니다.

  • “D-30” : 비즈니스 담당자가 코딩 테스트 계약을 체결했습니다. 일정과 인원수, 시험 옵션이 공유됐습니다. 최초 응시 예정 규모는 약 3000명이었습니다.
  • “D-14” : SRE Squad와 Product Squad 간에 응시 규모 대비 서버 스케일링 규모 설정에 대해 논의했습니다. WAS 및 Code Build Server 전개 규모 정하고 Chime과 관련하여 동시 접속에 대한 예상 커넥션 수 산출했습니다.
  • “D-12” : Chime 관련하여 AWS 측에 사용 예상 RPS Limit 설정 항목 관련하여 제한 완화 요청했습니다. (CreateMeetingWithAttendees)
  • “D-7” : AWS Console에서 Chime API RPS Limit 완화 여부를 확인했습니다.
  • “D-2” : 시험 대비 서버 스케일 아웃, 시나리오를 설정한 Load Testing 진행했습니다.
  • “D-day” : 대규모 코딩 테스트 시험을 치뤘습니다.

응시자들은 역량 평가에 앞서 대기실에 입장해 대기합니다. 실시간 감독에 필요한 설정(화상카메라, 스마트폰, 화면 공유)을 하고 테스트합니다. 시간이 되면 시험실에 입장해 시험을 치룹니다.
대기실에는 보통 30분 전부터 응시자가 입장하기 시작합니다. 문제는 시험이 시작되면 동시에 유저가 접속하는 데 있습니다. 다른 역량 평가 사례에서 보인 대기실로 접속하는 최대 접속자 추이는 다음과 같습니다.

<그림 7. X축 = 시간(분), Y축 = 입장 응시자 수 (백명)>

이러한 과거 패턴을 근거로, 역량 평가 시작 시 예상되는 최대 사용자 수와 예상 커넥션 수, 트래픽을 감안해 WAS와 Code Build Server 스케일링 규모 등을 미리 산정했습니다.
응시자가 대기실에 입장하면 응시자가 접속하게 될 미팅룸이 생성됩니다. 미팅룸은 인당 1개만 생성되도록 최대 3,000개를, 미팅에 참여한 참석자 수는 3,000명당 옵저뷰에 필요한 미디어 스트리밍(카메라, 화면 공유, 웹캠)을 3개로 계산해 9,000+a개로 제한을 두었습니다.

시험 당일에는 Amazon CloudWatch Logs의 Chime Event Log로 RPS를 산출하고 모니터링했습니다.

<그림8. Chime Event Log>

Chime 서버와 통신 시 발생한 에러 상황 등은 서비스 로그로 수집되는데, 이와 Chime Event Log와 크로스 체크하며 모니터링을 했습니다.

<그림9. 서비스 로그>

마무리

2016년 출시된 구름DEVTH 옵저뷰의 실시간 감독 기능은 2024년 1월 AI로 더욱 개선되었습니다. 기존 옵저뷰는 실시간 감독 시 감독실에 사람이 접속해 감독해야 했지만, AI 옵저뷰는 시험 후 AI가 부정행위를 자동으로 탐지합니다. 이제 감독관이 직접 실시간으로 감독할 필요가 없습니다.

스타트업이 아닌 대기업이더라도 언제나 인력과 비용은 충분치 않습니다. 자체 서비스를 개발로 방향을 잡을 수도 있었지만, 구름은 AWS라는 신뢰할 수 있는 파트너사의 이미 여러 레퍼런스로 검증된 아마존 Chime SDK를 도입한 끝에 단 2주 만에 프로토타입을 만들고 빠르게 서비스함으로써 시장의 요구를 충족하고 비대면 역량 평가 시장에서 선도적 입지를 구축할 수 있었습니다.

앞으로도 구름은 AWS라는 신뢰할 수 있는 파트너사와 함께 ‘역량 평가’ 시장에 발빠르게 대응하며 고객 중심의 비대면 역량 평가 서비스를 지속해 나갈 계획입니다.

김기덕 / 구름(goorm)

김기덕님은 구름에서 코딩테스트 플랫폼 개발을 담당하고 있습니다. 풀스택 개발을 진행하고 있으며 스쿼드 리드를 맡고 있습니다.

기우석 / 구름(goorm)

기우석님은 구름에서 코딩테스트 플랫폼 개발을 담당하고 있습니다. 백엔드 개발을 담당하고 있으며 대규모 트래픽 처리와 성능 최적화에 많은 관심이 있습니다.

김지은 / 구름(goorm)

김지은님은 구름에서 코딩테스트 플랫폼 개발을 담당하고 있습니다. 풀스택 개발을 진행하고 있으며, 클린 아키텍처를 지향합니다. 가독성 있는 코드를 작성하기 위해 많은 노력을 하고 있습니다.

 

Seongjin Ahn

Seongjin Ahn

안성진 Solutions Architect는 다양한 고객 인프라 운영 경험을 바탕으로 AWS 서비스의 효율적인 사용을 위해 Startup 고객에게 기술을 지원하는 역할을 합니다.

Cheol Kang

Cheol Kang

강철님은 AWS 스타트업팀 어카운트 매니저입니다. 스타트업 고객분들이 폭넓고 깊이있는 AWS 클라우드 서비스를 통해 고객의 환경에 맞는 최적의 서비스를 개발 및 운영하고, 이를 통해 비즈니스 성과를 달성하실 수 있도록 지원하고 있습니다.