사전 대소문자 접기
표준

대소문자 접기

gabury1고친 사람 github-actions[bot]

대소문자 접기는 대문자와 소문자의 차이를 지워서 두 문자열을 같다고 볼 수 있게 해 줍니다. Hello 와 HELLO 를 같은 낱말로 다루고 싶을 때 씁니다. 두 문자열을 모두 한 가지 꼴로 바꾼 다음 비교합니다. 어느 글자를 어떤 꼴로 바꿀지는 유니코드가 글자마다 정해 둡니다.

쉽고 빠른 이해

대소문자 접기는 대소문자만 다른 글자들을 한 꼴로 모읍니다. A 와 a 는 둘 다 a 가 됩니다. 독일어 ß 와 SS 는 둘 다 ss 가 됩니다.

이게 없으면 Straße 로 가입한 사람을 STRASSE 로 찾지 못합니다. 소문자로 바꾸기만으로는 이런 글자가 안 모입니다.

어떻게 도나:

  1. 비교할 두 문자열을 받습니다
  2. 글자마다 정해 둔 표를 보고 접은 꼴로 바꿉니다
  3. 바꾼 결과끼리 비교합니다

대가도 있습니다. 접으면 문자열 길이가 바뀔 수 있습니다. 원래 어느 글자가 대문자였는지도 알 수 없게 됩니다. 그래서 원문은 따로 두고 접은 값은 비교에만 씁니다.

상세

이 절은 대소문자 접기가 무엇을 같다고 보고 어떻게 한 꼴로 모으는지를 ß · ς · İ 세 글자로 따라갑니다.

대소문자를 무시한 비교

로그인 아이디나 검색어처럼 사람이 손으로 치는 값은 대소문자가 섞여 들어옵니다. Kim 으로 가입한 사람이 kim 으로 로그인하면 같은 사람으로 봐야 할 때가 많습니다. 이렇게 대소문자 차이를 무시하고 두 문자열을 비교하는 것을 대소문자 무시 비교라고 부릅니다.

컴퓨터는 글자를 번호로 들고 있습니다. 유니코드는 이 번호를 코드 포인트라고 부릅니다. A 는 U+0041, a 는 U+0061 이라서 번호만 비교하면 둘은 다른 글자입니다.

비교하기 전에 두 문자열을 한 가지 꼴로 바꿔 둡니다. 대문자와 소문자로 흩어진 글자를 한 꼴로 겹쳐 모은다는 뜻에서 이 일을 접기(folding)라고 부릅니다. 대소문자를 모으는 접기가 대소문자 접기입니다.

ASCII 범위의 접기

ASCII(American Standard Code for Information Interchange, 미국 정보 교환 표준 부호)는 영어 알파벳과 숫자, 기호 128개에 번호를 매긴 문자 집합입니다. 이 범위 안에서는 접기가 간단합니다. A 부터 Z 까지를 a 부터 z 로 바꾸면 끝납니다.

ASCII 에서 대문자와 소문자는 번호가 32씩 차이 납니다. 글자 하나의 번호만 보고 바로 바꿀 수 있습니다. 글자 수도 바뀌지 않습니다.

소문자로 바꾸기와 다른 점

ASCII 밖으로 나가면 소문자로 바꾸기와 접기가 갈립니다. 소문자로 바꾸기는 사람에게 보여 줄 소문자를 만드는 일입니다. 접기는 비교에 쓸 키를 만드는 일입니다.

독일어 ß 가 둘이 갈리는 대표 예입니다. ß 는 소문자입니다. 대문자로 쓸 때는 흔히 SS 두 글자로 씁니다. 따라서 straße 와 STRASSE 는 같은 낱말입니다.

아래 코드는 파이썬 문자열 메서드로 둘을 비교합니다. lower() 는 소문자로 바꾸고, casefold() 는 접습니다.

Python
"straße".lower()     # 'straße'
"STRASSE".lower()    # 'strasse'
"straße".casefold()  # 'strasse'

lower() 는 ß 가 이미 소문자라서 건드리지 않습니다. 그래서 두 낱말이 straße 와 strasse 로 갈립니다. casefold() 는 ß 를 ss 로 펴서 두 낱말을 같은 꼴로 모읍니다.

유니코드가 정한 접기 표

어느 글자를 어떤 꼴로 접을지는 규칙 하나로 끌어낼 수 없습니다. 유니코드는 글자마다 접은 결과를 표로 정해 둡니다. 표에 없는 글자는 접어도 안 바뀝니다. 한글이나 한자처럼 대소문자가 없는 글자가 여기 듭니다.

아래 표는 접기 표에서 뽑은 몇 줄입니다.

글자 접은 꼴 어떤 글자인가
A (U+0041) a 영어 대문자
ß (U+00DF) ss 대문자로 쓰면 SS 가 되는 독일어 소문자
Σ (U+03A3) · ς (U+03C2) σ (U+03C3) 그리스어 시그마. ς 는 낱말 끝에만 쓰는 소문자
K (U+212A) k 켈빈 온도 기호. 라틴 대문자 K 와 번호만 다르다
fi (U+FB01) fi 두 글자를 한 덩이로 붙여 그린 합자

그리스어 시그마는 소문자가 둘입니다. 소문자로 바꾸면 Σ 가 낱말 속 위치에 따라 σ 나 ς 로 갈립니다. 접기는 위치를 보지 않고 셋을 모두 σ 로 모읍니다.

Python
"ς".casefold()       # 'σ'
"K".casefold()  # 'k'

첫 줄은 낱말 끝 시그마가 보통 시그마로 접히는 모습입니다. 둘째 줄의 "K" 는 켈빈 기호입니다. 접고 나면 라틴 소문자 k 와 같은 문자열이 됩니다.

단순 접기와 완전 접기

위 표에서 ß 와 fi 는 한 글자가 두 글자로 늘어납니다. 글자 수가 바뀌는 접기를 허용하느냐에 따라 방식이 둘로 갈립니다.

방식 한 글자가 ß 는 쓰는 곳
완전 접기(full case folding) 여러 글자가 될 수 있다 ss 문자열 전체를 비교할 때
단순 접기(simple case folding) 언제나 한 글자다 ß 로 남는다 글자 수가 바뀌면 안 될 때

완전 접기는 straße 와 STRASSE 를 같다고 판정합니다. 대신 접은 문자열의 길이가 원문과 달라집니다. 그래서 접은 문자열에서 찾은 위치로 원문의 위치를 바로 짚을 수 없습니다.

단순 접기는 글자를 하나씩 짝지어 바꾸므로 길이가 안 바뀝니다. 편집기에서 찾은 낱말을 원문 위에 칠하는 기능처럼, 접은 문자열의 위치를 원문 위치로 옮겨야 하는 처리에 맞습니다. 그 대신 ß 와 ss 는 다른 문자열로 남습니다.

터키어의 i

터키어와 아제르바이잔어에는 i 가 넷입니다. 점 있는 i 와 점 없는 ı 가 각각 대문자와 소문자를 갖습니다. 이 언어에서 I 의 소문자는 ı 입니다. i 의 대문자는 İ 입니다.

이 소절에서 기본 접기는 터키어 규칙을 쓰지 않은 완전 접기를 말합니다. 기본 접기는 언어를 가리지 않고 영어처럼 I 를 i 로 접습니다. 터키어 낱말을 비교할 때는 이 결과가 틀린 답이 됩니다.

그래서 유니코드는 터키어용 규칙을 따로 둡니다. 그 규칙을 쓸지 말지는 쓰는 쪽이 고릅니다.

아래 표에는 윗점 부호가 나옵니다. İ 의 윗점을 따로 떼어 적는 부호로, 앞 글자 위에 얹혀 한 글자로 그려지는 결합 문자입니다.

글자 터키어에서의 짝 기본 접기 터키어 규칙 접기
I (U+0049) 소문자가 ı i ı
ı (U+0131) 대문자가 I ı ı
İ (U+0130) 소문자가 i i + 윗점 부호 i
i (U+0069) 대문자가 İ i i

표의 셋째 줄이 눈여겨볼 대목입니다. 기본 접기는 İ 를 i 와 윗점 부호 두 글자로 바꿉니다.

Python
"İ".casefold()       # 'i̇'
len("İ".casefold())  # 2

접은 결과가 두 글자라서 i 한 글자와 같지 않습니다. 터키어 규칙 접기를 쓰면 İ 가 i 한 글자로 접힙니다.

대문자로 바꾸는 쪽에서도 사고가 납니다. 이번에는 반대로, 영어 낱말에 터키어 규칙이 걸리는 사고입니다. 실행 환경의 언어 설정을 로캘이라고 부릅니다.

대소문자를 바꾸는 함수 가운데 로캘을 따르는 것이 있습니다. 로캘이 터키어이면 title 을 대문자로 바꾼 값이 TİTLE 이 됩니다. 이 값을 TITLE 과 비교하던 코드는 한 글자 때문에 어긋납니다.

기본 접기는 로캘을 타지 않습니다. 같은 입력이면 어느 서버에서 돌려도 같은 결과가 나옵니다.

정규화와 함께 쓰는 순서

유니코드 정규화는 같은 글자를 적는 여러 번호 열을 한 꼴로 맞추는 일입니다. é 는 전용 번호 하나로도, e 뒤에 강세 부호를 붙인 두 번호로도 적힙니다. 접기는 이 차이를 모으지 않고 대소문자만 봅니다.

정규화에는 형식이 여럿 있습니다. 그 가운데 합쳐진 글자를 가장 작은 조각으로 쪼개 두는 형식이 NFD(Normalization Form D, 정규화 형식 D — D 는 분해 decomposition)입니다. é 한 글자는 NFD 에서 e 와 강세 부호 두 번호가 됩니다.

대소문자와 적는 방법이 둘 다 다를 수 있는 입력은 두 일을 함께 합니다. 순서는 셋입니다.

  1. NFD 로 쪼갭니다
  2. 접습니다
  3. 다시 NFD 로 맞춥니다

맨 앞 NFD 는 부호의 순서를 맞춥니다. 위와 아래처럼 서로 다른 곳에 붙는 부호 둘은 적는 순서가 달라도 같은 글자로 칩니다. NFD 는 이런 부호들을 정해진 순서로 줄 세웁니다.

접기는 드물게 부호 하나를 보통 글자로 바꿉니다. 그리스어 글자 밑에 작게 붙이는 이오타 부호(U+0345)가 ι 로 접히는 경우입니다. 보통 글자가 되고 나면 NFD 가 그 앞뒤 부호를 다시 줄 세우지 못합니다. 그래서 접기 전에 먼저 줄 세워 둡니다.

마지막 NFD 는 접은 결과를 다시 맞춥니다. 접기가 글자를 바꾼 뒤에는 결과가 NFD 꼴이라는 보장이 없기 때문입니다.

아래 그림은 대문자 É 한 글자와 소문자 e 에 강세 부호를 붙인 두 글자가 같은 키로 모이는 과정입니다.

flowchart TD
    A["É · 한 글자"] --> A2["NFD · E + 강세 부호"]
    A2 --> A3["접기 · e + 강세 부호"]
    B["é · e + 강세 부호"] --> B2["NFD · e + 강세 부호"]
    B2 --> B3["접기 · e + 강세 부호"]
    A3 --> K["다시 NFD · 같은 키"]
    B3 --> K

왼쪽 입력은 NFD 에서 E 와 강세 부호로 쪼개집니다. 접기에서 E 가 e 로 바뀝니다. 오른쪽 입력은 처음부터 쪼개진 꼴입니다. 이미 소문자라서 두 단계를 지나도 모양이 같습니다.

두 입력이 끝에서 같은 번호 열이 되므로 비교가 참으로 나옵니다. 부호가 하나뿐인 É 는 맨 앞 NFD 가 없어도 끝의 NFD 가 쪼개 줍니다. 맨 앞 단계는 위에서 본 부호 순서 때문에 둡니다.

아래 코드의 key() 는 그림의 세 단계를 파이썬으로 옮긴 함수입니다. 마지막 줄의 "é" 은 e 뒤에 강세 부호를 붙인 두 번호입니다.

Python
from unicodedata import normalize as nf

def key(s):
    return nf("NFD", nf("NFD", s).casefold())

key("É") == key("é")  # True

정규화를 빼고 casefold() 만 걸면 이 비교는 거짓이 됩니다. 왼쪽은 é 한 글자로, 오른쪽은 두 번호로 남기 때문입니다.

백엔드에서 접기가 필요한 곳

접기를 거는 곳을 세 장면으로 봅니다. 셋 모두 원문은 남기고, 접은 값은 비교에만 씁니다.

첫째는 대소문자를 무시하는 아이디입니다. Kim 과 kim 을 한 사람으로 보려면 저장할 때 접은 값을 따로 둡니다. 접은 값에 유니크 제약을 걸면 대소문자만 다른 아이디가 따로 가입되지 않습니다. 원문은 화면에 보여 주려고 남깁니다.

둘째는 검색입니다. 전문 검색은 색인을 만들 때와 검색어를 받을 때 같은 방식으로 접어야 합니다. 색인은 소문자로만 바꿔 straße 를 그대로 두고, 검색어는 완전 접기로 접었다고 해 봅니다. 그러면 strasse 로 친 검색어가 색인의 straße 에 안 걸립니다.

셋째는 ASCII 글자만 접으면 되는 곳입니다. DNS(Domain Name System)의 도메인 이름과 HTTP(HyperText Transfer Protocol) 헤더 이름은 대소문자를 가리지 않고 비교합니다. 두 곳 모두 ASCII 글자만 쓰므로 A 부터 Z 까지만 접으면 됩니다.

접기가 모으지 않는 차이

접기는 대소문자만 모읍니다. 겉보기에 비슷해도 대소문자 차이가 아니면 손대지 않습니다. 아래 넷이 그런 경우입니다.

  • 강세 부호는 남습니다. é 와 e 는 접어도 다른 글자입니다. 부호를 무시하고 비교하려면 강세 무시 비교를 따로 합니다
  • 글자 폭은 남습니다. 한 칸 폭을 차지하게 만든 전각 문자 A(U+FF21)는 접으면 전각 소문자 a(U+FF41)가 됩니다. 보통 a 와 같아지지 않습니다
  • 모양이 닮은 다른 문자는 남습니다. 키릴 문자 а 와 라틴 문자 a 는 접어도 다른 글자입니다. 이런 글자로 남의 이름을 흉내 내는 공격을 동형 문자 공격이라고 부릅니다
  • 정렬 순서는 정하지 않습니다. 어느 문자열이 앞에 오는지는 콜레이션이 정합니다

관련 항목

대소문자 접기를 이루는 방식과 규칙

단순 접기 · 완전 접기 · 터키어 i 문제 · 정준 대소문자 무시 일치

대소문자 접기가 다루는 글자의 구성 요소

코드 포인트 · 결합 문자 · 합자 · 전각 문자 · 대문자 · 소문자

대소문자 접기가 속하는 상위 표준과 자료

Unicode · 유니코드 문자 데이터베이스 · ASCII · 문자 인코딩

대소문자 접기와 함께 도는 텍스트 처리 단계

유니코드 정규화 · NFD · NFKC · 콜레이션 · 문자열 비교 · 대소문자 변환 · 로캘

대소문자 접기 결과로 값을 비교하는 백엔드 기능

대소문자 무시 비교 · 유니크 제약 · 전문 검색 · 인덱스 · DNS · HTTP

대소문자 접기로 모이지 않는 비교 문제

동형 문자 공격 · 강세 무시 비교 · 자소 분리 · 문자소 클러스터

다른 이름: case folding · Case Folding · 케이스 폴딩