LabHub
시작하기
배우기 러닝패스 코스

좋은 서비스를 만드는 CS — 교과서 개념을 측정으로 다시 배운다

문자열 길이는 셋이고, 오늘은 어디의 오늘인가

LabHub 에서 이어서 보기

한 줄 요약

"닉네임은 10자까지" 라는 한 줄에는 길이가 세 개 숨어 있습니다. 저장소가 세는 UTF-8 바이트, 파이썬 len() 이 세는 코드 포인트, 사람이 세는 글자입니다. 여기에 "같은 이름" 과 "오늘" 까지 사람의 눈과 기계의 셈이 갈리는 자리를 숫자로 맞춰 봅니다.

왜 이게 필요했나

가입 화면은 "6글자까지" 라고 말하고, 서버는 len(name) <= 6 으로 검사하고, 저장소 칸은 바이트로 잽니다. 영문만 쓰는 시험에서는 셋이 같아서 아무 문제가 없습니다. 그러다 가족 이모지 하나가 들어오면 화면에는 한 글자인데 len() 은 5, 바이트는 18 이 됩니다. 알림 미리보기를 바이트로 자르면 끝에 물음표 마름모가 찍히고, 맥에서 붙여 넣은 한글 이름은 같은 이름인데 중복 검사를 통과합니다.

날짜도 같은 모양입니다. 이벤트는 UTC 로 찍히는데 사용자는 자기 시간대의 "오늘" 을 삽니다. UTC 날짜로 일별 통계를 묶으면 서울 사용자의 아침 아홉 시 전 활동이 전날로 넘어갑니다.

어떻게 동작하나

세 가지 길이. 이 파드의 파이썬 3.12 로 잰 예시입니다.

문자열 코드 포인트 UTF-8 바이트 사람이 보는 글자
a 1 1 1
ß 1 2 1
(완성형) 1 3 1
(자모 ㄱ+ㅏ, NFD) 2 6 1
é(e + 결합 악센트) 2 3 1
👍🏽(피부색 수식) 2 8 1
가족 이모지(사람 셋을 ZWJ 로 이음) 5 18 1

파이썬 문서는 str 을 "코드 포인트의 불변 시퀀스" 라고 정의하므로 len() 은 코드 포인트 수입니다. 바이트는 인코딩이 정합니다. RFC 3629 의 UTF-8 은 한 글자를 1~4 옥텟으로 쓰고, 이어지는 옥텟은 모두 10xxxxxx 꼴이며, 첫 옥텟이 전체 길이를 알려 줍니다. 그래서 RFC 가 말하듯 옥텟 흐름 어디서든 글자 경계를 쉽게 찾을 수 있습니다. 바이트 한도로 자를 때 10xxxxxx 가 아닌 자리까지 뒤로 물러서기만 하면 깨진 글자가 남지 않습니다.

사람이 보는 글자는 UAX #29 의 grapheme cluster 입니다. 결합 표시와 ZWJ 앞에서 끊지 않는 규칙(GB9), 한글 자모 L·V·T 를 한 음절로 묶는 규칙(GB6~GB8), 이모지 ZWJ 시퀀스 규칙(GB11), 국기 쌍 규칙(GB12·GB13) 등으로 됩니다. 표준 라이브러리 unicodedata 에는 이 경계를 나누는 함수가 없습니다. 그래서 실습에서는 단순화한 규칙을 직접 씁니다. 결합 표시·변형 선택자·피부색 수식자·ZWJ 는 앞에 붙이고, ZWJ 다음 글자도 붙이고, 자모는 L·V·T 규칙으로 묶습니다. 국기, Prepend, SpacingMark 는 다루지 않고 GB11 의 '앞이 그림 문자여야 한다' 조건도 뺐습니다. 실서비스라면 UAX #29 를 구현한 라이브러리를 쓰고, 그 라이브러리가 따르는 유니코드 버전까지 확인해야 합니다.

같은 이름. UAX #15 는 두 가지 같음을 나눕니다. 정준 동등(canonical)은 "같은 추상 글자이고 올바르게 그리면 언제나 같게 보이는" 관계로, 완성형 와 자모 ㄱ+ㅏ 가 그렇습니다. NFC·NFD 가 이것을 맞춥니다. 호환 동등(compatibility)은 더 약해서 모양이 달라도 됩니다. 전각 K, 합자 fi 이고, NFKC·NFKD 가 맞춥니다. 같은 문서는 KC·KD 가 서식 구분을 지우므로 "임의의 텍스트에 맹목적으로 적용하면 안 된다" 고 경고합니다. 그래서 원문은 그대로 두고 비교용 키에만 씁니다.

대소문자는 str.casefold 문서의 예가 전부를 말합니다. ß 는 이미 소문자라 lower() 는 그대로 두지만 casefold()ss 로 바꿉니다. StraßeSTRASSE 는 lower 로는 갈리고 casefold 로는 만납니다.

오늘은 어디의 오늘인가. datetime 문서에는 두 문장이 있습니다. aware datetime 에 timedelta 를 더해도 "시간대 조정은 하지 않는다". 같은 tzinfo 를 가진 aware 끼리 빼면 tzinfo 를 무시하고 벽시계 차이를 돌려준다. zoneinfo 문서의 예가 그 결과입니다. 로스앤젤레스 12:00 에 timedelta(days=1) 을 더하면 서머타임이 끝난 다음 날에도 12:00 입니다. 반대로 UTC 로 바꾼 순간에 24시간을 더하면 전환일에 현지 시각이 한 시간 밀립니다. "매일 09:00" 은 현지 날짜마다 09:00 을 만들어 UTC 로 바꾸는 것이고, 두 예약 사이의 실제 간격은 UTC 로 바꾼 뒤에 빼야 23시간·25시간이 드러납니다. zoneinfo 는 시스템의 IANA 데이터베이스나 tzdata 패키지를 읽으므로, 컨테이너 이미지에 그 데이터가 있는지도 확인할 거리입니다.

현장에서 만나는 모습

인코딩 판정·모지바케·보이지 않는 글자 걷어내기는 '고객 데이터 다루기' 코스의 fde-dat-encoding 실습이 깊게 다룹니다. 서머타임 전환일에 없는 시각과 두 번 오는 시각을 fold 로 가르는 일은 '로그가 미래에서 왔다' 코스가 맡습니다. 잘린 문자열을 errors="ignore" 로 조용히 삼키고 0 으로 끝나는 배치가 왜 위험한지는 '실패했는데 종료 코드는 0 이었다' 코스가 도구의 규율로 다룹니다. 이 모듈은 그 사이의 셈, 곧 길이·자르기·같음·날짜를 서비스 규칙으로 못박는 자리입니다.

다음 실습에서 할 것

닉네임 360개의 바이트·코드 포인트·글자 수를 세고, 단순화한 글자 규칙을 구현한 뒤, 바이트 한도와 글자 한도로 깨지지 않게 자릅니다. NFC·casefold·NFKC 를 차례로 얹으며 충돌하는 이름을 세고, UTC 이벤트를 현지 날짜로 묶고, 서머타임을 건너는 "매일 09:00" 예약을 UTC 로 바꾼 뒤, 전부를 가입 심사 함수 하나로 모읍니다.