이름은 코드 어디에나 있다. 변수, 함수, 인수, 클래스, 패키지, 파일 — 하루에도 수십 번 이름을 짓는다. 그래서 이름을 잘 짓는 규칙 몇 가지만 몸에 배면 코드 전체의 가독성이 함께 올라간다. 2장은 그 규칙들의 모음이다.
이름은 작은 설계다
코드에서 이름은 단순한 표시가 아니다. 변수, 함수, 클래스가 무엇을 알고 무엇을 하는지 가장 먼저 설명하는 장치다. 좋은 이름은 주석을 줄이고, 나쁜 이름은 짧은 코드도 어렵게 만든다. 그래서 이름 짓기는 사소한 취향 문제가 아니라 읽기 쉬운 코드를 만드는 핵심 작업이다.
의도를 드러내는 이름은 “무엇을 하는가”뿐 아니라 “왜 필요한가”까지 어느 정도 알려준다. 이름 하나가 바뀌면 같은 코드가 얼마나 달리 읽히는지 보자.
// before — d 는 아무것도 말하지 않는다
int d; // 경과 시간(단위: 날)
// after — 이름이 단위와 의미를 품는다
int elapsedTimeInDays;
int daysSinceCreation;
int fileAgeInDays;d에 붙은 주석은 코드가 바뀌면 어긋나지만, elapsedTimeInDays는 코드의 일부라 계속 검증된다. 이름을 길게 쓰는 것이 항상 좋은 것은 아니지만, 짧다는 이유만으로 의미를 생략하면 읽는 사람이 계속 문맥을 추적해야 한다.
판단 기준: 이름에 설명 주석을 달고 싶어지는 순간이 곧 이름을 고칠 신호다. 주석이 말하려던 것을 이름 안으로 옮긴다.
그릇된 단서를 피하기
의미 있는 이름을 붙이려면 먼저 잘못된 단서를 피해야 한다. 실제로 List가 아닌데 accountList라고 부르거나, 서로 거의 구분되지 않는 data1, data2 같은 이름을 쓰면 코드는 읽는 사람에게 틀린 방향을 가리킨다. 이름은 구현의 세부 구조와 역할을 가능한 정확히 반영해야 한다.
// before — 자료구조가 List가 아닌데 List라 부르고,
// 두 이름은 눈으로 구별이 거의 안 된다
Map<String, Account> accountList;
int XYZControllerForEfficientHandlingOfStrings;
int XYZControllerForEfficientStorageOfStrings;
// after — 컨테이너 종류를 이름에 넣지 않고 집합을 뜻하는 이름으로,
// 서로 뚜렷이 구별되는 이름으로
Map<String, Account> accounts;
int stringHandlingController;
int stringStorageController;비슷한 개념은 일관된 단어로 표현하고, 다른 개념은 분명히 다른 단어로 구분하는 편이 좋다. fetch, retrieve, get을 섞어 쓰면서 차이가 없다면 읽는 사람은 차이를 찾으려 애쓴다. 반대로 실제 의미가 다르다면 그 차이가 이름에서도 드러나야 한다. 이것이 “일관성”과 “그릇된 단서 금지”의 양면이다.
함정: l(소문자 엘)과 1, O(대문자 오)와 0처럼 눈으로 헷갈리는 문자를 변수명에 쓰면 그 자체가 그릇된 단서다.
말하고 찾을 수 있는 이름
좋은 이름은 회의 중에 말하기 쉽고, 코드베이스에서 검색하기도 쉽다. genymdhms(generation date, year, month, day, hour, minute, second) 같은 축약어는 발음할 수 없어 팀 안에서 공유하기 힘들다. 이를 generationTimestamp로 바꾸면 “제너레이션 타임스탬프”라고 소리 내어 논의할 수 있다.
검색 가능성도 중요하다. 상수 7이나 한 글자 변수 e는 코드베이스에서 찾을 수 없다. 이름이 길어지는 대신 grep 한 번으로 모든 사용처를 찾을 수 있게 된다.
// before — 7과 s는 검색으로 찾을 수 없다
for (int j = 0; j < 34; j++)
s += (t[j] * 4) / 5;
// after — 이름 있는 상수는 검색되고 의미도 남는다
int realDaysPerIdealDay = 4;
final int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for (int j = 0; j < NUMBER_OF_TASKS; j++) {
int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
int realTaskWeeks = realTaskDays / WORK_DAYS_PER_WEEK;
sum += realTaskWeeks;
}판단 기준: 이름의 길이는 그 이름의 유효 범위(scope)에 비례해야 한다. 한두 줄짜리 반복문 카운터 i는 괜찮지만, 널리 참조되는 변수일수록 검색 가능하고 명확한 긴 이름이 값을 한다.
인코딩과 헝가리안 표기법은 이제 짐이다
옛날 방식처럼 타입이나 범위를 이름에 억지로 끼워 넣는 방식은 대부분의 경우 도움이 되지 않는다. 언어와 도구(IDE, 타입 시스템)가 이미 그 정보를 제공하기 때문이다.
// before — 헝가리안 표기, 멤버 접두사, 인터페이스 접두사
String phoneString; // 타입이 바뀌면 이름이 거짓말이 된다
int m_dsc; // 멤버라는 뜻의 m_ 접두사
IShapeFactory factory; // 인터페이스라는 뜻의 I
// after — 타입은 컴파일러가, 멤버는 IDE가 알려준다
String phone;
int description;
ShapeFactory factory;이름은 부가 정보를 숨기는 암호가 아니라 의도를 드러내는 문장에 가까워야 한다. phoneString은 나중에 타입을 PhoneNumber로 바꾸는 순간 이름이 거짓말이 된다.
클래스는 명사, 메서드는 동사
이름 규칙은 문법적 품사로 정리하면 외우기 쉽다. 클래스와 객체는 명사구, 메서드는 동사구가 기본이다. 아래 지도가 규칙의 뼈대다.
flowchart TD Root["이름을 짓는다"] --> Kind{"무엇의 이름인가?"} Kind -->|클래스·객체| C["명사구<br/>Customer, WikiPage, Account<br/>(Manager·Processor·Data 같은<br/>공허한 단어 지양)"] Kind -->|메서드| M["동사구<br/>postPayment, deletePage, save"] M --> Acc["접근자·변경자·조건은<br/>get/set/is 접두사<br/>getName, setName, isPosted"] Kind -->|같은 개념| One["개념 하나엔 단어 하나<br/>fetch/retrieve/get 혼용 금지"] Kind -->|생성자 대안| F["정적 팩터리 메서드<br/>Complex.fromRealNumber(23.0)"]
한 개념에 한 단어를 고수하되, 서로 다른 개념에 같은 단어를 쓰는 “말장난”은 피한다. 예를 들어 여러 클래스에서 값을 더하거나 이어 붙이는 메서드를 모두 add라 부르면, 어떤 add는 덧셈이고 어떤 add는 리스트 삽입이라 혼란스럽다. 후자는 insert나 append가 맞다.
맥락을 더하되, 넘치지 않게
state라는 변수 하나만 보면 그것이 주소의 “주(state)“인지 상태 기계의 “상태(state)“인지 알 수 없다. addrState처럼 접두사를 붙이거나, 더 낫게는 Address 클래스로 묶어 맥락을 준다. 반대로 이미 맥락이 분명한데 접두사를 덧대면 잡음이다. Address 클래스 안의 필드를 addrFirstName이라 부를 필요는 없다.
함정: 불필요한 맥락은 자동완성을 방해한다. 모든 클래스에 프로젝트 약자(GSD)를 접두사로 붙이면 GSD를 친 순간 무관한 후보가 쏟아진다. 맥락은 이름이 홀로 모호할 때만 더한다.
정리
- 이름은 작은 설계다. 이름에 주석을 달고 싶어지면, 그 주석을 이름 안으로 옮긴다.
- 그릇된 단서(List 아닌
~List, 구별 안 되는 유사 이름, 헷갈리는 문자)를 먼저 제거한다. - 이름 길이는 유효 범위에 비례한다. 널리 쓰이는 이름일수록 발음·검색 가능해야 한다.
- 헝가리안 표기·멤버 접두사·인터페이스 접두사는 현대 도구 아래서 짐이다. 타입은 컴파일러가 안다.
- 클래스는 명사, 메서드는 동사, 한 개념엔 한 단어. 맥락은 모호할 때만 더하고 넘치면 잡음이 된다.
다음장으로 3장