본문 바로가기
도서

[클린코드 핥아먹기 시리즈] 1. 의미 있는 이름

by 무자비한 낭만주먹 2024. 1. 3.
목차
0. 개요
1. 의도를 분명히 밝혀라
2. 그릇된 정보를 피하라
3. 의미 있게 구분하라
4. 발음하기 쉬운 이름을 사용해라
5. 검색하기 쉬운 이름을 사용해라
6. 너 분명히 기억 못하니까 i, j, k 같은 상징적인 변수명 말곤 갖잖은 변수명 쓰지 마라
7. 클래스 이름과 메서드 이름
8. 너만 아는 밈으로 이름 짓지 마라
9. 비슷한 개념은 최대한 통일하되 서로 다른 로직은 확실히 구분하라
10. 의미 있는 맥락을 추가하라
11. 불필요한 맥락을 없애라

 

 

 

0. 개요
6달 전 즈음 .. 당시에 나는 클린 코드를 감명깊게 읽고 "내 인생책" 이라고 평가를 했었는데 몇 달 지난 지금 사실 무슨 내용이었는지 잘 기억이 나지 않는게 아닌가. 내 인생책의 내용을 기억 못하는 상황이 웃겨서 글로 다시 한번 정리해야겠다는 생각이 들어 본 시리즈를 작성키로 했다.

[그림1]. 작명에 혼을 담는 걸 멈추지마라, 마치 박명수처럼

이번 장의 주제는 "이름을 잘 짓는 방법"이다. 사실 이름을 짓는 다는 행위는 결국 "특정 공동체의 문화" 라던지 "협업을 위해 정의한 컨벤션", "업무 도메인"등 다양한 환경적 요인이 고려되야 하는 행위이기 때문에 "정답"을 찾는건 무의미한 행동이라고 생각한다. 다만 클린코드의 저자가 책의 개요 부분에 "이건 우리의 방법이니 그냥 그런게 있구나 ~ 하고 존중해주세요" 라고 밑밥을 깔아놨던게 기억나 그냥 "이런 방법도 있구나 ~" 정도로만 보려고한다.

 

 

1. 의도를 분명히 밝혀라

 

[그림2]. 의도가 분명하지 않은 표현

저자가 제안한 이름 잘 짓는 첫 번째 방법은 변수명이나 메서드명을 지을 때 "의도를 분명히" 밝히는 것이다. 예컨대

.
.
overDate(d);

위 코드만 딱 주어졌다고 했을 때 저 변수명 'd'가 어떤 값을 가지고 있는지 알 방법이 있는가? "overDate라는 거 보니까 뭐 날짜 인가 ..?  d니까 date에 약자겠고만 .. 그럼 무슨 날짜지?" 정도가 내가 추론할 수 있는 최대의 영역이다. 그럼 저 d 값을 찾기 위해 굳이 불편하게 해당 변수의 정의부를 찾아가봐야한다.

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

 

다행히 주석이 달려 있어 무슨 의미인지는 이해할 수 있었다. 하지만 주석도 없다면 ? 모든 코드를 분석해 저 변수가 어디에 쓰이는지를 유추해야한다. 변수명 하나만 잘 지었으면 아무렇지 않게 넘어갔을 일인데, 불필요한 작업 시간이 발생했다.

# 저자가 제안한 이름
(1) int elapsedTimeInDays;
(2) int daysSinceCreation;
(3) int daysSinceModification;
(4) int fileAgeInDays;

 

그러면 변수명을 어떻게 지어야 의도가 분명할 수 있는가? 저자는 이에 대한 해결로 위 이름들을 제시했고 "보다 직접적이고 명확한 묘사"가 담긴 이름이 "좋은 이름"이라고 주장하는 것으로 나는 이해했다.

[그림3]. 예시 하나 더 주세요 ~

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

 

위 코드는 저자가 제시한 예시 코드인데 이 코드는 어떤 문제를 야기할까?

Q1. theList에는 뭐가 들었을까?
Q2. theList에서 0 번째 값을 4와 비교하네? 저기 뭐가 들은거여?
Q3. 4는 뭘 의미하는거여? 4가 의미하는게 뭐지?
Q4. 저 list1은 호출한 곳에서 어떻게 쓰이는거지?

 

변수명이 명확하지 않은 관계로 이 코드를 처음보는 나는 위와 같은 질문을 해결해야한다. 이 질문들을 해결하기 위해 아래와 같은 방법들을 사용할텐데,

A1. 일단 theList가 뭔지 찾아야하고,
A2. theList의 0번째 값에 뭐가 들어가는지 디버깅 해봐야한다.
A3. 4라는 값이 뭘 의미하는지 히스토리를 찾아봐야한다.
        (심지어 다른 업무단에서 관리하는 거라면? 또 물어봐야 한다)
A4. list1 메서드가 쓰이는 클래스를 찾아가서 확인해봐야 한다.

[그림4]. 확 짜증나는 개발자 명수씨

시뮬레이션만 해봤는데 벌써 짜증난다. 예시 코드는 간단해서 그래도 금방 이해가 갔지만 구조가 조금만 복잡해진다면 하루 종일 분석해도 반영 전에 찝찝한 상황이 발생할 것 같다. 일단 먼저 개선 해보자.

[그림5]. 의도가 명확하지 않은 변수명을 개선하자

infer1. getflaggedCells() 라는 메서드 이름을 보니 게임보드 상에 체크된 애들 정보를 받는건가?
infer2. int[] 리스트인 flaggedCells라는 객체를 생성했네? 응답으로 내려줄 응답 객체인가보다.
infer3. 실제 게임보드에서 각 요소를 cell로 추출해서 STATUS_VALUE 라는 상태값이 FLAGGED 인지 확인하고
          응답 객체(flaggedCells)에 넣어주네

개인적으로 이정도만 되도 개발하는데 큰 어려움은 없을 거 같다. 하지만 저자가 제안한 방법은 여기서 한 발 더 나아갔는데 int[] 같은 배열로 (map도 마찬가지) 상태를 표시하는 건 좋지 못한 선택이기에 Class 로 추출하고 확인은 메서드를 통해 하도록 했다. 또 내부적으로 ENUM을 이용해 확인하는게 더 명확하고 주장했다.

[그림6]. 저자가 최종적으로 제안한 개선안

기존의 int[]를 통해 상태값을 나타내던 구조를 Class로 변경한 뒤 객체의 메서드를 이용해 값을 검증하고, 코드에는 나오지 않았지만 값 식별을 위해서 ENUM을 이용할 것을 권고했다.

[그림7]. 최초 코드와 최종 개선 코드

이제 최초 코드와 개선된 코드를 비교해보자. 이해하기 쉬워졌다고 느껴지는가? 나는 그런 거 같다. 어쨌든 이번 절에서 저자가 하고 싶었던 말을 핵심은 아래와 같을 것 같다.

결론: "변수, 메서드명에 의도를 담아라"

 

 

2. 그릇된 정보를 피하라

[그림8]. 아무튼 가짜뉴스 ..

당연하겠지만 변수 명을 '가짜 뉴스' 만들듯이 하면 안된다. 그렇지만 사실 나는 의도치 않게 이런 유혹을 받는 경우가 종종 있다. 아래 예시를 보자.

fun Product(): Unit {
    val productList: IntArray = intArrayOf()
.
.
}

위 코드를 보면 productList는 int형 배열인데 'List'라는 suffix를 사용하고 있다. 물론 틀린 표현은 아니지만 개발자 관점에서 List와 Array는 전혀 다른 개념이기 때문에 이번 절의 주제인 "그릇된 정보를 제공"하는 경우가 될 수 있다.

getProductAllByUserName()
getProductAnyByUserName()

두번째 예시로, 위 두 메서드를 한번 읽어보자. 두 메서드명 사이에 어떤 차이가 있는지 한 눈에 보이는가? 아마 한참 찾아봐야 할 것이다. 사실 두 메서드가 같은 모듈이나 클래스 안에 존재한다고 하면 큰 문제는 없을 거 같지만 저 메서드들이 서로 다른 모듈이나 클래스에 존재한다고 하면 이 또한 "잘못된 정보를 제공"한다고 볼 수도 있다고 저자는 주장한다.

#case 1.
int a = l;
int a = 1;

#case 2.
int b = o;
int b = 0;

위 두 코드를 보면 무엇이 문제인지 알겠는가? 최근에는 IDE가 많이 발전해서 헷갈리는 일이 많이 적어졌지만 저런 유사한 형태의 문자들이 혼동을 줄 수 있기 때문에 I, l, 0, o 같은 단일 문자들을 식별자로 두는 것이 "잘못된 정보를 제공"하는 행위가 될 수 있음을 인지하고 지양해야 한다고 주장했다.

결론: "내가 짠 코드가 동료에게 잘못된 정보를 제공할 가능성이 있는지 검토하라"

 

 

3. 의미 있게 구분하라
대충 의미 없는 불용어 가져다 붙여서 변수 구분하지 마라.

이번에는 이번 절의 결론을 먼저 요약해봤다. 거두절미하고 아래 코드를 보자.

fun capyChars(a1: charArray, a2: charArray){
    for (i in a1.indices)
        a2[i] = a1[i]
}

보면 그냥 a라는 변수 때려넣고 그 뒤에 하나 더 필요하니까 [a1, a2, a3, ... aN]까지 연속적인 숫자를 이용하는 경우들이 있는데 이건 앞서 설명한 "그릇된 정보"를 제공하는 것도 아니고 그냥 아무런 정보를 제공하지 못하는 이름이 된다. 

(1) Money와 MoneyAmount
(2) TheKing과 King
(3) Product와 ProductInfo

The, Amount, Info 같은 것들도 불용어가 될 수 있다. TheKing과 King의 차이가 뭔지 유추가 되는가? Product와 ProductInfo는? ProductInfo니까 Product의 정보가 들어간 클래스라고 보면 되는건가? 그럼 Product에는 클래스의 정보가 안들어가야 하는건가? 등등 .. 그 차이를 이해하는데 하등 도움이 되지 않는다. 오히려 ProductInfo같은 경우는 같은 의미가 중복으로 들어간다고도 볼 수 있기 때문에 가독성 측면에서도 좋지 않다.

결론: 구분한답시고 의미 없는 불용어 때려넣지 마라

 

 

 

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

[그림9]. 발음하기 어려운 이름

사실 이 부분은 그렇게 크게 공감은 안된다. 저자의 주장은 개발자는 회의 등을 통해서 실제 클래스나 변수명을 말로써 설명해야할 때가 있는데 그 때 발음하기 어려운 이름이면 바보같기 때문에 이런 커뮤니케이션의 편의성을 위해 "발음하기 쉬운 이름을 사용해라"였다. 근데 뭐 .. 그렇게 중요한건진 모르겠다.

 

 

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

이건 사실 진짜 겪으면 귀찮은 상황인데, 만약에 내가 찾으려는 변수의 이름이 List<String> productNumbers3 이라고 가정하자. 운영 서버에 대해서 grep을 통해 로그를 확인한다고 하면 "grep productNumber3 server.log"를 쳐서 검색을 하려고 하는 경우에 문제가 생긴다. 만약productNumbers33이라는 녀석이 있으면 이 녀석들까지 무자비하게 찾아오게 되는 것이다. 이걸 해결하려면 정규표현식 써서 작업해야 하는데 아무 생각 없이 조회했다가 한번 더 해야 하는 일이 종종 생긴다.

[그림10]. 검색이 곤란해질 때

위 그림은 방금 설명한 예제에 대한 것은 아니지만 저런식으로 검색할 때 여러 부분에 중복되는 형태로 이름들이 많이 지어져있으면 유지보수할 때 은근히 힘들다. 예컨대 'contract'라는 메서드를 사용하는 곳에 대해 찾으려고 했는데 smartContract, secretContract등 온갖게 많으면 또 정규표현식 써서 찾아야 하는데 IDE가 없는 환경에선 그 어려움이 배가 될 수 있다.

결론: 근데 이건 고려하고 자시고 할 게 없는 거 같다. 일단 보류 ..

 

 

6. 너 분명히 기억 못하니까 i, j, k 같은 상징적인 변수명 말곤 갖잖은 변수명 쓰지 마라
사람의 기억력엔 한계가 있으니 제발 이상한 변수명 대충 지어놓고 기억한다는 소리 하지 말라는 내용이다. 예를 들어서 "akab123이라는 변수는 신용대출 고객들 중에 AML 대상이 아니면서 1원 이체를 수행하지 않은 고객의 암호화된 성명을 저장한 변수"라는 거를 기억할 수 있는게 아니라면 그렇게 하지 말라는게 저자의 주장이었다.

 

 

7. 클래스 이름과 메서드 이름

[그림11]. World라는 클래스 명은 모호해서 안된다고 피력하는 손웅정씨

결론: 클래스는 명사나 명사 구를 써야하고 변수, 메서드와 마찬가지로 모호한 이름은 피해라.

 

 

8. 너만 아는 밈으로 이름 짓지 마라

[그림12]. 변수명 이렇게 짓지 마라

결론: kill 대신 whack을 쓰는 경우처럼 특정 문화에서만 사용하는 농담으로 이름 짓는 것을 경계해라.

 

 

9. 비슷한 개념은 최대한 통일하되 서로 다른 로직은 확실히 구분하라

 

첫 째로, 비슷한 개념은 최대한 통일해야한다.

 조회 관련 클래스들의 조직도를 파악하는 일을 해야한다고 가정하자. 만약에 이 조회라는 개념에 대해 ["get", "fetch", "find", "retrieve"]등의 다양한 이름으로 변수명들이 작성되었다고 할 때 개발자는 위 네 상황뿐 아니라 다른 조회 관련 표현이 있는지도 찾아봐야 하고 현실적으로 찾지 못할수도 있다. 저자는 이를 방지하기 위해 비슷한 개념은 최대한 통일 하라는 당부를 했다.

 

둘 째로, 통일하되 로직이 다르면 확실히 구분해야한다.

(1) fun addEachValue(baseValue: Int, increment: Int): Int{ return baseValue + increment}
(2) fun addProductList(newProduct: Product): Unit{ ... productList.add(newProduct) }

위 두 코드를 보면 (1)의 add는 두 변수를 더하는거고 (2)의 add는 리스트의 값을 append 하는 역할을 한다. 물론 List의 메서드가 add이기 때문에 저쪽까진 건드릴 수없지만 두 메서드는 전혀 다른 역할을 하고 있기 때문에 (2)의 경우 appendProductList가 더 적합하다고 저자는 주장했다.

결론: 비슷한 역할을 하는 메서드의 prefix는 최대한 통일하되 add와 append와 같이 서로 다른 로직일 경우 이를 구분해 이름을 짓는 세심함이 필요하다.

 

 

10. 의미 있는 맥락을 추가하라

private void printGuessStatistics(char candidate, int count) {
    String number;
    String verb;
    String pluraModifier;
    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);
}

위 코드는 왜 맥락이 불분명하다고 하는걸까?

 

주요하게 사용되는 변수는 number, verb, pluralModifier인데 이 녀석들의 이름만 딱 봤을 땐 추상적인 표현이어서 맥락을 파악할 수가 없다. 만약 이 녀석들에 대해 좀 더 깊게 이해하려면 알고리즘을 다 분석해봐야 한다. 예를 들어 아래와 같다.

(1) 자 ... 어디보자 candidate? 따로 연산하는 건 없고 마지막 출력문에 영향을 주는 놈이고
(2) count는 문자열에 들어갈 요소를 분류하는 값이네
(3) There %s %s %s%s 구조인데, verb는 is, are 같은게 담길거고 pluralModifiers에는 아 .. 그니까 count에 따라 문장 요소 분류해서 출력하는 함수구나 !

이제 저자가 제안하는 개선된 코드를 확인해보자.

public class GuessStatisticsMessage {
    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(int count) {
        number = "1;
        verb = "is";
        pluralModifier = "";
    }
    private void thereAreNoLetters(int count) {
        number = "no";
        verb = "are";
        pluralModifier = "s";
    }
}
(1) 자 .. 보면 createPluralDependentMessageParts()를 통해 메시지 생성하나보네?
(2) 음 .. count 값에 따라 알맞는 문장 던져주는 구조인가보다!
.
.
끗 !
결론: 사실 예제를 따라 치면서도 좀 유난 아닌가 .. 싶었는데 복기하다 보니 이거 진짜 괜찮은 거 같다. 역시 전문가한테 배워야 혀 ~

 

 

11. 이 장을 마치며
좋은 이름 선택을 위해선 동료와 문화적 배경이 같아야한다.
이름 변경에 대해 타인의 질책같은 두려움이 있을 수 있지만
그렇다고 코드를 개선하려는 노력을 중단해서는 안된다.

- 로버트 씨 마틴 -

 

 

오늘 하루도 공부할 수 있어 크게 감사합니다. 2023-12-03

개발자 최찬혁