자바스크립트를 활성화 해주세요

Clean Code 2장: "의미 있는 이름" 정리

 ·  ☕ 11 min read

2장 의미 있는 이름을 읽으면서 내용에 대해 보충하거나, 개인적인 의견으로 반박하거나, 고민해 볼 부분에 대해 적어보려 한다.

들어가면서

이름 짓기는 이미 프로그래머들이 어려워하는 일로 유명하다.

프로그래머들은 변수, 함수, 클래스, 파일, 패키지/모듈, 프로젝트 이름 등 이곳 저곳에서 이름을 짓곤 한다.

하지만 마치 태어날 아이의 이름을 짓는 것 처럼, 좋은 이름을 지어 주는 것은 상당히 어려운 일이다.

의도를 분명히 밝혀라

변수, 함수, 클래스 등의 이름은 왜 이것이 존재하는지, 무엇을 하는지, 어떻게 쓰는 지 알려줘야 한다. 만약 해당 이름이 주석을 요구한다면 그것은 그 의도를 정확히 나타내지 못하는 것이다.

int d; // elapsed time in days
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;

책에서 나온 예제로, 좀 더 상세한 정보를 나타내길 바라고 있다. 하지만 나는 수정한 예시가 별로 좋지 못하다고 생각한다. (정확히는 예시의 시작부터 별로 좋지 못하다고 생각한다.)

  1. 단위가 변수 이름에 나오는데, 이는 변수 명이 너무 장황하게 한다.
  2. daysSinceCreation, daysSinceModification은 모두 days로 시작하는데, 복수 표현으로 인해 collection형 타입으로 의심할 수도 있다.
  3. 해당 변수 존재 자체가 무의미하다 생각한다.
    내 생각에 변수는 중요한 정보를 담기 위해, 계산 과정이 복잡한 결과를 보관하기 위해, 중간 상태를 저장하기 위해 존재해야 한다고 생각한다. 하지만 이렇게 시간 간격을 나타내는 값은 단순하게 timestamp 간 차이로 알 수 있다.
  4. 요즘 IDE의 자동완성, 정의 미리보기 등의 기능을 고려했을 때, 해당 주석이 잘 보일 가능성이 높다.
  5. 내가 영어를 잘 못하는 것일 수도 있지만, int elapsedDays로도 충분해보인다.
public List<int[]> getThem() {
    List<int[]> list1 = new ArrayList<int[]>{};
    for (int[] x : theList)
        if (x[0] == 4)
            list1.add(x);
    return list1;
}
public List<int[]> getFlaggedCells() {
    List<int[]> flaggedCells = new ArrayList<int[]>{};
    for (int[] cell : gameBoard)
        if (cell[STATUS_VALUE] == FLAGGED)
            flaggedCells.add(cell);
    return flaggedCells;
}
public List<Cell> getFlaggedCells() {
    List<Cell> flaggedCells = new ArrayList<int[]>{};
    for (Cell cell : gameBoard)
        if (cell.isFlagged())
            flaggedCells.add(cell);
    return flaggedCells;
}

초기 코드에서는 theList가 무엇인지도 알 수 없고, 왜 배열의 0번째 요소를 확인하는지, 왜 값이 4인지 확인하는지에 대한 정보를 알 수 없다. 또한 list1이 코드 흐름에 따르면 무엇인가를 찾아서 그 목록을 반환해주는 것은 알 수 있지만 적절한 이름으로는 보이지 않는다.

1차 리팩토링 과정에서는 위의 문제를 해결하도록 변수, 상수 명을 교체, 선언했다.

2차 리팩토링 과정에서는 코드를 단순화 하면서 왜 getFlaggedCells()의 반환 타입이 int[]인지 고민하지 않도록 Cell이란 클래스로 추상화하였다. 또한 이를 추상화 함으로서 실제 배열의 특정 인덱스 값을 비교하는 것에서 간단한 확인 함수를 호출하는 것으로 변경하였다.

1차 리팩토링까지는 격하게 공감하지만, 2차 리팩토링의 경우 조금 의문을 갖게 한다.

  1. 해당 Cell이란 클래스가 따로 클래스로 구별되어야 할 만큼 추상화가 필요할까?
  2. 간단한 비교문을 확인하기 위해 메서드 isFlagged()가 호출된다.
    함수 호출로 인한 시간적 비용이 추가된다. (차라리 비트 연산을 통해 확인하는 것이었다면 함수로 표현한 것이 더 좋은 추상화라 생각한다.)
  3. Cell을 클래스로 추상화했다면, 해당 메서드는 다른 클래스의 멤버 메서드가 아니라, Cell 클래스의 static 메서드여야 더 적절하지 않을까?

위의 예시들은 의미 있는 이름을 표현하기 위한 단적인 예시를 보여주느라 복합적인 상황은 판단하지 못한 것으로 보인다.

그릇된 정보를 피하라

프로그래머들은 코드에 잘못된 단서를 남기는 것을 피해야 한다. 이 잘못된 단서로 인해 전체 코드의 의미를 오해할 수 있기 때문이다.

예를 들어 accountList는 해당 변수가 진짜 List 형식이 아니라면 사용하지 말고, 차라리 accountGroup, bunchOfAccounts, 혹은 그냥 accounts로 표현하는 것이 더 낫다고 한다.

나도 약간 비슷한 기법을 사용하는데, collection형 타입(배열, 템플릿 기반 자료구조 등)은 언제나 복수로 표현한다. 한국어와 달리 영어는(당연히 코드를 영어로 작성하니 영어만 말하는 것이다.) 단수, 복수 표현이 엄밀하게 구분되므로, 복수 표현만으로 collection형 정보를 나타낼 수 있다는 것은 매우 경제적이라 생각한다.

변수명에서 헷갈리는 글자를 쓰지 말라고 하면서, 소문자 L, 대문자 i, 숫자 1들을, 대문자 o, 숫자 0를 섞어 사용하지 말라고 한다. 그런데 이건 프로그래머라면 이런 글자들을 잘 구분할 수 있는 폰트를 사용하고 있을텐데, 그렇게 치명적인 문제가 될 수 있는지 모르겠다. 게다가 IDE에서 자동완성이나 문법 검사 등으로 경고를 해줄텐데 말이다.

의미 있게 구분하라

수열 형태의 이름(a1, a2, a3…)은 의도를 알 수 없는 이름의 대표적인 예시이다.

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

책에서 설명한 예시처럼 복사할때 원본과 복사본을 잘 구분할 수 있게 수정해 본 예시다.

그 외에도 무의미하게 비슷한 이름은 헷갈리게 하는 명명법 중 하나라고 하면서 아래 예시를 보여주고 있다.

getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();

그런데 위에서 얘기한 것 처럼 나는 복수 표현으로 일반 변수와 collection형 변수를 구분하는데, 이 외에도 영어에서의 관사 표현과 관련된 이야기도 하고 있다.

영어의 문법적인 용법을 모두 사용하는 것은 좋은 방법일 수도 있지만, (대부분이 개발용 표현 언어로 영어만 쓰는 상황에서) 오히려 기초지식을 요구하는 안 좋은 기법이 될 수도 있겠다는 생각이 든다.

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

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";
    /* ... */
}

전혀 동의되지 않는 기법이다. 일단 Java답게 쓸데없이 이름이 길어지는 것도 불필요하다고 생각하고, 변수 명을 저렇게 줄여놔도 당연히 서로 대화할 때는 풀어서 부르지 않을까?

추가적으로 변수명을 축약하기 위한 내 기법은 다음과 같다.

  1. 적절한 영문 단어를 선택한다.
  2. 해당 영문 단어를 한글로 음독한다고 가정한다.
  3. 각 음절의 초성에 해당하는 알파벳으로 단어를 줄인다.

예를 들어, DB의 항목을 표현하는 변수를 Record라 했을 때, 레코드에서 rcd로 변수 명을 선언하고, 서비스를 svc로 선언하는 식이다.

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

솔직히 검색하기 쉬운 이름같은 표현보단 매직 넘버를 사용하지 말라고 표현하는게 더 간단한데, 굳이 이런 긴 설명이 필요했을까 싶다.

인코딩을 피하라

대표적인 변수 이름을 인코딩 하는 방법인 헝가리식 표기법 위주로 설명을 하고 있다.

책에서 다루는 헝가리식 표기법 외에도, 이전 회사의 경우 C언어에서 변수가 포인터인지, 일반 변수인지 구별을 위해 앞에 pt를 붙이는 방법도 사용해봤다.

헝가리식 표기법

MFC 하면 신나게 볼 수 있는 헝가리안 표기법이다.

나도 헝가리안 표기법이 안 좋은 표기법이라고 생각하는데, 아쉽게도 로마에서는 로마법을 따르라고, MFC에서는 헝가리안 표기법을 따르는게 가독성을 올리는 방법이다.

헝가리안 표기법의 문제는 다음과 같다.

  1. 현대 IDE에서 변수의 자동완성은 쉽다.
  2. 자료형을 prefix에 표현하는데, 자료형이 변경될 때 매번 리팩토링 해 줘야 한다.
  3. 부자연스러운 표기법이 된다.

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

반복문의 인덱스 표현을 위한 i, j, k는 이미 전통적으로 사용되어왔고, 코드 상 해당 변수의 볌위가 넓지 않으므로 괜찮으나, 다른 부분에서 사용하는 짧은 글자의 변수는 좋지 않다.

클래스 이름

클래스 이름은 명사 혹은 명사구여야 한다.

Manager, Processor, Data, Info같은 단어 사용을 피하라고 하는데, 이는 너무 일반화된 단어를 사용하여 해당 클래스의 의미를 모호하게 하지 말라는 것으로 보인다.

클래스 이름은 동사가 되어선 안된다.

메서드 이름

메서드 이름은 동사 혹은 동사구여야 한다.

접근자, 수정자, 서술자(조건 확인)는 각각 get, set, is등으로 시작하여 해당 값을 나타내는 방식으로 표현해야 한다.

생성자가 오버로딩 되어있는 경우, static 팩토리 메서드를 통해 어떤 값으로 생성되는지를 나타내라.

기발한 이름은 피하라

대표적 실패한 예시로 리눅스의 kill, PHP5의 explode()가 생각난다. (개인적으로는 C++의 stl vector<>도 안 좋은 예시라 생각한다.)

kill은 리눅스에서 프로세스를 종료시키는 명령어인데, 마치 살아있는 프로세스를 죽이는 것 처럼 표현하기 위해 kill이란 이름을 사용한 것으로 보인다. kill이란 표현법 자체가 비유인 것도 아쉽지만, 내부 동작 원리나, 실제 사용 예시 등을 고려했을 때 더 실패한 예시라 생각한다. kill의 내부 원리는 해당 프로세스에게 시그널을 보내는 것 뿐이다. 기본적으로 SIGTERM을 보내는데, 해당 시그널은 종료 요청일 뿐, 해당 프로세스가 바로 종료되지 않을 수도 있다. (보통 프로세스 종료 전 리소스 정리, 마무리 등을 할 수 있게 하려고 사용한다.) 섬뜩한 설명이지만, 상대를 죽일 때 유서를 쓸 시간도 주고, 인생을 정리 할 시간도 주고 죽이는가? kill의 의미에 더 적절한 시그널은 SIGKILL인데, (즉시 해당 프로세스가 종료된다.) 시그널의 이름이나, 명령어 역할이 완벽히 일치하지 않는 문제를 볼 수 있다.

explode()는 예전부터 봤던 실패한 함수 이름의 대표적인 예시다. 해당 문서를 보면 알 수 있겠지만, 문자열을 다른 경계 문자열로 자르는 함수다. 대부분의 프로그래밍 언어는 split()으로 해당 기능을 제공하며, C의 경우 strtok()으로 해당 기능을 제공한다. split()는 문자열을 분리한다는 의미를 내포하고 있으며, strtok()는 문자열을 토큰화 한다는 의미로 명명되었다. 하지만 explode()는 폭발시킨다는 뜻인데, 이것만으로는 문자열을 분리한다고 이해하기 힘들 수도 있다.

vector<>는 Java의 ArrayList<>처럼 길이가 가변적인 배열을 표현하는 collection형 자료구조다. 일반적인 벡터라는 단어의 용례를 생각했을 때, 이 이름이 적절한지 의문이다. (물론 해당 자료구조를 사용해 정말 벡터를 표현할 수도 있겠지만, 일반적으로 사용할 수 있는 가장 적절한 이름은 절대 아닌 것 같다.)

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

프로그래밍 중에는 자주 사용하는 비슷한 개념의 용어들이 많다. fetch, retrieve, get 모두 무언가를 가져오거나 받을 때 사용할 만한 함수의 이름이다. 이런 식으로 비슷한 단어를 이용하여 이름을 지으면 나중에 코드를 읽거나, 검색할 때 헷갈리기 쉽다.

책에서는 다른 예시로 controller, manager, driver가 모두 비슷한 역할처럼 보이기 때문에 정확한 용도를 구분하기 힘들다고 이야기 한다. 이전 회사에서는 한 프로젝트 안에서 저 단어들을 모두 사용하여 작업한 적이 있는데, 당시에는 각 단어들을 엄밀하게 정의했기 때문에 가능했다. (게다가 임베디드 환경은 이런 비슷하게 들리는 단어들을 자주 사용하는 편이다.)

적절한 단어를 선택하고자 한다면 Thesaurus(유의어 사전)을 참고하는 것도 좋다. (좀 더 목적에 정확한 단어 선택을 위해 참고)

말장난을 하지 마라

한 개념에 여러 단어를 사용했을 때의 문제와 반대로, 한 단어에 여러 개념을 표현하지 말라는 것이다.

예를 들어 collection형 자료구조에서 add()를 사용한다면 특정 자료를 추가하는 것은 알 수 있으나, 특정 위치에 삽입되는지, 자료 구조의 특징에 따라 자율적으로 배치되는지, 다른 collection과 합치는 것인지 구분하기 힘들다. append()insert()로 목적을 더 정확하게 표현하는 것이 좋다.

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

당연히 코드를 읽는 프로그래머들은 컴퓨터과학 지식을 갖고 있을테니 컴퓨터과학 관련 용어, 알고리즘 이름, 패턴 이름, 수학 용어 등을 활용하자.

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

도메인 전문 용어가 필요한 경우, 해당 용어를 사용하자.

의미 있는 맥락을 추가하라

예를 들어 street, houseNumber, city, state, zipcode가 같이 선언된 코드 맥락에서 state는 주(미국 기준, 아마 한국 기준으로는 시/도)를 표현하는 것을 바로 알 수 있다. 하지만 다른 변수가 없이 그냥 state만 있었다면 상태를 나타내는 것이라고 생각할 수도 있다. 이럴 경우 앞에 맥락을 추가해서 의도를 분명히 할 수 있다. 예를 들어 addrState처럼 말이다.

private void printGuessStatistics(char candidate, int count) {
    String number;
    String verb;
    String pluralModifier;
    if (count == 0) {
        number = "no";
        verb = "are";
        pluralModifier = "s";
    } else if (count == 1) {
        number = "1";
        verb = "is";
        pluralModifier = "";
    } else {
        number = Integer.toString(count);
        verb = "are"
        pluralModifier = "s";
    }
    String guessMessage = String.format(
        "There %s %s %s%s", verb, number, candidate, pluralModifier
    );
    print(guessMessage);
}
public class GuessStatisticMessage {
    private String number;
    private String verb;
    private String pluralModifier;

    public String make(char candidate, int count) {
        createPluralDependentMessageParts(count);
        return String.format(
            "There %s %s %s%s",
            verb, number, candidate, pluralModifier );  
    }

    private void createPluralDependentMessageParts(int count) {
        if (count == 0) {
            thereAreNoLetters();
        } else if (count == 1) {
            thereIsOneLetter();
        } else {
            thereAreManyLetters(count);
        }
    }

    private void thereAreManyLetters(int count) {
        number = Integer.toString(count);
        verb = "are"
        pluralModifier = "s";
    }

    private void thereIsOneLetter() {
        number = "1";
        verb = "is";
        pluralModifier = "";
    }

    private void thereAreNoLetters() {
        number = "no";
        verb = "are";
        pluralModifier = "s";
    }
}

솔직히 예시로 보여준 코드에선 크게 공감이 가지 않는다. (그냥 주석으로 단수/복수 표현에 관련된 문법 표현을 변경한다고만 해도 충분해 보인다.)

불필요한 맥락을 없애라

격하게 공감하는 바이다. 이전 회사의 코딩 스타일에서는 불필요할정도로 회사의 키워드를 prefix로 쓰는 경우가 많았다.

특히 임베디드 환경이다 보니 정수형 타입도 비트 크기에 따라 U8, S32, 등으로 구분하여 표현하는 것이 좋은데, 굳이 그런 모든 타입에 회사 키워드 prefix를 붙이게 했다.

회사내 정확한 코딩 컨벤션의 문서에서는 외부로 공유될 코드에는 해당 규칙을 따르라고 했는데, 솔직히 무슨 의미가 있나 싶다. 괜히 불필요한 타입 변환 경고만 출력하게 하고, 추상화가 잘 이루어 지는 것도 아니고, 그렇다고 구조체 수준의 복잡한 자료구조가 많았던 것도 아닌데 말이다.

그나마 이런 prefix를 붙여서 의미 있는 경우라면, 여러 종류의 제품이나 환경을 합치는 adapter pattern을 적용할 때가 있을 것 같다.

마치면서

역시 나는 Java가 싫은가보다. 여러 특성 중 Java의 장황함이 가장 싫어서 그런 것 같다.


JaeSang Yoo
글쓴이
JaeSang Yoo
the programmer

 
목차