---
title: "파일명 중간의 점이 확장자로 잡힐 때, splitext 가 만든 거짓 깨진 링크 2,049건"
description: "위키링크 전수 스캔에서 3,136회 중 2,049회가 깨졌다고 나왔지만, 대부분은 os.path.splitext 가 번호 접두어의 점을 확장자로 오인해 생긴 거짓 양성이었습니다. 알려진 확장자만 벗기자 141건으로 줄었습니다."
date: 2026-10-31
updated: 2026-10-30T23:13:17.259Z
tags: [python, filesystem, debugging, tooling]
canonical: https://blog.wooncloud.com/posts/splitext-dots-false-broken-links
---

마크다운 문서 저장소의 위키링크(`[[문서명]]`)를 전수 스캔해서 깨진 링크를 찾는 스크립트를 새로 짰습니다. 결과는 링크 3,136회 중 **2,049회가 깨짐**, 전체의 65% 였습니다.

![splitext 가 번호 접두어의 점에서 파일명을 잘라 깨진 링크로 오판했고, 알려진 확장자만 벗기자 숫자가 대부분 사라졌다.](/images/posts/splitext-dots-false-broken-links/3a449562-9767-4db9-a652-ccb966be66b5.webp)

너무 많죠? 표본을 하나 열어 보니 멀쩡했습니다.

```text
[39] 11-8. 권한 판정 설계(코드)
      ← 11-1. 개요 인덱스.md
```

가리키는 `11-8. 권한 판정 설계(코드).md` 는 실제로 있었고, 링크에 오타도 없었습니다.

## 멀쩡한 링크가 왜 깨졌다고 나올까?

범인은 링크 대상을 파일 인덱스와 맞추기 전에 확장자를 벗기던 이 한 줄이었습니다.

```python
stem = os.path.splitext(name)[0]
```

`splitext` 는 마지막 점을 기준으로 자릅니다. 실제 파일명에는 잘 동작합니다.

```python
os.path.splitext("11-8. 권한 판정 설계(코드).md")
# → ('11-8. 권한 판정 설계(코드)', '.md')   OK
```

문제는 확장자 없이 적힌 링크 대상입니다.

```python
os.path.splitext("11-8. 권한 판정 설계(코드)")
# → ('11-8', '. 권한 판정 설계(코드)')       NG
```

`11-8` 만 남으니 인덱스에 있는 전체 이름과 맞을 리가 없습니다. 이 저장소는 `01. 개요.md`, `11-8. 설계.md` 처럼 번호 접두어를 점으로 끊는 규칙을 쓰고 있었고, 그 점이 전부 확장자로 오인됐습니다. 규칙을 열심히 지킨 문서일수록 더 많이 깨진 것처럼 보이는 셈입니다.

![확장자 없는 링크 대상에 splitext 를 쓰면 번호 접두어의 점에서 잘려 11-8 만 남아 깨진 링크로 집계되고, 알려진 확장자일 때만 벗기는 stem_of 는 전체 이름을 유지해 인덱스와 일치한다.](/images/posts/splitext-dots-false-broken-links/e5bd4920-0657-4dcf-be7e-b24ae6a5c63d.svg)

## 왜 눈치채기 어려웠을까요?

스크립트가 에러를 내지 않았습니다. 문자열이 잘렸을 뿐이라 정상 종료합니다.

결과도 그럴듯했습니다. 정리 안 된 저장소에 깨진 링크가 많다는 건 충분히 있을 법한 결론이라, 숫자만 봐서는 의심이 안 듭니다. 게다가 141건은 진짜 깨진 링크였습니다. 진짜와 거짓이 섞여 있으면 표본 하나만 확인하고 넘어가기 쉽습니다.

그리고 파일명에 점을 안 쓰는 저장소에서는 영원히 재현되지 않습니다.

## 알려진 확장자만 벗깁니다

마지막 점을 무조건 벗기지 말고, 허용 목록에 있는 확장자일 때만 벗깁니다.

```python
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 name
```

```python
stem_of("11-8. 권한 판정 설계(코드)")      # 그대로 유지
stem_of("11-8. 권한 판정 설계(코드).md")   # '.md' 만 제거
```

이 한 군데를 고치자 2,049건이 **141건**으로 줄었습니다.

## 줄어든 숫자는 믿어도 될까요?

숫자가 크게 움직였으니 새 결과도 그대로 믿으면 안 됩니다. 그래서 이전에 다른 방식으로 짠 스크립트가 남긴 리포트와 하위 분류 단위로 대조했습니다.

"확장자 누락" 분류가 이전에는 11종 16회였고, 새 스크립트도 11종 16회였습니다. 서로 다른 시점에 다른 방식으로 짠 두 스캐너가 같은 값을 낸 거죠.

전체 합계만 비교했다면 못 했을 검증입니다. 집계를 하위 분류까지 쪼개서 내보내 두면 교차 검증이 가능해집니다.

## splitext 가 틀리는 이름들

`splitext` 는 "마지막 점 뒤가 확장자"라는 가정을 코드에 심습니다. 이 가정이 깨지는 이름은 생각보다 흔합니다.

```python
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)` 도 같은 함정을 그대로 갖습니다.

측정 도구가 내놓은 큰 숫자는 문제의 크기가 아니라 도구의 버그 크기일 수 있다는 걸 다시 느꼈습니다. 놀라운 결과가 나오면 보고하기 전에 표본 하나를 손으로 따라가 봐야겠습니다.

## 참고

- https://docs.python.org/3/library/os.path.html#os.path.splitext
