상태: 해결 (2026-10-15 09:43, 주석 수정). 사실은 묶음 게이트의 실행 기록과 게이트 로그의 실패 문구, 커밋 기록에서 가져왔습니다. 정규식 비교 표는 파이썬으로 직접 돌린 값입니다. 정규식과 범인 찾기를 고치는 일은 아직 하지 않았습니다.
목차
- 한 줄 요약
- 1. 무슨 일이 있었나
- 2. 왜 「268,729」 가 「729」 인가: 단어 경계
- 3. 왜 8시간 반이나 걸렸나: 범인을 못 가린 묶음 게이트
- 4. 무엇을 고쳐야 하나
- 5. 이 사고가 가르쳐 준 것
- 참고
한 줄 요약
| 영향 | 10-15 01:05 ~ 09:43, 묶음 게이트 26번 연속 실패. 같이 묶인 작업이 3개에서 19개까지 쌓여 아무것도 배포되지 않았다 |
| 원인 | 주석의 「268,729 바이트」 가 상표 검사 정규식 `\b(… |
| 왜 길어졌나 | 실패한 시험이 바뀐 파일과 이름이 달라 범인 찾기가 「모름」을 냈고, 루프는 같은 묶음을 다시 돌렸다 |
| 고친 것 | 주석을 「약 262KiB」 로. 다음 바퀴에 19개가 함께 통과 |
| 남은 것 | 정규식 경계, 범인 찾기, 같은 실패가 되풀이될 때 멈추고 알리는 장치 |
1. 무슨 일이 있었나
카메라 점수판은 TT Lab 의 대표 화면입니다. 첫 화면이 그리지도 않는 녹화·편집·자동 점수 코드까지 먼저 받고 있어서, 덩어리로 나눠 늦게 부르게 바꿨습니다. 그 변경의 머리 주석에 이렇게 적었습니다.
// 정적 import 로 268,729 바이트를 받았다(Fast 3G 첫 그리기 4.7초). …
숫자를 정확히 적는 건 좋은 습관입니다. 문제는 이 저장소에 탁구 놀이터의 정적 파일에 특정 용품 상표와 내부 이름이 섞이지 않게
막는 시험이 있다는 것이었습니다. 상표 목록에는 탁구 러버로 유명한 숫자 이름 하나가 들어 있습니다. 네, 729 입니다.
BRANDS = re.compile(r"\b(butterfly|tenergy|…|sanwei|729)\b", re.I)
게이트 로그의 실패 문구는 정직했습니다.
AssertionError: <re.Match object; span=(1518, 1521), match='729'> is not None : …/backend/static/tt/play/cam.js
2. 왜 「268,729」 가 「729」 인가: 단어 경계
\b 는 글자가 아니라 자리에 맞습니다. 한쪽이 단어 문자(\w — 글자, 숫자, 밑줄)이고 다른 쪽이 단어 문자가 아닌 자리입니다.
,는 단어 문자가 아닙니다. 그래서,와7사이는 경계입니다.9뒤의 공백도 단어 문자가 아닙니다. 그래서9와 공백 사이도 경계입니다.- 결과: 사람 눈에는 「26만 8천 7백 29」 인 숫자가, 정규식 눈에는 「268」 과 「729」 두 낱말입니다.
천 단위 쉼표뿐 아니라 소수점(v1.729)이나 하이픈(729-2)도 마찬가지입니다. 숫자 상표를 \b 로 감싸면 숫자가 들어간 글은 언제든
걸릴 수 있습니다.
3. 왜 8시간 반이나 걸렸나: 범인을 못 가린 묶음 게이트
게이트(전체 시험)는 한 번에 약 20분 걸립니다. 그래서 작업마다 따로 돌리지 않고 여러 작업을 묶어 한 번에 시험하고, 통과하면 같이 올립니다. 떨어지면 「범인 찾기」로 실패의 원인이 된 작업만 빼고 다시 돌립니다.
범인 찾기는 이런 단서를 썼습니다.
- 떨어진 시험의 이름(
test_tt_play)과 같은 이름의 파일(tests/test_tt_play.py,backend/app/tt_play.py)을 건드린 작업 - 실패 문구에 따옴표로 나온
.py파일을 건드린 작업
이번에는 둘 다 빗나갔습니다. 떨어진 시험 파일은 아무도 고치지 않았고, 실패 문구에 나온 파일은 .py 가 아니라 cam.js 였습니다.
실패 문구에 cam.js 의 전체 경로가 버젓이 있었는데도 범인 찾기는 그것을 보지 않았습니다. 결과는 「게이트 실패, 범인을 가리지
못했습니다」.
그리고 바깥 루프는 몇 분 뒤 같은 묶음을 다시 돌렸습니다. 새로 들어온 작업도 같은 묶음에 올라탔습니다.
| 시각(10-15) | 묶음에 든 작업 | 결과 |
|---|---|---|
| 01:05 ~ 01:46 | 3 | 실패 3번 |
| 02:06 ~ 02:25 | 8 | 실패 2번 |
| 02:45 ~ 08:21 | 9 | 실패 18번 |
| 08:42 | 18 | 실패 |
| 09:03 ~ 09:23 | 19 | 실패 2번 |
| 09:43 | 19 | 통과 — 19개 함께 올라감 |
26번 모두 같은 시험, 같은 파일, 같은 match='729' 였습니다(남아 있는 게이트 로그 26개를 모두 확인). 주석을 「정적 import 로 약 262KiB 를 받았다」로 바꾼 작업 파일이 고쳐진 시각이
09:42, 다음 바퀴가 09:43 에 시작해 통과했습니다(268,729 ÷ 1,024 ≈ 262.4).
4. 무엇을 고쳐야 하나
정규식: 숫자 상표는 숫자에 붙은 자리를 뺀다
파이썬으로 두 정규식을 비교해 봤습니다.
old = re.compile(r"\b(729)\b")
new = re.compile(r"(?<![\d.,])729(?![\d.,])") # 앞뒤가 숫자·점·쉼표면 상표가 아니다
| 글 | \b729\b |
숫자 이웃 제외 |
|---|---|---|
268,729 바이트 |
걸림 | - |
약 262KiB |
- | - |
Friendship 729 러버 |
걸림 | 걸림 |
729-2 러버 |
걸림 | 걸림 |
v1.729 |
걸림 | - |
0x729 |
- | 걸림 |
새 식도 완벽하지 않습니다(0x729 를 잡습니다). 정규식 하나로 「상표로 쓴 729」 와 「숫자 속 729」 를 완벽히 가를 수는 없으니,
걸리는 쪽으로 두되 걸렸을 때 바로 알 수 있게 하는 것이 더 중요합니다. 이 변경은 아직 하지 않았습니다.
범인 찾기: 실패 문구 속 경로를 본다, 모르면 쪼갠다
- 실패 문구에 나온 모든 파일 경로(확장자 상관없이)를 작업이 건드린 파일과 견준다. 이번에는 이것 하나로 바로 찾았을 것입니다.
- 그래도 모르면 묶음을 반으로 나눠 떨어진 시험만 돌린다(이분 탐색). 시험 하나는 몇 초면 끝납니다.
루프: 같은 실패가 되풀이되면 멈추고 알린다
같은 시험이 같은 이유로 두 번 떨어졌다면 세 번째도 떨어집니다. 그때는 다시 돌리지 말고 멈춰서 사람에게 실패 문구를 보여 줘야 합니다. 26번의 재시도가 쓴 시간은 8시간 반이었고, 그동안 상관없는 작업 18개가 함께 묶여 기다렸습니다.
세 가지 모두 아직 하지 않았습니다. 이 글이 그 할 일 목록입니다.
5. 이 사고가 가르쳐 준 것
- 지키는 시험이 틀린 이유로 떨어질 수 있습니다. 상표 검사는 제 일을 했습니다. 다만 「729」 라는 상표와 「268,729」 라는 숫자를 구분할 방법을 몰랐을 뿐입니다.
- 「모름」은 결과가 아니라 경보여야 합니다. 범인을 못 가렸다는 말은 「다시 해 보자」가 아니라 「사람이 봐야 한다」입니다.
- 묶음은 함께 이기고 함께 집니다. 묶음 게이트는 시간을 아끼지만, 범인을 가리지 못하는 순간 묶음 전체가 인질이 됩니다.
티티 한마디. 729 는 좋은 러버지만 숫자 속에 숨어 있으면 곤란해요! 정규식의
\b는 쉼표를 벽으로 안 봐요. 그리고 똑같은 이유로 두 번 넘어졌다면, 세 번째로 달리기 전에 신발 끈부터 보세요.
참고
모든 자료는 2026-10-16 에 열람했습니다.
- 파이썬
re문서(\b, 앞뒤 보기(?<!…)·(?!…)): https://docs.python.org/3/library/re.html