파일명 중간의 점이 확장자로 잡힐 때, splitext 가 만든 거짓 깨진 링크 2,049건
위키링크 전수 스캔에서 3,136회 중 2,049회가 깨졌다고 나왔지만, 대부분은 os.path.splitext 가 번호 접두어의 점을 확장자로 오인해 생긴 거짓 양성이었습니다. 알려진 확장자만 벗기자 141건으로 줄었습니다.
마크다운 문서 저장소의 위키링크([[문서명]])를 전수 스캔해서 깨진 링크를 찾는 스크립트를 새로 짰습니다. 결과는 링크 3,136회 중 2,049회가 깨짐, 전체의 65% 였습니다.
너무 많죠? 표본을 하나 열어 보니 멀쩡했습니다.
[39] 11-8. 권한 판정 설계(코드)
← 11-1. 개요 인덱스.md가리키는 11-8. 권한 판정 설계(코드).md 는 실제로 있었고, 링크에 오타도 없었습니다.
멀쩡한 링크가 왜 깨졌다고 나올까?
범인은 링크 대상을 파일 인덱스와 맞추기 전에 확장자를 벗기던 이 한 줄이었습니다.
stem = os.path.splitext(name)[0]splitext 는 마지막 점을 기준으로 자릅니다. 실제 파일명에는 잘 동작합니다.
os.path.splitext("11-8. 권한 판정 설계(코드).md")
# → ('11-8. 권한 판정 설계(코드)', '.md') OK문제는 확장자 없이 적힌 링크 대상입니다.
os.path.splitext("11-8. 권한 판정 설계(코드)")
# → ('11-8', '. 권한 판정 설계(코드)') NG11-8 만 남으니 인덱스에 있는 전체 이름과 맞을 리가 없습니다. 이 저장소는 01. 개요.md, 11-8. 설계.md 처럼 번호 접두어를 점으로 끊는 규칙을 쓰고 있었고, 그 점이 전부 확장자로 오인됐습니다. 규칙을 열심히 지킨 문서일수록 더 많이 깨진 것처럼 보이는 셈입니다.
왜 눈치채기 어려웠을까요?
스크립트가 에러를 내지 않았습니다. 문자열이 잘렸을 뿐이라 정상 종료합니다.
결과도 그럴듯했습니다. 정리 안 된 저장소에 깨진 링크가 많다는 건 충분히 있을 법한 결론이라, 숫자만 봐서는 의심이 안 듭니다. 게다가 141건은 진짜 깨진 링크였습니다. 진짜와 거짓이 섞여 있으면 표본 하나만 확인하고 넘어가기 쉽습니다.
그리고 파일명에 점을 안 쓰는 저장소에서는 영원히 재현되지 않습니다.
알려진 확장자만 벗깁니다
마지막 점을 무조건 벗기지 말고, 허용 목록에 있는 확장자일 때만 벗깁니다.
EXTS = {'.md', '.html', '.pdf', '.png', '.jpg', '.svg', '.csv', '.json', '.txt'}
def stem_of(name: str) -> str:
i = name.rfind('.')
if i > 0 and name[i:].lower() in EXTS:
return name[:i]
return namestem_of("11-8. 권한 판정 설계(코드)") # 그대로 유지
stem_of("11-8. 권한 판정 설계(코드).md") # '.md' 만 제거이 한 군데를 고치자 2,049건이 141건으로 줄었습니다.
줄어든 숫자는 믿어도 될까요?
숫자가 크게 움직였으니 새 결과도 그대로 믿으면 안 됩니다. 그래서 이전에 다른 방식으로 짠 스크립트가 남긴 리포트와 하위 분류 단위로 대조했습니다.
"확장자 누락" 분류가 이전에는 11종 16회였고, 새 스크립트도 11종 16회였습니다. 서로 다른 시점에 다른 방식으로 짠 두 스캐너가 같은 값을 낸 거죠.
전체 합계만 비교했다면 못 했을 검증입니다. 집계를 하위 분류까지 쪼개서 내보내 두면 교차 검증이 가능해집니다.
splitext 가 틀리는 이름들
splitext 는 "마지막 점 뒤가 확장자"라는 가정을 코드에 심습니다. 이 가정이 깨지는 이름은 생각보다 흔합니다.
os.path.splitext("11-8. 설계") # ('11-8', '. 설계')
os.path.splitext("v1.2.3") # ('v1.2', '.3')
os.path.splitext("2026.09.02 회의록") # ('2026.09', '.02 회의록')
os.path.splitext("archive.tar.gz") # ('archive.tar', '.gz')확장자가 확실히 붙어 있는 실제 파일명에는 splitext 를 써도 됩니다. 하지만 링크 대상, 검색어, 설정값처럼 사람이 손으로 적은 문자열에는 쓰면 안 됩니다. 확장자가 있는지부터 불확실하니까요. rsplit('.', 1) 도 같은 함정을 그대로 갖습니다.
측정 도구가 내놓은 큰 숫자는 문제의 크기가 아니라 도구의 버그 크기일 수 있다는 걸 다시 느꼈습니다. 놀라운 결과가 나오면 보고하기 전에 표본 하나를 손으로 따라가 봐야겠습니다.