- 들어가며 — 왜 이름이 그렇게 어려운가
- 이름은 구현이 아니라 의도를 드러내야 한다
- 이름의 길이는 스코프에 비례한다
- 인코딩과 헝가리안 표기법을 피하라
- 검색 가능한 이름을 쓰라
- 불리언, 함수, 컬렉션의 관례
- 이름 바꾸기는 하나의 리팩터링이다
- 흔한 함정들
- 마치며
- 참고 자료
들어가며 — 왜 이름이 그렇게 어려운가
필 칼튼(Phil Karlton)의 유명한 농담이 있습니다. "컴퓨터 과학에는 어려운 것이 딱 두 가지 있다. 캐시 무효화, 그리고 이름 짓기." 처음 들으면 그냥 재치 있는 말장난 같습니다. 이름 짓기가 캐시 무효화만큼 어렵다고? 변수 하나에 단어 하나 붙이는 일인데?
그런데 몇 년 코드를 쓰다 보면 이 농담이 뼈아프게 다가옵니다. 이름 짓기가 어려운 이유는 타이핑이 어려워서가 아니라, 이름을 짓는 순간 우리가 그 개념을 정말로 이해했는지가 드러나기 때문입니다. 어떤 함수에 좋은 이름이 떠오르지 않는다면, 그건 대개 그 함수가 한 가지 일을 하지 않거나, 우리가 그 함수의 역할을 아직 명확히 정의하지 못했다는 신호입니다. 이름 짓기는 사고의 도구이지 장식이 아닙니다.
이 글은 좋은 이름을 짓는 몇 가지 원칙을 실무 관점에서 정리합니다. 규칙을 외우자는 것이 아니라, 각 원칙이 왜 존재하는지를 이해하면 낯선 상황에서도 스스로 판단할 수 있게 됩니다.
이름은 구현이 아니라 의도를 드러내야 한다
가장 중요한 원칙 하나만 남긴다면 이것입니다. 좋은 이름은 "무엇을 하는지"가 아니라 "왜 존재하는지"를 말합니다. 읽는 사람이 구현을 들여다보지 않고도 그 값이나 함수의 의미를 알 수 있어야 합니다.
나쁜 예를 봅시다.
# d 는 무엇일까? 구현을 봐야 안다
d = 30
# 리스트를 순회하며 어떤 조건을 만족하는 것만 골라 새 리스트에 담는다
list1 = []
for x in list2:
if x[3] == 1:
list1.append(x)
d, list1, list2, x[3] == 1 중 어느 것도 스스로 말하지 않습니다. 이 코드를 이해하려면 주변 맥락을 전부 읽어야 합니다. 이제 의도를 드러내는 이름으로 바꿔 봅시다.
GRACE_PERIOD_DAYS = 30
active_users = []
for user in all_users:
if user.is_active:
active_users.append(user)
바뀐 것은 로직이 아니라 이름뿐입니다. 그런데 코드가 스스로 설명합니다. 30이라는 숫자가 무엇을 의미하는지, 우리가 왜 이 사용자들을 고르는지가 이름에 담겨 있습니다. 주석으로 "이건 유예 기간 30일이야"라고 설명할 필요가 없어졌습니다. 좋은 이름은 주석을 대체합니다.
이름의 길이는 스코프에 비례한다
초보 시절 흔한 두 가지 극단이 있습니다. 하나는 모든 것을 a, b, tmp로 줄이는 것이고, 다른 하나는 반대로 theListOfAllActiveUsersInTheCurrentOrganization 같은 문장급 이름을 붙이는 것입니다. 둘 다 좋지 않습니다.
좋은 규칙은 이름의 길이가 그 이름의 스코프(생존 범위)에 비례해야 한다는 것입니다.
# 짧은 스코프: 한 줄짜리 컴프리헨션 안에서만 사는 변수는 짧아도 된다
squares = [n * n for n in numbers]
# 넓은 스코프: 클래스 전체에서 쓰이는 필드나 모듈 전역은 충분히 설명적으로
class PaymentProcessor:
def __init__(self):
self.pending_settlement_count = 0
한 줄 안에서만 존재하는 반복 변수 n은 짧아도 문제없습니다. 눈이 그 짧은 범위를 한 번에 담을 수 있기 때문입니다. 반대로 클래스 전체에서 살아가는 필드가 n이면 재앙입니다. 이 값이 어디서 어떻게 쓰이는지 추적하려면 이름이 스스로 맥락을 지고 있어야 합니다.
정리하면 이렇습니다. 루프 인덱스나 짧은 람다의 인자는 짧게, 함수 파라미터는 적당히, 넓은 범위에서 오래 사는 이름일수록 설명적으로. 스코프가 넓을수록 이름이 감당해야 할 설명의 무게가 커집니다.
인코딩과 헝가리안 표기법을 피하라
한때 유행했던 헝가리안 표기법은 변수 이름 앞에 타입 정보를 접두어로 붙이는 방식이었습니다. strName(문자열 name), iCount(정수 count), arrUsers(배열 users)처럼요. 지금은 대부분 안티패턴으로 봅니다.
# 헝가리안 표기법 - 피하자
strUserName = "alice"
iRetryCount = 3
arrActiveUsers = [...]
# 타입은 언어와 IDE가 알려준다. 이름은 의미에 집중하자
user_name = "alice"
retry_count = 3
active_users = [...]
이유는 두 가지입니다. 첫째, 현대 언어는 타입 시스템과 IDE가 타입을 알려줍니다. 이름에 타입을 중복해 넣는 것은 잉여입니다. 둘째, 그리고 더 위험한 것은, 타입이 바뀌면 이름이 거짓말을 하게 된다는 점입니다. arrUsers를 나중에 집합(set)이나 딕셔너리로 바꾸면 접두어 arr이 틀린 정보가 됩니다. 이름은 언제나 진실이어야 하는데, 구현 세부를 이름에 인코딩하면 그 진실이 쉽게 깨집니다.
같은 이유로 멤버 변수에 m_을 붙이거나 전역에 g_를 붙이는 관행도 요즘은 잘 쓰지 않습니다. 정말 구분이 필요하다면 self.count처럼 언어가 제공하는 문법으로 충분합니다.
검색 가능한 이름을 쓰라
코드는 쓰는 시간보다 읽고 찾는 시간이 훨씬 깁니다. 그래서 **이름이 검색 가능한가(grep-able)**는 생각보다 중요한 실무 기준입니다.
숫자 리터럴 7을 코드 곳곳에 흩뿌려 두었다고 합시다. 나중에 이 7이 "일주일"을 의미한다는 걸 알고 값을 바꾸려 할 때, 프로젝트 전체에서 7을 검색하면 온갖 무관한 7이 쏟아집니다. 하지만 이름을 붙여 두면 딱 그 개념만 찾을 수 있습니다.
# 나쁨: 이 7이 무엇을 뜻하는지, 어디에 또 있는지 찾기 어렵다
if days_since_login > 7:
send_reminder()
# 좋음: 이름으로 검색하면 이 개념이 쓰인 곳만 정확히 나온다
INACTIVE_THRESHOLD_DAYS = 7
if days_since_login > INACTIVE_THRESHOLD_DAYS:
send_reminder()
같은 맥락에서, 한 글자 변수나 너무 흔한 단어는 검색을 방해합니다. data, value, item 같은 이름이 코드베이스에 수백 개 있으면, 특정한 그 값을 찾기가 어렵습니다. 이름은 고유하고 구체적일수록 나중의 나를 돕습니다.
불리언, 함수, 컬렉션의 관례
몇 가지 자리 잡은 관례는 익혀 두면 팀 전체의 코드를 읽기 쉽게 만듭니다.
불리언은 예/아니오로 답할 수 있는 이름으로. is, has, can, should 같은 접두어를 붙이면 조건문에서 자연스럽게 읽힙니다.
# 이름만으로 참/거짓 질문이 된다
is_active = True
has_permission = user.role == "admin"
can_retry = attempt < max_attempts
if is_active and has_permission:
...
active나 permission처럼 명사형으로 두면 if active:가 "활성?"인지 "활성으로 만들라"인지 모호합니다. 접두어 하나가 그 모호함을 없앱니다.
함수 이름은 동사로 시작한다. 함수는 행동이기 때문입니다. getUser, calculateTotal, sendEmail처럼요. 반면 값을 담는 변수는 명사입니다. 그리고 어떤 값을 돌려주는지도 이름에서 드러나면 좋습니다. getUser()는 사용자를, isValid()는 불리언을 돌려줄 것이라고 읽는 사람이 자연스럽게 기대합니다.
컬렉션은 복수형으로. 여러 개를 담는 리스트나 집합은 복수형 이름을 씁니다.
# 단수는 하나, 복수는 여럿 - 순회할 때 자연스럽다
users = fetch_all_users()
for user in users:
notify(user)
user는 한 명, users는 여럿. 이 단순한 규칙 덕분에 for user in users:가 문장처럼 읽힙니다. 반대로 for u in userList: 같은 코드는 매번 잠깐 멈춰 해석해야 합니다.
이름 바꾸기는 하나의 리팩터링이다
여기서 많은 사람이 걸립니다. "지금 이름이 별로인 건 아는데, 바꾸면 여기저기 다 고쳐야 해서..." 그래서 나쁜 이름이 그대로 화석처럼 남습니다.
하지만 이름 바꾸기(rename)는 현대 도구에서 가장 안전한 리팩터링 중 하나입니다. IDE의 "심볼 이름 바꾸기" 기능은 단순 문자열 치환이 아니라, 언어를 이해한 채로 그 심볼이 참조되는 곳만 정확히 바꿉니다. 같은 이름의 무관한 변수는 건드리지 않습니다.
원칙은 이렇습니다. 더 나은 이름이 떠올랐다면, 그 순간이 바꿀 때입니다. 이름을 고치는 커밋은 로직 변경과 섞지 말고 따로 두는 것이 좋습니다. 그래야 리뷰어가 "이건 순수한 이름 변경이고 동작은 그대로구나"를 한눈에 확인할 수 있습니다.
좋은 습관:
커밋 1: rename calc() -> calculate_monthly_total() (동작 변화 없음)
커밋 2: 실제 로직 수정 (동작 변화 있음)
나쁜 습관:
커밋 1: 이름도 바꾸고 로직도 바꾸고 필드도 옮기고... (리뷰 불가능)
이름을 고치는 것을 두려워하지 마세요. 코드는 이해가 깊어질수록 좋은 이름을 얻어 가는, 살아 있는 문서입니다. 처음부터 완벽한 이름을 지을 필요는 없습니다. 이해가 자라면 이름도 함께 자라게 하면 됩니다.
흔한 함정들
마지막으로 실무에서 자주 마주치는 이름 관련 함정을 짚습니다.
- 거짓말하는 이름: 함수 이름은
getUser인데 내부에서 사용자를 만들거나 삭제한다면, 이름이 거짓말을 하는 것입니다. 조회 함수는 부수 효과가 없을 것이라 기대되므로 이런 배신은 특히 위험합니다. - 일관성 없는 어휘: 같은 개념을 어떤 곳은
fetch, 어떤 곳은get, 어떤 곳은retrieve로 부르면 읽는 사람이 "이 셋이 다른 건가?" 하고 헷갈립니다. 한 코드베이스 안에서는 한 개념에 한 단어를 정해 밀고 나가세요. - 의미 없는 불용어:
UserData,UserInfo,UserObject에서Data,Info,Object는 아무것도 더하지 않습니다. 그냥User면 충분할 때가 많습니다. - 발음할 수 없는 이름:
genymdhms(generation year month day hour minute second) 같은 이름은 동료와 대화할 때 소리 내 부를 수 없습니다. 코드는 팀이 함께 이야기하는 대상이므로 발음 가능성도 중요합니다.
마치며
이름 짓기가 "가장 어려운 쉬운 일"인 이유는, 문법적으로는 아무 제약이 없지만 그 안에 우리의 이해가 고스란히 드러나기 때문입니다. 좋은 이름은 구현이 아니라 의도를 말하고, 스코프에 비례한 길이를 가지며, 타입을 인코딩하지 않고, 검색 가능하며, 팀의 관례를 따릅니다.
무엇보다 이름은 고정된 것이 아닙니다. 더 나은 이름이 떠오르면 리네임은 안전한 리팩터링으로 언제든 할 수 있습니다. 완벽한 이름을 처음부터 짜내려 애쓰기보다, "지금 내 이해로는 이게 최선"인 이름을 짓고, 이해가 깊어질 때마다 다듬어 가는 편이 훨씬 건강합니다. 결국 잘 지은 이름들이 모이면 코드는 별도의 설명서 없이도 읽히는 글이 됩니다.
참고 자료
- Robert C. Martin, "Clean Code" (2장: Meaningful Names)
- Phil Karlton의 원 인용 배경: https://www.karlton.org/2017/12/naming-things-hard/
- Tim Ottinger, "Ottinger's Rules for Variable and Class Naming"
- Kernighan & Pike, "The Practice of Programming" (명확성과 이름)
현재 단락 (1/76)
필 칼튼(Phil Karlton)의 유명한 농담이 있습니다. "컴퓨터 과학에는 어려운 것이 딱 두 가지 있다. 캐시 무효화, 그리고 이름 짓기." 처음 들으면 그냥 재치 있는 말장...