가상축구 리그 일정 최적화 알고리즘

리그 스케줄이 어수선하면 관객의 대기 시간이 늘고, 팀별 체력 분배가 뒤틀리고, 중계 동시접속이 급증하는 구간에 서버 병목이 생긴다. 현실 스포츠도 그렇지만, 가상축구는 특성상 더 민감하다. 경기 시간이 짧고 이벤트 밀도는 높으며, 다종의 가상 스포츠가 같은 플랫폼에서 동시에 돌아가기 때문이다. 개발실에서 운영 스케줄을 하루만 잘못 짜도 코호트 유지율이 3에서 5%포인트 떨어지는 것을 몇 번이나 확인했다. 일정 최적화는 미학이 아니라 생존 방법에 가깝다.

image

여기서는 가상축구 리그를 기준으로 이야기를 풀되, 가상농구, 가상경마, 가상개경주처럼 전개 속도와 경기 길이가 다른 종목을 동시에 다루는 플랫폼 상황을 염두에 둔다. 특정 알고리즘 이름만 나열하지 않고, 실제 운영 중 만나게 되는 제약과 돌발 변수까지 포함해 설계와 구현의 디테일을 정리한다.

문제 정의를 제대로 세우기

가상축구 리그 일정 최적화의 핵심은 세 가지 축으로 정리된다. 첫째, 매칭 공정성. 팀들은 균형 잡힌 홈, 원정, 연속 경기 수, 상위권과 하위권 대진 분포를 갖춰야 한다. 둘째, 운영 효율. 경기들은 플랫폼의 트래픽 파형과 결제 피크, 중계 슬롯, 딜러 서버의 RNG 시드 배포 로직과 맞물려야 한다. 셋째, 사용자 경험. 경기 간 템포가 적절해야 하고, 이벤트가 겹쳐 피로도가 높아지지 않아야 한다.

가상축구에서는 한 경기가 3에서 6분 안팎으로 끝나는 경우가 많다. 하이라이트 중심의 연출이면 2분 컷도 가능하다. 그만큼 라운드 회전이 빠르니, 20팀 기준 38라운드를 하루 안에 돌릴 수도 있다. 단, 같은 팀이 너무 짧은 간격으로 반복 노출되면 이탈이 생긴다. 실제로 8분 이하 간격으로 같은 팀이 세 번 노출된 구간에서 평균 체류 시간이 12에서 15% 줄어드는 패턴을 본 적이 있다. 스케줄러는 휴식 간격을 변수로 두고, 팀별 노출 간격을 조정해야 한다.

여기에 가상농구나 가상경마, 가상개경주를 함께 운영하면 리소스 충돌이 생긴다. 예를 들어 가상경마는 레이스가 1분 내외로 빈번하게 돌아가며 베팅 마감과 결과 발표가 촘촘히 붙는다. 가상농구는 포제션 단위 연출로 4분에서 7분 정도의 길이를 가진다. 가상개경주는 경마보다 약간 짧지만 동시 레이스 수가 많다. 동일한 지갑 서비스와 스트리밍 인프라를 쓰는 경우, 종목 간 동시 피크를 피하도록 슬롯을 어긋나게 배치해야 한다.

목표 함수, 현실적으로 구성하기

목표 함수는 여러 항의 가중합이 된다. 단일 목적 최적화는 현장에서 잘 작동하지 않는다. 아래 다섯 가지 정도가 보편적이며, 플랫폼의 전략에 따라 가중치를 조절한다.

    팀별 노출 간격 편차 최소화: 같은 팀 연속 노출 간격을 10에서 15분 이상으로 벌리고, 표준편차를 최소화한다. 동시접속 부하 평탄화: 초당 처리량 TPS를 기준으로 라운드 시작 시각을 분산시키고, 95퍼센타일 대기시간 목표를 만족시킨다. 대진 공정성: 홈, 원정 비율, 중상위 팀 연속 대진 수, 이동거리(가상에서도 서버 존 이동 비용 혹은 시청자 언어권 전환 비용으로 모델링) 균형을 맞춘다. 수익 기여도 최적화: 결제 전환율이 높은 시간대에 핵심 매치를 배치하되 과도한 몰림을 방지한다. 교차 종목 간 슬롯 충돌 최소화: 가상농구의 쿼터 브레이크, 가상경마의 출발 시보, 가상개경주의 집입 구간과 겹치지 않도록 오프셋을 준다.

수치 최적화에 넣기 전에, 각 항의 단위가 다르다는 점부터 정리한다. 노출 간격은 분 단위, 부하는 초당 요청, 공정성은 조합적 지표다. 표준화가 필요하다. 보통 z-score로 정규화하고, 운영 중 얻은 A/B 테스트 결과로 가중치를 갱신한다. 예컨대 두 달 전 실험에서 동시접속 평탄화 항의 가중치를 0.3에서 0.45로 올렸을 때, 피크 대비 평균 TPS 비율이 1.9에서 1.5로 완화되면서 오류율도 0.6%포인트 감소했다.

필수 제약 조건, 빠뜨리면 망가지는 것들

제약은 하드와 소프트로 나눈다. 하드는 위반하면 안 되고, 소프트는 위반시 패널티로 모델링한다. 하드는 경기당 딱 한 번만 배정, 중복 시간 금지, 라운드 내 고유성 같은 기본 조건이자 라이선스 계약으로 못 박힌 방송 슬롯 같은 외부 구속도 포함한다. 소프트는 휴식 시간, 트래픽 분산, 스토리라인 유지 같은 가변 요소가 많다.

팀 휴식은 팀당 최소 r min, 최대 rmax로 모델링한다. 실제로 r min을 8분, rmax를 50분으로 두었더니 사용자 체류가 안정됐다. 너무 길면 잊히고, 너무 짧으면 피로감이 쌓인다. 대진 공정성의 경우, 상위권 팀끼리의 맞대결을 특정 날짜의 황금 시간대에 몰아주는 요청이 들어오곤 한다. 이건 수익에는 도움되지만 공정성 패널티를 늘린다. 운영회사가 감수할 범위는 숫자로 정리해두는 게 좋다. 예를 들어 상위권 더비를 하루 3경기 이상 배치하지 않거나, 연속 라운드에서 같은 팀이 더비를 두 번 치르지 않도록 한다.

교차 종목 충돌은 종종 간과된다. 가상경마의 경우 출발 순간의 동시요청 폭발이 가장 크다. 가상농구는 쿼터 종료 리플레이 구간에 체류가 몰린다. 이 타이밍과 가상축구 킥오프가 겹치면, 라이브 댓글과 배당 업데이트가 한꺼번에 들어와서 캐시가 녹는다. 스케줄러는 종목 간 충돌 매트릭스를 미리 만들고, 최소 간격을 보장하도록 제한을 둔다.

알고리즘 선택, 정답은 없고 상황이 있다

스케줄링은 전형적인 NP-난해 문제다. 정확해 보이는 정수계획법을 쓰면 깔끔하지만, 운영 현실에선 수 초 이내 재계산이 필요한 순간이 잦다. 따라서 조합 최적화의 가상축구 재료를 섞어 쓰는 편이 실전에서 잘 통한다.

라운드 로빈의 기초는 1-팩토리제이션, 흔히 말하는 circle method로 충분하다. 20팀이면 19라운드를 돌리되, 라운드마다 마지막 팀을 고정하고 나머지를 회전시킨다. 홈과 원정을 뒤집어 38라운드를 만든다. 이 기본표는 순식간에 생성된다. 문제는 이후이다. 광고, 서버, 휴식, 교차 종목을 반영해 미세조정을 거쳐야 한다.

정수선형계획법은 의사결정 변수 x_m,t를 경기 m의 시간 슬롯 t 배정 여부로 두고, 제약을 선형식으로 적는다. 소프트 제약은 벌점 변수를 둬 패널티로 처리한다. 구글 OR-Tools CP-SAT는 부등식, 불리언, 구간을 자연스럽게 표현할 수 있어 이 문제에 맞다. 다만 20팀, 38라운드, 10초 단위 슬롯 24시간이면 슬롯 수가 8640이고, 매치 수는 380이라 변수 수가 수백만으로 뻗을 수 있다. 따라서 슬롯을 30초나 1분 단위로 뭉치고, 라운드 시작점 후보를 몇 개로 제한하는 프리세팅이 필요하다.

image

메타휴리스틱은 실전의 친구다. 시뮬레이티드 어닐링은 빠르게 수용할 만한 해로 수렴한다. 온도를 1.0에서 시작해 0.0001까지 로그 스케줄로 낮추고, 인접해는 두 경기의 시간 슬롯을 스왑하거나 라운드 시작점을 미세 이동하는 방식이 일반적이다. 유전 알고리즘은 다수의 해를 병렬 탐색할 수 있지만, 교차 연산을 잘못 설계하면 하드 제약을 깨기 쉽다. 탭 서치는 단기 중복을 피하며 지역 최적해에서 탈출하는 데 장점이 있다. 운영 중에는 CP-SAT로 초기 feasible 해를 만든 뒤, 시뮬레이티드 어닐링이나 탭 서치로 실시간 튜닝하는 하이브리드 구성이 안정적이다.

현장에서 체감한 점을 덧붙이자면, 탐색보다 평가가 병목이 된다. 목표 함수 평가에서 트래픽 시뮬레이션을 매번 돌리면 수 ms 타임박스를 넘는다. 사전 학습된 경량 회귀모델로 TPS와 오류율을 근사 예측하고, 100번에 한 번 정도만 상세 시뮬레이션을 태우면 속도와 정확도의 균형이 잡힌다.

데이터 구조와 캐싱, 평범하지만 치명적이다

스케줄러는 결국 배열과 그래프의 싸움이다. 팀 간 제약은 그래프 색칠과 유사한 구조를 가진다. 한 라운드 내에서 같은 팀은 하나의 매치에만 속해야 하므로, 간섭 그래프의 최대 차수는 라운드의 크기와 동일하다. 시간 슬롯은 세그먼트 트리로 관리하면 구간 충돌 검사가 빠르다. 특히 다종 종목 충돌 검사를 할 때, 슬롯별 카운트를 빠르게 합산해야 한다. 사전 계산된 prefix sum과 비트셋을 섞으면 CPU 캐시 친화적으로 운용된다.

외부 제약, 예를 들어 방송사 A의 골든타임 20시에서 22시를 팀 X에게 한 번만 줄 수 있다는 요구는 집합 제약으로 들어온다. 이를 위해 각 팀에 허용 슬롯의 비트마스크를 만들고, 할당 시 비트 연산으로 즉시 가능여부를 판단한다. 매 라운드마다 전체 탐색을 하면 느려진다.

캐싱은 스케줄뿐 아니라 벌점 계산에도 필요하다. 예컨대 팀별 연속 원정 카운트, 휴식 간격 히스토리는 매번 누적 계산하지 말고, 변경된 매치만 반영하는 증분 업데이트를 걸어야 한다. 380경기 전체를 다시 평가하는 우직한 방식은 훈련 단계에만 허용된다.

경기 길이 분포와 난수 시드, 가상 스포츠 특유의 이슈

가상축구는 시뮬레이션 기반이므로 경기 길이가 확률 분포를 가진다. 하이라이트 컷 기반이면 평균 3분, 표준편차 20에서 30초 정도가 흔하다. 스케줄러는 고정 길이로 가정하면 안 되고, 지연을 고려한 버퍼를 둬야 한다. 특히 라운드 시작 간격을 4분으로 고정했는데 경기가 3분 40초로 자주 늘어나면, 뒤쪽으로 갈수록 지연이 누적된다. 실전에서는 경기 뒤에 15에서 20초의 버퍼를 붙이고, 지연 누적이 1분을 넘으면 라운드 시작을 재산정하는 리샘플링 규칙을 둔다.

RNG 시드는 공정성과 관련된다. 같은 시드가 반복으로 배정되면 결과 패턴이 반복된다는 오해를 부를 수 있다. 스케줄링 과정에서 시드풀을 시간, 팀 조합, 라운드 번호의 해시로 섞고, 지갑 트랜잭션 블록 해시 같은 외부 엔트로피를 소량 주입하면 심리적 불신을 줄일 수 있다. 단, 외부 엔트로피 사용은 감사 가능성을 해칠 수 있으니, 로깅과 공개 문서를 준비해야 한다.

교차 종목 동기화, 플랫폼 차원의 최적화

가상농구, 가상경마, 가상개경주가 같은 뷰어와 백엔드를 공유한다면, 스케줄러는 단일 종목만 볼 수 없다. 각 종목마다 길이와 피크 포인트가 다르다. 플랫폼 운영에서 효과적이었던 한 가지 규칙은, 서로 다른 종목의 피크 이벤트를 최소 20초, 가능하면 40초 이상 벌려두는 것이다. 예를 들어 가상경마 출발을 매 2분 정각에 맞추고, 가상축구는 2분 정각에서 30초 뒤, 가상농구는 쿼터 시작을 1분 10초 뒤로 미는 식이다. 이렇게 오프셋을 걸어두면 같은 순간에 채팅, 배당 업데이트, 하이라이트 렌더가 겹치지 않는다.

가상개경주는 레이스 회전 속도가 빨라 다수 동시 레이스를 돌리는 경우가 많다. 동일 인프라에서 처리하려면, 종목 간 동시 레이스 상한을 정하고, 상한 초과 시 자동으로 다음 슬롯으로 밀어내는 큐잉이 필요하다. 이때 사용자의 대기 시간을 5초 이상 늘리지 않는 범위에서만 밀어내야 한다. 실험 결과 7초를 넘는 대기는 복귀율에 악영향을 줬다.

예시 시나리오, 20팀 가상축구 리그

가정: 20팀, 38라운드. 경기 평균 3분 10초, 표준편차 25초. 플랫폼 공용 슬롯, 가상경마는 매 2분 정각, 가상농구는 6분 주기, 가상개경주는 90초 주기.

1단계로 circle method로 38라운드 기본 대진표를 생성한다. 2단계로 방송 슬롯과 스토리라인 요청을 반영한다. 예컨대 상위권 매치 8개를 황금 시간대에 흩뿌리되, 하루 최대 3경기 제한 규칙을 지킨다. 3단계로 각 라운드 시작점을 후보군 8개로 제한하고, CP-SAT로 하드 제약을 만족하는 배치를 구한다. 4단계로 어닐링으로 소프트 제약 벌점을 줄인다. 온도 스케줄은 1.0에서 시작해 50,000번 반복 중 로그 감소. 5단계로 교차 종목 충돌 매트릭스를 적용해, 피크 오프셋 최소 30초를 확보한다.

이 과정을 거치면, 팀별 최소 휴식 9분, 최대 46분을 보장하고, 전체 피크 TPS는 1.6배 수준으로 제한된다. 상위권 더비는 황금 시간대에 2경기 배치, 나머지 1경기는 21시 40분대에 붙인다. 라운드 간 시작 간격은 4분 10초에서 5분 범위로 부드럽게 흔들리도록 만들었다. 가상경마의 출발과 가상축구의 킥오프가 겹치는 경우는 120라운드 후보 중 3회로 낮췄고, 이 3회도 25초 오프셋 범위 안에 뒀다.

평가 지표와 모니터링

모델이 낸 결과가 좋은지 나쁜지는 런타임에서 판가름난다. 시뮬레이션 상 좋았던 스케줄이 실제에선 예상 못한 병목을 만든다. 따라서 배포 전후에 비교할 항목을 딱 정해둬야 한다. 운영에선 다음 지표를 가장 자주 들여다본다.

    95퍼센타일 응답시간과 피크 TPS 비율: 스케줄 전환 후 72시간, 주간 단위로 확인한다. 장애는 애초에 피해야 하므로, 감소폭이 적어도 증가하면 즉시 롤백 준비를 한다. 팀별 평균 노출 간격과 표준편차: 10에서 15분 사이에 머무는지 확인하고, 특정 팀이 과소 혹은 과다 노출되지 않았는지 본다. 라운드 지연 누적: 특정 라운드가 60초 이상 지연되면 알람을 울리고 다음 라운드 시작점을 재조정한다. 버퍼 정책의 성능을 수치로 확인한다. 교차 종목 피크 충돌 건수: 30초 오프셋 규칙을 지켰는지 자동 검증한다. 위반이 생기면 다음 날 배치에 반영한다.

이 지표는 단순 로깅 수준이 아니라, 스케줄러의 피드백 루프에 직접 들어가야 한다. 하루 단위로 가중치를 미세 조정하는 온라인 러닝을 붙이면, 계절성이나 이벤트 주간의 변화를 따라갈 수 있다.

실시간 교정, 돌발 변수에 대응하는 방법

운영 중엔 예기치 못한 일이 생긴다. 스트리밍 CDN 한 구간이 느려지거나, 결제 게이트웨이 타임아웃이 늘거나, 갑자기 외부 트래픽이 몰리는 일도 있다. 이때 스케줄러는 계획을 수정해야 한다. 전체를 다시 짜는 것은 불가능하니, 국소적 재배치로 대응한다.

여기서 중요한 원칙은, 하드 제약을 지키면서 소프트 벌점 증가를 최소화하는 것이다. 우선 지연 누적이 큰 라운드를 찾아, 뒤의 두 라운드 시작을 각각 20에서 30초씩 미뤄준다. 이어 교차 종목 피크와 겹치지 않도록 오프셋을 재가한다. 마지막으로, 팀별 노출 간격이 r_min 아래로 내려가는지 확인하고, 한두 경기를 인접 슬롯과 스왑한다. 이 때 탐색 폭을 라운드 주변으로 한정해야 응답성이 유지된다. 구현에선 neighborhood를 라운드 ±2로 제한하는 방식이 무난했다.

수익과 공정성의 줄다리기

사업팀이 상위권 매치를 황금 시간대에 몰아 넣고 싶어할 때, 기술팀은 서버 부하와 공정성의 악화를 걱정한다. 어느 쪽이 옳다 그르다의 문제가 아니라, 가중치와 한계선을 미리 합의해두느냐의 문제다. 한 차례 굵직한 캠페인에서, 상위권 더비를 20시대에 몰아 넣은 결과 ARPU는 4에서 7% 상승했지만, 피크 TPS가 2.3배까지 치솟았고 오류율이 1%포인트 늘었다. 스케줄러의 벌점 구조를 조정해 더비 몰림을 완화하고, 오프셋을 넉넉히 주자 수익은 2에서 3%포인트 떨어졌지만 오류율은 기존 수준으로 복귀했다. 이후로는 페이백 기간과 장기 지표까지 고려해 가중치 테이블을 주간 단위로 업데이트한다.

데이터 품질, 기본 중의 기본

스케줄링은 입력 데이터가 전부다. 팀 가중치, 경기 길이 분포, 결제 피크, 서버 지연, 외부 이벤트 캘린더가 맞아야 계산이 맞는다. 가상축구의 경기 길이는 연출 정책 변경만으로도 10% 이상 변할 수 있다. 연출팀이 하이라이트 컷을 늘리면 평균 길이가 늘고, 스케줄은 순식간에 어긋난다. 배포 파이프라인에 연출 정책 변경 이벤트를 붙이고, 길이 분포를 30분마다 재학습하면 틀어짐을 빠르게 잡아낸다. 또한 가상경마, 가상개경주의 레이스 수 변동도 스케줄에 입력해야 한다. 이벤트성 레이스를 추가할 때는 사전에 슬롯을 비워두거나, 자동 밀어내기 규칙을 명확히 해야 한다.

간단한 코드 스케치

실전 코드는 길어지기 마련이지만, 구조를 요약하면 다음과 같다. 상세 구현은 각자의 기술 스택과 인프라 제약에 맞춰 조정하면 된다.

# 입력: teams, rounds, slot_candidates, cross_sport_peaks, constraints, weights # 출력: assignment[m] = slot Base_schedule = circle_method(teams) # 38라운드 대진표 Slots = preselect_slots(day_start, day_end, step=60) # 1분 단위 후보 Model = CPSAT() X = (m, t): model.bool_var() for m in matches(base_schedule) for t in slots # 하드 제약 For m in matches(base_schedule): Model.add(sum(x[m, t] for t in feasible_slots(m, constraints)) == 1) For t in slots: Model.add(sum(x[m, t] for m in matches_overlapping(t)) <= capacity(t)) # 소프트 제약 벌점 변수 Penalty = [] Penalty += rest_time_penalties(x, weights['rest']) Penalty += fairness_penalties(x, base_schedule, weights['fairness']) Penalty += cross_sport_penalties(x, cross_sport_peaks, weights['cross']) Model.minimize(sum(penalty)) Solution = model.solve(time_limit=5.0) # 초 단위 제한 Assignment = local_search_refine(solution, eval_fn, budget=0.2) # 초 단위 예산 <p> 핵심은 time_limit와 budget처럼 시간 예산을 명확히 두는 것이다. 스케줄러는 최고의 해가 아니라 충분히 좋은 해를 제시간에 내는 도구다.

가상농구, 가상경마, 가상개경주와의 비교 관점

가상농구는 쿼터 단위의 장단주기가 뚜렷해, 스케줄링에서 페이싱 제어가 중요하다. 6분 주기 안에 베팅 마감, 하이라이트, 쿼터 인터벌이 반복되므로, 가상축구 킥오프를 쿼터 종료 직후 20에서 40초 뒤로 배치하면 체류가 부드럽게 이어진다.

가상경마는 입장, 출발, 결승, 결과 발표가 약 60에서 90초 주기로 빠르게 돈다. 피크는 출발 직전과 결과 발표 직후 두 군데가 명확하다. 스케줄러는 이 두 피크에 맞춰 가상축구의 킥오프를 비켜야 하며, 피크 오프셋을 두 겹으로 관리하면 성능이 더 안정된다.

가상개경주는 레이스 수를 많게 잡는 경향이 있어 동시성 관리가 관건이다. 캐시 키 전략과 DB 연결 수 상한을 스케줄링 레벨에서 고려해야 하고, 필요하면 가상축구 쪽 라운드 시작을 미세하게 흔들어 동시성 상한을 넘지 않도록 한다.

운영 환경에서의 트릭들

테스트 환경에서 완벽하게 보이던 스케줄이 실서버에서 뒤틀리는 이유는 장애 한 번이면 충분하다. 예방책으로는 세 가지가 특히 유효했다.

    시뮬레이터의 비관적 가정: 평균 대신 95퍼센타일 경기 길이로 버퍼를 산정하고, CDN 지연 50에서 150ms를 추가로 더한다. 백오프 계획의 계층화: 지연 누적이 30초, 60초, 90초를 넘을 때 각각 다른 강도의 재배치를 실행한다. 90초에서는 라운드 전체를 1분 밀고, 교차 피크 규칙을 일부 완화하는 식이다. 사용자 메시징: 예기치 못한 조정이 생길 때 배너나 자막으로 사유와 예상 지연을 안내하면 불만이 확 줄어든다. 스케줄러가 운영툴과 직접 연계해 메시지를 푸시하도록 만들어두면 현장에서 유용하다.

윤리와 투명성

가상 스포츠는 난수와 연출이 얽힌 콘텐츠다. 공정성에 대한 의심을 상시로 받기 쉽다. 스케줄러가 특정 팀이나 매치업을 유리하게 배치하는 것은 계약이나 내부 가이드라인 위반이 될 수 있다. 이런 문제를 피하려면, 하드 제약과 벌점 구조, RNG 시드 정책을 감사 가능한 방식으로 문서화하고, 최소한의 내부 검증 프로세스를 갖춰야 한다. 교차 종목 간 밀어내기 규칙도 이 문서에 포함되어야 한다. 분쟁이 생겼을 때 근거가 된다.

마무리 메모, 실전에서 통했던 판단들

처음부터 거창한 최적화를 꿈꾸기보다, 라운드 시작점 후보를 줄이는 프리세팅이 일반적으로 가장 큰 체감 성능 향상을 준다. 다음으로, 평가함수의 경량화가 탐색 속도를 좌우한다. 마지막으로, 교차 종목 오프셋 규칙을 단순하고 강력하게 유지하는 편이 장애를 줄이는 데 가장 도움이 된다. 가중치의 정답은 없지만, 지표를 정직하게 보고 조금씩 조정하면 사용자 체감이 달라진다.

가상축구, 가상농구, 가상경마, 가상개경주가 같은 플랫폼 위에서 조화를 이루려면, 스케줄러는 단일 종목의 이익을 넘어 전체의 안정과 흥분의 균형을 만들어야 한다. 목표 함수의 몇 개 항과 제약의 몇 줄이 매일의 체류와 매출, 그리고 신뢰를 가른다. 결국 일정 최적화는 숫자를 잘 맞추는 기술이면서, 콘텐츠의 리듬을 세심하게 조율하는 일이다. 운영 현장의 데이터를 믿고, 작은 실험을 이어가며, 알고리즘과 규칙을 같이 다듬는 팀이 원하는 결과에 가장 빨리 닿는다.