2장 의미 있는 이름

"첫 아이의 이름을 짓듯이"

2장에서는 이름을 잘 짓는 간단한 규칙을 소개한다.

소프트웨어에서 이름은 어디나 쓰인다. 우리는 변수에도 이름을 붙이고, 함수에도, 인수, 클래스, 패키지, 소스 파일, 소스 파일이 담긴 디렉터리, jar 파일, war 파일, ear 파일에도 이름을 붙인다. 여기저기 도처에서 이름을 사용한다. 이렇듯 많이 사용하므로 이름을 잘 지으면 여러모로 편하다. 좋은 이름을 지으려면 시간이 걸리지만 좋은 이름으로 절약하는 시간이 훨씬 더 많다. 그러므로 이름을 주의 깊게 살펴 더 나은 이름이 떠오르면 개선하기를 바란다. 그러면 코드를 읽는 사람이 좀 더 행복해지리라.

의도를 분명히 밝혀라

말하기는 쉽다. 하지만 의도가 분명한 이름은 정말로 중요하다. 변수나 함수 그리고 클래스 이름은 다음과 같은 굵직한 질문에 모두 답해야 한다.

  • 변수(혹은 함수나 클래스)의 존재 이유는?
  • 수행 기능은?
  • 사용 방법은?

위 질문에 답하기 위해 따로 주석이 필요하다면 의도를 분명히 드러내지 못했다는 뜻이다.

나쁜 예시:

int d; // 경과 시간(단위: 날짜)

이름 d는 아무런 정보도 제공하지 못한다. 경과 시간이나 날짜라는 느낌이 안 든다. 이런 이름은 나쁜 이름이다.

좋은 예시:

int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;

위와 같이 측정하려는 값과 단위를 이름에서 확인할 수 있다. 이름만으로도 주석이 필요 없어진다.

다음은 나쁜 함수의 예시다.

public List<int[]> getThem() {
  List<int[]> list1 = new ArrayList<int[]>();
  for (int[] x : theList)
    if (x[0] == 4)
      list1.add(x);
  return list1;
}

코드가 하는 일을 짐작하기 어렵다. 왜일까? 복잡한 문장은 없다. 문제는 코드의 단순성이 아니라 코드의 함축성이다. 다시 말해, 코드 맥락이 코드 자체에 명시적으로 드러나지 않는다. 위 코드는 암암리에 독자가 다음과 같은 정보를 안다고 가정한다.

  1. theList에 무엇이 들어있는가?
  2. theList의 0번째 값이 어째서 중요한가?
  3. 값 4는 무슨 의미인가?
  4. 반환된 리스트 list1을 어떻게 사용하는가?

위 코드 샘플엔 이와 같은 정보가 드러나지 않는다. 이번엔 좋은 예시를 보자.

public List<int[]> getFlaggedCells() {
  List<int[]> flaggedCells = new ArrayList<int[]>();
  for (int[] cell : gameBoard)
    if (cell.isFlagged())
      flaggedCells.add(cell);
  return flaggedCells;
}

이름만 봐도 코드가 하는 일을 짐작할 수 있다. gameBoard에서 flaggedcell을 찾아 반환한다. 이제 주석이 필요 없다.

위 코드는 지뢰 찾기 게임을 만드는 데 사용되는 코드다. gameBoard는 게임판을 나타내는 2차원 배열이다. cell.isFlagged()는 지뢰가 있다고 표시한 상태를 나타낸다. 이런 정보를 이름만으로 알 수 있다.

이처럼 이름만 고쳤는데도 함수가 하는 일을 이해하기 쉬워졌다. 바로 이것이 좋은 이름이 주는 위력이다.

그릇된 정보를 피하라

프로그래머는 코드에 그릇된 단서를 남겨서는 안 된다. 그릇된 단서는 코드를 오해하게 만든다.

  1. 나름대로 널리 쓰이는 의미가 있는 단어를 다른 의미로 사용하면 안 된다.

예를 들어, hp라는 이름을 사용했는데 이게 honor points의 약자라고 한다면, 이는 그릇된 정보다. hp는 보통 유닉스 OS 플랫폼의 Hewlett Packard Unix를 떠올릴 수 있기 때문에 명확하지 않은 hp라는 변수는 독자에게 그릇된 정보를 제공한다.

  1. 데이터 타입이 다른 타입 이름을 변수명에 사용하면 안 된다.

여러 계정을 그룹으로 묶을 때, 실제 List 데이터 타입을 사용한 게 아니라면, accountList라는 이름을 사용하면 안 된다. 계정을 담는 컨테이너가 실제 List가 아니라면 프로그래머에게 그릇된 정보를 제공할 수 있다. 따라서 accountGroup이나 bunchOfAccounts, 아니면 단순히 Accounts 등이 좋은 이름이다.

  1. 서로 흡사한 이름을 사용하지 않도록 주의한다. 하지만 유사한 개념은 유사한 표기법을 사용한다.

한 모듈에서 XYZControllerForEfficientHandlingOfStrings라는 이름을 사용하고, 조금 떨어진 모듈에서 XYZControllerForEfficientStorageOfStrings라는 이름을 사용한다면? 차이를 알아채기 힘들 정도로 두 단어는 비슷하다. 이런 이름은 프로그래머에게 흡사한 기능을 제공한다고 오해하게 만든다.

반면에 유사한 개념은 유사한 표기법을 사용한다. 일관성이 떨어지는 표기법은 그릇된 정보다. 예를 들어, controller라는 이름을 사용했다면, manager라는 이름을 사용하지 않는다. controllermanager는 유사한 개념이므로, 유사한 표기법을 사용해야 한다.

의미 있게 구분하라

컴파일러나 인터프리터만 통과하려는 생각으로 코드를 구현하는 프로그래머는 스스로 문제를 일으킨다. 예를 들어 동일한 범위 안에서는 다른 두 개념에 같은 이름을 사용하지 못한다. 그래서 프로그래머는 한쪽 이름을 마음대로 바꾸고 싶은 유혹에 빠진다.

나쁜 예시:

public static void copyChars(char a1[], char a2[]) {
  for (int i = 0; i < a1.length; i++) {
    a2[i] = a1[i];
  }
}

이름이 달라야 한다면 의미도 달라져야 한다. 연속적인 숫자를 덧붙인 이름(예: a1, a2, ..., aN)은 의도적인 이름과 정반대다. 이런 이름은 아무런 정보를 제공하지 못하는 이름이며, 저자의 의도를 전혀 드러내지 못한다.

좋은 예시:

public static void copyChars(char source[], char destination[]) {
  for (int i = 0; i < source.length; i++) {
    destination[i] = source[i];
  }
}

반면에 위와 같이 함수의 인수 이름을 sourcedestination으로 사용한다면 의도를 명확히 전달할 수 있고, 코드 읽기가 훨씬 더 쉬워진다.

의미가 불분명한 단어를 추가한 이름 역시 아무런 정보도 제공하지 못한다.

예를 들어, Product라는 클래스가 있다고 가정하자. 다른 클래스의 이름을 ProductInfo 혹은 ProductData라고 짓는다면, 이는 개념을 구분하지 않은 채 이름만 달리한 것이다. 위 두 클래스에서 제품 정보를 찾으려면 어느 클래스를 뒤져야 빠를까? 이런 이름은 그릇된 정보를 제공한다.

명확한 관례가 없다면 변수 moneyAccount는 money와, customerInfo는 customer와, accountData는 account와, theMessage는 message와 구분이 안 된다. 읽는 사람이 차이를 알도록 이름을 지어야 한다.

발음하기 쉬운 이름을 사용하라

이름을 발음하기 어렵다면, 그 이름은 사용하기 어렵다. 프로그래머는 코드를 읽고 말하기 때문에 발음하기 쉬운 이름을 사용해야 한다. 예를 들어, 책에서 한 회사에서 사용되었던 genymdhms (generate date, year, month, day, hour, minute, second)라는 변수를 나쁜 예시로 소개한다. 이런 이름은 새로운 개발자가 들어오면 변수를 설명해 준 다음 누군가 만들어낸 발음을 알려줘야 할 것이다. 발음하기 어려운 이름은 사용하기 어렵게 만든다.

나쁜 예시:

class DtaRcrd102 {
  private Date genymdhms;
  private Date modymdhms;
  private final String pszqint = "102";
}

좋은 예시:

class Customer {
  private Date generationTimestamp;
  private Date modificationTimestamp;
  private final String recordId = "102";
}

놀랍게도 두 코드는 같은 코드지만, 두 번째 코드는 훨씬 더 읽기 쉽다. 발음하기 쉬운 이름은 의미를 명확하게 전달해 줄 뿐만 아니라 코드를 읽기 쉽게 만든다.

검색하기 쉬운 이름을 사용하라

문자 하나를 사용하는 이름과 상수는 텍스트 코드에서 쉽게 눈에 띄지 않는다는 문제가 있다. 예를 들어, 코드에서 e라는 이름을 검색하면 e라는 문자가 들어간 모든 변수를 찾아야 한다. 이런 이름은 검색하기 어렵다.

이름의 길이는 범위의 크기에 비례해야 한다.

MAX_CLASSES_PER_STUDENT는 grep으로 찾기 쉽지만, 숫자 7은 찾기 어렵다. 7이 들어가는 파일 이름이나 수식이 모두 검색되기 때문이다. 이런 관점에서 긴 이름이 짧은 이름보다 좋다.

이름을 의미 있게 지으면 함수가 길어진다. 로컬 변수를 한 문자로 사용하는 것은 큰 문제가 되진 않는다. 하지만 변수나 상수를 여러 곳에서 사용한다면 검색하기 쉬운 이름이 바람직하다. 또한 이름을 사용하는 모든 곳을 찾아야 할 때, 약어를 사용한 짧은 이름보단 의미를 담은 긴 이름이 검색에 더 효과적이다.

인코딩을 피하라

유형이나 범위 정보까지 인코딩에 넣으면 그만큼 이름을 해독하기 어려워진다. 문제 해결에 집중하는 개발자에게 인코딩은 불필요한 정신적 부담이다. 인코딩한 이름은 대부분이 발음하기 어려우며 오타가 생기기도 쉽다.

헝가리식 표기법을 피하라

프로그래밍 언어에서 변수 및 함수의 임자 이름 앞에 데이터 타입을 명시하는 코딩 규칙이다.

IDE라는 게 부실했던 80년대 당시에는 이 규칙이 엄청난 센세이션을 불러일으켰다. 당시는 컴파일러가 타입을 점검하지 않았으므로 프로그래머에게 타입을 기억할 단서가 필요했다.

하지만 최근의 모든 프로그래밍 언어는 훨씬 많은 타입을 지원하며, 또한 컴파일러가 타입을 기억하고 강제한다. 자바 프로그래머는 더 이상 변수 이름에 타입을 인코딩할 필요가 없다. 객체는 강한 타입이며, IDE는 코드를 컴파일하지 않고도 타입 오류를 감지할 수 있다. 따라서 이제는 헝가리식 표기법이나 기타 인코딩 방식이 오히려 방해될 뿐이다. 변수, 함수, 클래스 이름이나 타입을 바꾸기가 어려워지며, 읽기도 어려워진다.

나쁜 예시:

PhoneNumber phoneString; // 타입이 바뀌어도 이름은 바뀌지 않는다!

멤버 변수 접두어

멤버 변수에 m_이라는 접두어를 붙이는 방식도 더 이상 불필요하다. 이런 접두어를 사용하는 대신 멤버 변수를 다른 색상으로 표시하거나 눈에 띄게 보여주는 IDE를 사용해야 마땅하다.

나쁜 예시:

public class Part {
  private String m_dsc; // 설명
  void setName(String name) {
    m_dsc = name;
  }
}

좋은 예시:

public class Part {
  String description;
  void setDescription(String description) {
    this.description = description;
  }
}

인터페이스 클래스와 구현 클래스 (클래스 구분 설명) (opens in a new tab)

때로는 인코딩이 필요한 경우도 있다. 예를 들어, 도형을 생성하는 abstract factory를 구현한다고 가정하자. 이 팩토리는 interface class다. 구현은 concrete class에서 한다. 그렇다면 두 클래스 이름을 어떻게 지어야 좋을까?

옛날 코드에서 많이 사용하는 접두어 I는 주의를 흩트리고 과도한 정보를 제공한다. 그래서 인터페이스 클래스 이름과 구현 클래스 이름 중 하나를 인코딩해야 한다면 구현 클래스 이름을 택하는 게 좋다고 판단한다.

나쁜 예시:

interface IShapeFactory {
  Shape newShape();
}

좋은 예시:

interface ShapeFactory {
  Shape newShape();
}
 
class ShapeFactoryImpl implements ShapeFactory {
  Shape newShape() {
    return new Shape();
  }
}

자신의 기억력을 자랑하지 말라

독자가 코드를 읽으면서 변수 이름을 자신이 아는 이름으로 변환해야 한다면 그 변수 이름은 바람직하지 못하다. 예를 들어 문자 하나만 사용하는 변수 이름은 문제가 있다. 독자가 실제 개념으로 변환해야 하니까. 최악은 a와 b를 이미 사용하므로 c를 선택한다는 논리다.

똑똑한 프로그래머와 전문가 프로그래머 사이에서 나타나는 차이점 하나만 들자면, 전문가 프로그래머는 명료함이 최고라는 사실을 이해한다. 전문가 프로그래머는 자신의 능력을 좋은 방향으로 사용해 남들이 이해하는 코드를 내놓는다.

클래스 이름

클래스 이름과 객체 이름은 명사나 명사구가 적합하다. Customer, WikiPage, Account, AddressParser 등이 좋은 예다. Manager, Processor, Data, Info 등과 같은 단어는 피하고, 동사는 사용하지 않는다.

메서드 이름

메서드 이름은 동사나 동사구가 적합하다. postPayment, deletePage, save 등이 좋은 예다. 접근자(getter), 변경자(setter), 조건자(predicate)는 javabean 표준에 따라 값 앞에 get, set, is를 붙인다.

String name = employee.getName();
customer.setName("mike");
if (paycheck.isPosted())...

생성자(constructor)를 중복정의(overload)할 때는 정적 팩토리 메서드를 사용한다.

생성자 대신 정적 팩토리 메서드를 고려하라

예를 들어, 아래의 두 예제를 보면 정적 팩토리 메서드를 사용한 것이 더 명확하다.

Complex fulcrumPoint = Complex.FromRealNumber(23.0);

메서드는 인수를 설명하는 이름을 사용한다. 위 코드가 아래 코드보다 더 명확한 의미를 전달한다.

Complex fulcrumPoint = new Complex(23.0);

기발한 이름은 피하라

이름이 너무 기발하면 저자와 유머 감각이 비슷한 사람만, 그리고 농담을 기억하는 동안만, 이름을 기억한다. 예를 들어 kill() 대신에 whack()이라 부르거나 abort() 대신 eatMyShort()이라 부른다면 특정 문화를 알아야 이해할 수 있을 것이다. 재미난 이름보다 명료한 이름을 선택하라.

한 개념에 한 단어를 사용하라

추상적인 개념 하나에 단어 하나를 선택해 이를 고수한다. 예를 들어, 똑같은 메서드를 클래스마다 fetch, retrieve, get으로 제각각 부르면 혼란스럽다. 따라서 메서드 이름은 독자적이고 일관적이어야 한다. 그래야 주석을 뒤져보지 않고도 프로그래머가 올바른 메서드를 선택할 수 있다.

마찬가지로, 동일 코드 기반에 controller, manager, driver를 섞어 쓰면 혼란스럽다. DeviceManager와 ProtocolController는 근본적으로 무엇이 다른가? 이름이 다르면 독자는 당연히 클래스도, 타입도 다르리라 생각한다.

일관성 있는 어휘는 코드를 사용할 프로그래머가 반갑게 여길 선물이다.

말장난하지 마라

한 단어를 두 가지 목적으로 사용하지 마라. 다른 개념에 같은 단어를 사용한다면 그것은 말장난에 불과하다.

예를 들어, 여러 클래스에 add라는 메서드가 생겼다. add라는 메서드는 값을 더하는 연산을 수행한다. 새로 작성하는 메서드는 집합에 값 하나를 추가한다. 이 메서드는 add라 불러도 괜찮을까? add라는 메서드가 많으므로 일관성을 지키려면 add라 불러야 하지 않을까? 하지만 새 메서드는 기존 add 메서드와 맥락이 다르다. 이런 경우에는 insertappend라는 이름이 적당하다. 새 메서드를 add라 부른다면 이는 말장난이다.

프로그래머는 코드를 최대한 이해하기 쉽게 짜야 한다. 집중적인 탐구가 필요한 코드가 아니라 대충 훑어봐도 이해할 코드 작성이 목표다. 의미를 해독할 핵임이 독자에게 있는 논문 모델이 아니라 의도를 밝힐 책임이 저자에게 있는 잡지 모델이 바람직하다.

해법 영역에서 가져온 이름을 사용하라

코드를 읽을 사람도 프로그래머라는 사실을 명심한다. 그러므로 전산 용어, 알고리즘 이름, 패턴 이름, 수학 용어 들을 사용해도 괜찮다. 모든 이름을 문제 영역(domain)에서 가져오는 정책은 현명하지 못하다. 같은 개념을 다른 이름으로 이해하던 동료들이 매번 고객에게 의미를 물어야 하기 때문이다.

문제 영역에서 가져온 이름을 사용하라

적절한 프로그래머 용어가 없다면 문제 영역에서 이름을 가져온다. 그러면 코드를 보수하는 프로그래머가 적어도 분야 전문가에게 의미를 물어 파악할 수 있기 때문이다.

우수한 프로그래머가 설계자라면 해법 영역과 문제 영역을 구분할 줄 알아야 한다. 문제 영역 개념과 관련이 깊은 코드라면 문제 영역에서 이름을 가져오는 게 좋다.

의미 있는 맥락을 추가하라

스스로 의미가 분명한 이름이 없지 않다. 하지만 대다수 이름은 그렇지 못하다. 그래서 클래스, 함수, 이름 공간에 넣어 의미를 분명하게 한다. 모든 방법이 실패하면 마지막 수단으로 접두어를 붙인다.

예를 들어, firstName, lastName, street, houseNumber, city, state, zipcode라는 변수가 있다. 변수를 훑어보면 주소라는 사실을 금방 알아챈다. 하지만 어느 메서드가 state라는 번수 하나만 사용한다면? 변수 state가 주소의 일부라는 사실을 금방 알아챌까?

가장 좋은 방법은 Address라는 클래스를 만드는 것이다. 그러면 state라는 변수가 Address 클래스의 일부라는 사실을 알 수 있다. 하지만 Address라는 클래스를 만들기에는 너무 작다면, Address라는 접두어를 붙여 addrFirstName, addrLastName, addrState라고 쓰는 방법도 있다. 접두어로 인해 변수가 좀 더 큰 구조에 속한다는 사실이 적어도 독자에게는 분명해진다.

불필요한 맥락을 없애라

Gas Station Deluxe라는 애플리케이션을 짠다고 가정하자. 모든 클래스 이름을 GSD로 시작하겠다는 생각은 전혀 바람직하지 못하다. 예를 들어 GSD 회계 모듈에 MailingAddress 클래스를 추가하면서 GSDAccountAddress로 이름을 바꿨다. 나중에 다른 고객 관리 프로그램에서 고객 주소가 필요할 때 GSDAccountAddress를 사용할 수 있을까?

일반적으로는 짧은 이름이 긴 이름보다 좋다. 단, 의미가 분명한 경우에 한해서다. 이름에 불필요한 맥락을 추가하지 않도록 주의한다.

accountAddresscustomerAddressAddress 클래스의 인스턴스로는 좋은 이름이다. 하지만 클래스 이름으로는 적합하지 않다. Address와 같이 불필요한 맥락을 제거해야 의미가 더 분명해진다.

결론

사람들이 이름을 바꾸지 않으려는 이유 중 하나는 다른 개발자가 반대할까 두려워서다. 하지만 (좋은 이름으로 바꿔준다면) 오히려 반갑고 고마울 것이다. 우리들 대다수는 자신이 짠 클래스 이름과 메서드 이름을 모두 암기하지 못한다. 암기는 요즘 나오는 도구에 맡기고, 우리는 문장이나 문단처럼 읽히는 코드 또는 적어도 표나 자료 구조처럼 읽히는 코드를 짜는 데만 집중해야 마땅하다. 여느 코드 개선 노력과 마찬가지로 이름 역시 나름대로 바꿨다가는 누군가 질책할지도 모른다. 그렇다고 코드를 개선하려는 노력을 중단해서는 안 된다. 이름을 바꾸는 데 시간을 들이면 코드를 읽는 사람이 훨씬 더 행복해진다. 그러므로 이름을 주의 깊게 살펴 더 나은 이름이 떠오르면 개선하기를 바란다.