이름 짓기는 왜 그렇게 어려울까
목차9

변수 이름 하나를 두고 회의실에서 30분을 보내본 적이 있다면, 이번 글이 꽤 반가울 것 같다. userList 로 할지 users 로 할지, fetch 인지 get 인지 load 인지를 두고 오가는 대화는 개발자라면 누구나 한 번쯤 겪어봤을 장면이다. (그리고 그 회의는 보통 "일단 이걸로 하고 나중에 바꾸자"로 끝나고, 나중은 오지 않는다)
컴퓨터 과학에서 어려운 일은 딱 두 가지뿐인데, 바로 캐시 무효화와 이름 짓기라는 말이 있다. 넷스케이프에서 일하던 필 칼튼(Phil Karlton)이 남긴 것으로 알려진 이 농담은 마틴 파울러의 블로그에도 소개될 만큼 유명하다. 많은 개발자가 이 말에 웃는 이유는 아마 웃기기 때문이라기보다 아프기 때문일 것이다.
흔히 이름 짓기가 어려운 이유를 영어 실력에서 찾는다. 틀린 말은 아니다. 하지만 영어를 모국어로 쓰는 개발자들도 똑같이 이름 때문에 괴로워한다는 걸 생각해보면, 어휘력만으로는 설명이 안 되는 무언가가 분명히 있다.
그래서 이번 포스팅에서는 이름 짓기가 왜 이렇게 어려운지를 정보이론의 눈으로 한번 뜯어보려고 한다. 이름이 정확히 무슨 일을 하는 녀석인지, 이름의 길이는 무엇으로 정해야 하는지, 그리고 이름이 도저히 떠오르지 않을 때 그게 무슨 신호인지를 차례대로 이야기해보자.
이름은 압축이다
먼저 이름이 하는 일을 정확히 짚고 넘어가자. 함수 하나가 50줄짜리라고 해보면, 그 함수를 호출하는 쪽에서 우리가 보는 건 50줄의 로직이 아니라 calculateShippingFee 라는 스무 글자 남짓의 이름뿐이다. 즉, 이름은 50줄짜리 의미를 스무 글자로 줄여서 전달하는 압축 파일인 셈이다.
압축이라는 단어가 나온 김에, 압축과 전달에 대해 가장 엄밀하게 이야기한 사람의 이야기를 해보려고 한다. 1948년 클로드 섀넌(Claude Shannon)은 통신의 수학적 이론이라는 논문에서 정보의 양을 재는 방법을 제안했다. 섀넌이 주목한 건 메시지의 내용이 아니라 받는 사람이 얼마나 놀라는가, 다시 말해 받는 사람의 불확실성이 얼마나 줄어드는가였다.
섀넌은 이 불확실성을 엔트로피라는 값으로 정의했다.
기호가 좀 많아 보이지만 하나씩 풀어보면 생각보다 단순하다. 여기서 는 "이 이름이 실제로 가리키는 것"이고, 는 이름을 읽은 사람이 그것을 번째 후보로 해석할 확률이다. 는 그 확률을 비트 단위로 바꿔주는 장치이고, 앞에 붙은 마이너스는 결과를 양수로 만들기 위한 장치일 뿐이다.
이걸 실제 이름에 대입해보자. 코드 리뷰에서 data 라는 변수를 만났다고 해보면, 이 친구가 가리킬 수 있는 건 정말 많다. 응답 본문일 수도 있고, 사용자 목록일 수도 있고, 설정값이나 캐시, 폼 입력일 수도 있다.
후보가 일곱 개이고 각각의 가능성이 비슷하다면, 는 모두 이 된다. 이걸 공식에 넣으면 엔트로피는 , 약 2.81비트가 나온다. 반면 unpaidInvoices 는 후보가 사실상 하나뿐이니 이고 엔트로피는 0이다. 같은 계산을 코드로 옮기면 이렇게 된다.
// 각 해석 후보의 확률을 받아 엔트로피를 비트로 계산
function entropy(probabilities: number[]) {
return -probabilities
.filter((p) => p > 0)
.reduce((sum, p) => sum + p * Math.log2(p), 0);
}
entropy(Array(7).fill(1 / 7)); // 2.807...
entropy([1]); // 0결국 하는 일은 확률마다 로그를 곱해서 더하는 것이 전부다. 수식은 겁나 보였지만, 꼴랑 reduce 한 줄로 끝나는 계산이었던 셈이다.
이 숫자가 의미하는 바는 꽤 직관적이다. 엔트로피가 2.81비트라는 건, 읽는 사람이 이 변수의 정체를 확정하려면 "예, 아니오" 질문을 평균 세 번쯤 던져야 한다는 뜻에 가깝다. 그리고 코드를 읽는 사람이 던지는 질문은 대부분 머릿속에서 일어나기 때문에, 그 비용은 리뷰 시간과 버그로 조용히 청구된다.
압축을 풀려면 같은 사전이 필요하다
압축이라는 비유를 조금만 더 밀고 나가보자. zip 파일을 받은 사람이 그 파일을 열 수 있는 건, 보낸 사람과 받는 사람이 같은 압축 알고리즘을 알고 있기 때문이다. 알고리즘을 모르는 사람에게 zip 파일은 그냥 의미 없는 바이트 덩어리일 뿐이다.
이름도 똑같다. 친구끼리 메신저에서 쓰는 "ㄱㅅ"이 "감사"로 읽히는 건, 두 사람이 같은 줄임말 사전을 공유하고 있기 때문이다. 같은 "ㄱㅅ"을 처음 보는 어르신께 보내면 아마 고소인지 감사인지 한참 고민하실 것이다.
개발자에게 익숙한 예로 바꾸면 req, res, ctx, err 같은 줄임말이 그렇다. Express 핸들러 안에서 req 를 보고 헷갈리는 사람은 거의 없다. 업계 전체가 "핸들러의 첫 번째 인자는 요청 객체"라는 사전을 공유하고 있기 때문이다. 반면 우리 팀만 쓰는 pcd 나 mrg 같은 줄임말은 사전이 팀 안에만 있어서, 새로 합류한 사람에게는 압축이 풀리지 않는 파일이 된다.
즉, 이름의 엔트로피는 이름 혼자 정하는 값이 아니다. 이름과 읽는 사람이 가진 사전의 조합으로 정해지는 값이다. 이 사실은 뒤에서 이름을 누구를 위해 짓는지 이야기할 때 다시 중요해진다.
짧은 이름과 긴 이름 사이
여기까지만 보면 "그럼 무조건 길고 구체적으로 쓰면 되는 거 아닌가"라는 생각이 들 수 있다. 그런데 우리가 실제로 좋아하는 코드를 떠올려보면 꼭 그렇지는 않다. 반복문의 i, 데이터베이스 레코드의 id 를 loopIndexCounter 나 uniqueRecordIdentifier 로 바꾸자고 하면 아마 대부분은 고개를 저을 것이다.
이 직관에는 생각보다 오래된 근거가 있다. 언어학자 조지 킹슬리 지프(George Kingsley Zipf)는 1935년에 자주 쓰는 단어일수록 짧아지는 경향이 있다는 관찰을 남겼다. 흔히 지프의 축약 법칙이라고 부르는 이 경향은 이후 수많은 자연어에서 확인됐고, 프로그래밍 언어의 식별자에서도 비슷한 경향이 관찰됐다.
이유는 비용 계산으로 보면 간단하다. 이름 하나가 독자에게 청구하는 비용은 대략 "한 번 읽을 때의 비용 × 읽는 횟수"에 가깝다. 수백 번 등장하는 이름이 한 글자 길어지면 그 비용은 수백 번 청구되지만, 딱 한 번 등장하는 이름은 길어져도 한 번만 청구된다. 그래서 자주 쓰는 이름은 짧게 깎이고, 드물게 쓰는 이름은 길어도 버틸 수 있는 것이다.
가상의 모듈 하나를 놓고 직접 계산해보자. 아래 숫자는 설명을 위해 지어낸 값이지만, 비율만큼은 실제 코드베이스와 크게 다르지 않을 것이다.
| 이름 | 등장 횟수 | 길이 | 읽어야 하는 글자 수 |
|---|---|---|---|
| i | 300 | 1 | 300 |
| loopIndexCounter | 300 | 16 | 4,800 |
| retryWithExponentialBackoff | 2 | 27 | 54 |
| rwb | 2 | 3 | 6 |
i 를 loopIndexCounter 로 바꾸면 독자가 읽어야 하는 글자는 4,500자나 늘어난다. 반면 retryWithExponentialBackoff 를 rwb 로 줄여서 아끼는 글자는 꼴랑 48자다. 48자를 아끼려고 모든 독자에게 "rwb 가 뭐지?"라는 질문을 던지게 만드는 건 아무래도 남는 장사가 아니다.
하지만 i 가 괜찮은 진짜 이유는 따로 있다. 정보이론에는 조건부 엔트로피라는 개념이 있는데, 어떤 단서 를 알고 있을 때 남는 불확실성을 뜻한다.
이 부등식은 단서가 생기면 불확실성은 평균적으로 줄거나 그대로일 뿐, 늘어나지는 않는다는 뜻이다. 세 줄짜리 for 문 안의 i 는 혼자 놓고 보면 엔트로피가 높은 이름이지만, "세 줄짜리 반복문 안"이라는 단서 가 붙는 순간 가리킬 수 있는 후보가 거의 하나로 줄어든다. 즉, 짧은 이름이 허용되는 조건은 짧은 이름 자체가 아니라 그 이름을 감싸는 문맥이 충분히 좁다는 데 있다.
// 문맥이 좁으면 짧은 이름도 후보가 하나로 줄어듦
for (let i = 0; i < rows.length; i++) {
total += rows[i].amount;
}
// 문맥이 넓으면 같은 이름도 다시 모호해짐
const d = await load(); // 이 d 는 200줄 아래에서 또 쓰인다여기서 이름의 길이를 정하는 기준이 하나 나온다. 이름의 길이는 취향이 아니라 스코프의 넓이와 등장 빈도로 정하는 값에 가깝다. 스코프가 좁고 자주 쓰이면 짧게, 스코프가 넓고 드물게 쓰이면 길게 가는 것이다.
이름을 안다는 것과 이해한다는 것
엔트로피와 축약 법칙으로 이름의 길이는 어느 정도 설명할 수 있다. 하지만 이걸로도 설명이 안 되는 상황이 있다. 문맥도 좁고 등장 빈도도 적당한데, 그냥 이름이 떠오르지 않는 경우다. 아마 이 경우가 우리가 회의실에서 30분을 쓰는 진짜 이유일 것이다.
물리학자 리처드 파인만(Richard Feynman)은 회고록 『남이야 뭐라 하건!』(What Do You Care What Other People Think?, 1988)에서 어린 시절 아버지에게 들은 이야기를 소개한다. 새의 이름을 세상의 모든 언어로 안다고 해도, 정작 그 새에 대해서는 아무것도 모를 수 있다는 이야기다. 이름을 아는 것과 그 대상을 이해하는 것은 전혀 다른 일이라는 뜻이다.
코드에서는 이 관계가 거꾸로 작동한다. 대상을 이해하지 못하면 이름을 지을 수 없다. 그리고 이해가 안 되는 가장 흔한 이유는 우리의 머리가 나빠서가 아니라 그 코드가 한 가지 일만 하고 있지 않아서다.
// 이름이 안 떠오르는 함수, 실제로는 세 가지 일을 함
function handleData(input: FormInput) {
const errors = validate(input);
if (errors.length) showToast(errors[0]);
return api.post("/orders", normalize(input));
}이 함수에 정직한 이름을 붙이면 validateShowErrorAndSubmitOrder 같은 괴물이 나온다. 그렇다면 이 괴물 같은 이름이 우리에게 말해주는 건 무엇일까?
…
이름이 길어진 건 우리가 말이 많아서가 아니라 함수가 하는 일이 많아서다.
이 경우 고쳐야 하는 건 이름이 아니라 함수의 경계다. 이름에 And 가 들어가거나, 동사가 두 개 이상 필요하다면 그 함수는 이미 두 가지 이상의 일을 하고 있을 가능성이 크다.
// 경계를 나누면 이름이 저절로 짧아짐
function validateOrder(input: FormInput): string[] { /* ... */ }
function submitOrder(input: FormInput) { /* ... */ }그래서 필자는 이름이 30초 안에 떠오르지 않으면, 이름 후보를 더 찾기 전에 그 코드가 몇 가지 일을 하고 있는지부터 세어보는 편이다. 이름 짓기의 어려움은 꽤 자주 설계가 보내는 경고음이기 때문이다. (물론 경고음을 무시하고 handleData2 를 만드는 것도 인간의 자유다)
이름은 누구를 위한 것인가
앞에서 엔트로피를 계산할 때 슬쩍 넘어간 부분이 하나 있다. 는 "읽는 사람이" 그 이름을 번째로 해석할 확률이라고 했다. 즉, 같은 이름이라도 읽는 사람이 누구냐에 따라 엔트로피가 달라진다.
예를 들어 결제 도메인에서 오래 일한 사람에게 settlement 는 "정산"이라는 단 하나의 의미를 가진 단어다. 하지만 이제 막 합류한 사람에게는 합의인지, 정착인지, 정산인지 알 수 없는 단어일 수 있다. 같은 이름의 엔트로피가 독자의 배경지식에 따라 0에서 몇 비트까지 오가는 셈이다.
에릭 에반스(Eric Evans)는 2003년 『도메인 주도 설계』에서 유비쿼터스 언어라는 개념을 제안했다. 개발자와 도메인 전문가가 같은 단어를 같은 뜻으로 쓰도록 언어 자체를 함께 다듬자는 이야기다. 정보이론의 말로 바꾸면, 팀 전체의 분포를 하나로 맞춰서 모두에게 엔트로피가 낮은 단어 집합을 만들자는 제안에 가깝다.
이렇게 보면 이름 짓기가 어려운 또 하나의 이유가 드러난다. 이름은 혼자 짓지만 읽는 사람은 여럿이고, 그 사람들이 무엇을 알고 있는지 우리는 정확히 모른다. 이름 짓기는 결국 보이지 않는 독자의 머릿속을 추측하는 일인 것이다.
이름도 늙는다
지금까지는 이름을 처음 지을 때의 이야기를 했다. 그런데 필자는 이름 짓기가 진짜로 어려워지는 순간이 이름을 지은 다음에 온다고 생각한다. 코드는 계속 바뀌는데, 이름은 처음 지은 그대로 남아 있기 때문이다.
회사에서 흔히 볼 수 있는 장면을 하나 떠올려보자. 5년 전에 "신규사업팀"이라는 이름으로 만들어진 팀이 지금은 기존 서비스 운영을 맡고 있는 경우다. 팀원들은 매일 기존 서비스를 고치는데, 조직도에는 여전히 신규사업팀이라고 적혀 있다. 처음 온 사람은 그 이름만 보고 이 팀이 새로운 일을 하는 팀이라고 오해하게 된다.
코드에서도 똑같은 일이 일어난다. 처음에는 정말로 사용자 정보만 가져오던 getUser 가, 몇 번의 요구사항을 거치면서 캐시를 갱신하고, 로그를 남기고, 가끔은 사용자를 생성하기까지 하는 함수로 자라난다. 이름은 여전히 get 인데 하는 일은 이미 getOrCreateAndTrack 에 가까워진 것이다.
// 처음에는 이름 그대로였던 함수
async function getUser(id: string) {
return db.users.find(id);
}
// 2년 뒤, 이름은 그대로인데 하는 일은 넷으로 늘어남
async function getUser(id: string) {
const cached = cache.get(id);
if (cached) return cached;
const user = (await db.users.find(id)) ?? (await db.users.create({ id }));
analytics.track("user_loaded", { id });
cache.set(id, user);
return user;
}엔트로피의 언어로 말하면, 이 함수의 이름은 시간이 지나면서 거짓말을 하게 된다. 처음에는 엔트로피가 0에 가까웠던 이름이, 코드가 바뀔수록 읽는 사람의 예상과 실제 동작 사이의 간극을 키운다. 정확히 말하자면 불확실성이 늘어나는 게 아니라 틀린 확신이 생기는 것이라서, 모호한 이름보다 더 위험하다. data 는 적어도 읽는 사람을 조심하게 만들지만, 거짓말하는 getUser 는 읽는 사람을 안심시키기 때문이다. (그리고 그 안심은 보통 장애 회고 문서에서 끝난다)
그래서 필자는 이름을 바꾸는 일을 리팩터링의 사치가 아니라 유지보수의 기본으로 보는 편이다. 함수의 동작을 바꾸는 PR 이라면, 이름이 여전히 진실을 말하고 있는지도 함께 리뷰하는 것이 좋다. 요즘 에디터는 이름 바꾸기를 안전하게 처리해주기 때문에, 이름을 고치는 비용은 이름이 거짓말을 하게 두는 비용보다 거의 언제나 싸다.
그래서 만능일까
여기까지 읽고 나면 이런 반론이 떠오를 수 있다.
결국 엔트로피를 낮추려면 이름을 최대한 구체적으로 쓰라는 이야기 아닌가?
맞다. 사실 대부분의 경우에는 구체적인 이름이 모호한 이름보다 낫다. 하지만 엔트로피를 무조건 0으로 만드는 게 목표는 아니다. 일부러 모호해야 하는 이름도 있기 때문이다.
제네릭 타입의 T, 유틸리티 함수의 value, 추상 인터페이스의 Repository 같은 이름은 일부러 여러 후보를 품도록 지은 이름이다. 이런 이름에서 엔트로피가 높은 건 버그가 아니라 기능이다. 추상화의 목적 자체가 여러 구체적인 경우를 하나의 이름으로 묶는 것이니까.
반대로 길고 구체적인 이름이 오히려 엔트로피를 높이는 경우도 있다. 관례가 이미 사전 역할을 하고 있는 곳에서 관례를 깨는 경우다. React 에서는 훅의 이름을 반드시 use 로 시작하도록 훅의 규칙으로 정해두었다. 덕분에 useCart 라는 이름만 보고도 우리는 이 친구가 훅이고, 컴포넌트 최상단에서만 불러야 한다는 사실까지 한 번에 알아챈다. 같은 함수에 getCartStateWithSubscription 처럼 더 길고 친절한 이름을 붙이면, 글자는 늘었는데 전달되는 정보는 오히려 줄어드는 슬픈 상황이 된다.
또 하나 짚고 넘어갈 점은, 지금까지의 계산은 모두 비유라는 것이다. 실제 독자의 머릿속에서 후보마다 정확한 확률이 매겨지지는 않는다. 엔트로피는 이름의 품질을 재는 측정기가 아니라, 이름이 왜 어렵게 느껴지는지를 설명해주는 렌즈에 가깝다.
이름을 고르는 습관
그렇다면 실제로 이름을 지을 때는 무엇을 하면 좋을까. 지금까지의 이야기를 개발자의 일상 행동으로 내려보면 몇 가지 습관으로 정리된다.
먼저 후보를 하나만 떠올리고 멈추지 말고, 최소 세 개는 적어보자. 후보를 나란히 놓으면 각 이름이 어떤 오해를 부르는지가 보인다. 그 오해의 개수가 곧 엔트로피다.
다음으로 이름의 길이를 스코프에 맞추자. 세 줄짜리 콜백 안이라면 x 도 충분하고, 모듈 바깥으로 export 되는 함수라면 조금 길어도 괜찮다. 길이에 대한 논쟁은 대부분 스코프에 대한 논쟁으로 바꿔서 이야기하면 금방 끝난다.
마지막으로 이름이 30초 안에 떠오르지 않으면, 이름 대신 경계를 의심해보자. 함수가 두 가지 일을 하고 있거나, 아직 도메인을 충분히 이해하지 못했을 가능성이 크다. 그럴 때 필요한 건 사전이 아니라 도메인 전문가와의 10분짜리 대화일 때가 많다.
마치며
처음에 던졌던 장면으로 돌아가 보자. 회의실에서 userList 와 users 를 두고 30분을 쓴 그 시간은 사실 영어 단어를 고르는 시간이 아니었다. 이 코드를 읽을 사람이 누구이고, 그 사람이 무엇을 이미 알고 있으며, 이 변수가 어디까지 살아남을지를 합의하는 시간이었던 것이다.
그래서 필자는 "좋은 이름은 무엇일까"라는 질문이 사실 "읽는 사람은 무엇을 이미 알고 있을까"라는 질문에 더 가깝다고 생각한다. 이름은 코드 안에 있지만, 이름의 의미는 읽는 사람의 머릿속에서 완성되기 때문이다.
다음에 이름 때문에 회의가 길어진다면, 단어를 바꾸기 전에 서로가 상상하고 있는 독자가 같은 사람인지부터 확인해보자. 생각보다 많은 이름 논쟁이 거기서 풀린다.
이상으로 이름 짓기는 왜 그렇게 어려울까 포스팅을 마친다.