7. 이름은 바뀌지 않는다.

2024. 3. 16. 12:04좋은 코드 (Good Code)/좋은 이름 (Good Name)


Immutable

 

이름을 부르는 건 상황에 따라 다르다.

예를 들어 호칭을 생각해보자.

본인의 이름은 OOO 세 글자이다. 하지만,

  • 어머니한테는 아들로
  • 조카들에게는 삼촌으로
  • 학교에서는 학생으로

불린다.

 

이러한 호칭으로 불린다는 건, 상대방이 본인의 존재 의미를 너무나 잘 알고 있다는 것이다.

따라서 본인이 만든 이름에서도 이런 호칭을 사용할 수 있다.

문제는 이게 제 3자에게 약이 될 수도, 독이 될 수도 있는 양날의 검이라는 것이다.

 

 

 

 

 

 

 


Poison of Alias

 

먼저 호칭이 이 되는 경우를 소개하겠다.

누구든지 먹으면 죽는 것이다.

 

1. 이름을 본인 입맛에 맞추기

class ChromeDriver:
	...

class Chrome:
	...

web_view_driver = ChromeDriver()
web_view = Chrome(web_view_driver)

 

변수 이름을 이렇게 지은 이유는 다음과 같다.

ChromeDriver 객체를 웹페이지를 보는 목적으로 사용하기 위해

 

예를 들어 다음과 같이 썼다고 해보자.

web_view_driver = ChromeDriver()
web_view = Chrome(web_view_driver)
web_view.change_url("http://...")

 

본인에게는 너무 자연스럽게 좋아보인다.

 

 

 

그러나 이걸로 방금 제 3자에게 독을 먹였다.

 

생각해보자.

ChromeDriver를 개발한 사람이 왜 이름을 ChromeDriver라고 지었을까.

어렵게 생각할 필요도 없이 그냥 크롬의 드라이버이기 때문이다.

해당 클래스의 함수들도 크롬의 드라이버로써 존재할 것이다.

 

그럼 응당 아래처럼 써야 맞다.

chrome_driver = ChromeDriver()
chrome = Chrome(chrome_driver)

 

 

만일 개발자가 드라이버를 설치해주는 함수 install()을 만들었다고 하자.

즉, 이런 코드를 사용할 수 있는 것이다.

web_view_driver.install()

 

감히 상상할 수도 없는 한 줄이 탄생했다.

이 코드만 보고 뭘 설치해줄 지 감이 오는가?

 

더 심각한 건 개발자가 Chrome 클래스에 멤버 변수로 web_view를 만들었을 경우이다.

real_web_view = web_view.web_view

 

이제 어디부터 잘못되었는 지 깨닫게 될 것이다.

 

 

 

2. 어쩌다 되는 이름으로 바꾸기

자, 아래처럼 이름을 만들어 잘 사용하고 있다고 하자.

class ChromeDriver:
    ...
    
chrome_driver = ChromeDriver()

 

 

그런데 어느 순간 ChromeDriver가 몇 가지 설정만 바꾸면 Edge에서도 되는 걸 찾았다.

그래서 이름을 다음과 같이 지었다.

edge_driver = ChromeDriver(property_file_path="./edge.properties")

 

이제 본인은 이 변수를 평생 엣지의 드라이버로 사용할 것이다.

 

 

이 상황이 직면한 문제는 너무 명확하다.

크롬 드라이버에 있는 확장 프로그램 리스트를 가져올 수 있다고 해보자.

chrome_extensions = chrome_driver.extensions

 

이걸 엣지에도 써보자

edge_extensions = edge_driver.extensions

 

허허...

edge_extensions에는 어떤게 들어있을까?

애초에 가져올 수가 있을까?

그 전에 이 코드만 보는 우리는 결과가 크롬의 확장 프로그램이 나오면 이해할 수 있을까?

 

 

이런 경우에는 본인이 EdgeDriver를 문서를 뒤져서 찾든,

되는 걸로만 모아서 EdgeDriver 클래스를 새로 만들어야 한다.

class EdgeDriver:
    chrome_driver: ChromeDriver
    
    # extensions는 안 만든다.
    ...
    
edge_driver = EdgeDriver()

 

어쩌다 되는 이름 쓸 바에는 그냥 존재 자체의 이름 그대로를 사용하라는 이야기이다.

 

 

 

3. 똑같은 의미지만 다른 이름으로 만들기

이 경우는 옛날에 나도 자주했던 실수였다.

대표적인 예시로는 size, length, count 이다.

 

미안하지만 실제로 영어 사전에서 이 3개 단어가 어떻게 다른지는 관심이 없다.

문제는 다양한 언어에서 사용되는 이름이다.

 

[Python]

example_numbers = [1, 2, 3]
example_numbers_length = len(example_numbers)

 

[Kotlin]

val exampleNumbers = listOf(1, 2, 3)
val exampleNumbersSize = exampleNumbers.size

 

[SQL]

CREATE TABLE Example (
    exampleNumberCount integer
    ...
)

 

만약 본인이 이 3가지 중 평생 하나만 사용할 것이라고 하면, 해당 챕터는 무시해도 된다.

 

자 이제 본인에게 선택지가 주어진다.

size, length, count 중 어떤 걸 선택하겠는가?

 

사실 고만고만 하면 어떤 이름을 써도 이해는 된다.

예를 들어

println(f"Example Number Length : {example_numbers_size}")

 

라고 한들 어차피 숫자 개수을 출력할 것인건 누구나 알 것이다.

여기까지 보면 그냥 아무꺼나 골라서 써도 될 것 같다.

 

 

이제 독의 효능이 발휘될 것이다.

example_numbers = [1, 2, 3]
example_numbers_length = len(example_numbers)
println(f"Example Number Length : {example_numbers_length}")

additional_example_numbers = [4, 5, 6]
additional_example_numbers_size = len(additional_example_numbers)
example_numbers.extend(additional_example_numbers)

println(f"Example Number Length : {example_numbers_length + additional_example_numbers_size}")

 

맨 마지막 줄의 example_numbers_length + additional_example_numbers_size 부분을 보자.

분명 두 변수 모두 개수가 맞다. 근데 이름이 서로 length, size로 다르다.

근데 또 의미는 두 개가 비슷하니까 얼추 똑같이 썼겠거니 한다.
근데 의미가 똑같으면 이름도 똑같아야 정상 아닌가.
의미가 살짝 다른 건가 합리적 의구심이 든다.
그래도 윗 줄에 있는 코드를 보니 같은 의미구나 이해한다.

 

 

의미가 같으면 어쨌든 똑같은 이름을 써야한다.

어디서는 length고 어디서는 size로 불리는 호칭은 이제 제거해야 마땅하다.

이에 대해 경험적으로 조언을 해보자면,

  1. 모두 하나로 통일한다.
  2. 언어에 따라 하나로 통일한다.

두 가지 방법 중 하나를 쓰면 된다.

 

1번에 대해서 예를 들어 본인이 어떤 언어가 되었든 간에 size로 쓰기로 마음먹었다면, 아래처럼 쓰면 된다.

example_numbers = [1, 2, 3]
example_numbers_size = len(example_numbers)
println(f"Example Number Size : {example_numbers_size}")

additional_example_numbers = [4, 5, 6]
additional_example_numbers_size = len(additional_example_numbers)
example_numbers.extend(additional_example_numbers)

println(f"Example Number Size : {example_numbers_size + additional_example_numbers_size}")

 

팀 프로젝트라고 하면, 이를 컨벤션으로 만들어서 공유하면 좋다.

 

 

2번에 대해서는 아래처럼 쓰면 된다.

 

[Python]

example_numbers = [1, 2, 3]
example_numbers_length = len(example_numbers)
println(f"Example Number Length : {example_numbers_length}")

additional_example_numbers = [4, 5, 6]
additional_example_numbers_length = len(additional_example_numbers)
example_numbers.extend(additional_example_numbers)

println(f"Example Number Length : {example_numbers_length + additional_example_numbers_length}")

 

[Kotlin]

val exampleNumbers = listOf(1, 2, 3)
val exampleNumbersSize = exampleNumbers.size

 

현재 나는 2번을 선호하는 편이다.

 

하지만 자주 쓰는 1번 표현이 단 한 가지 있다. 바로 map이다.

 

[Python]

example_number_and_name_map: dict = {
    1: "hello",
    2: "world"
}

 

[Kotlin]

val exampleNumberAndNameMap: Map<Int, String> = mapOf(
    1 to "hello",
    2 to "world"
)

 

Map은 키 - 값 구조로 이해하고 있기 때문에 어딜 가든 쉽게 알아보기 위했다.

그러나 현재는 2번처럼 map 대신 dict 을 쓰는 걸 고려해보고 있다.

 

 

 

 


Benefit of Alias

 

이름을 곧이 곧대로 만들지 않으면 어떤 일이 벌어질 지 위협적인 말만 썼는데,

사실 호칭을 잘 쓰면 이보다 더 좋은 이름이 탄생할 수도 없다.

 

하지만 호칭을 쓰는 방법이 적잖이 모호해서, 잘 쓰기란 어렵다.

나는 프로그래밍을 적당히하는 것과 잘하는 것의 기준 중 하나가 바로 호칭을 잘 만드는 것이라고 생각한다.

 

 

1. 어차피 알거나 중복 이름은 제거해주기

예를 들어 다음과 같은 Map을 떠올려보자.

class Example:
    ...

val exampleKeyAndExampleMap: Map<String, Example> = mapOf(...)

 

물론 이런 이름은 좋다.

해당 이름은 자신이 누구인지 충실히 표현하고 있기 때문이다.

 

문제는 너무 길어질 때이다.

class ChapteredGoodNameExample:
    ...

val chapteredGoodNameExampleKeyAndChapteredGoodNameExampleMap = ...

 

이제는 마냥 좋다고 볼 수는 없다.

이게 뭔지 바로 알 수 있다는 장점보다는,

그 전까지 읽는 데 시간이 너무 많이 소요되고 변수 이름 하나가 너무 많은 자리를 차지한다.

이건 단점이다.

 

따라서 자기 자신을 map의 값으로 사용하는 경우에는 그냥 value 이름을 빼버리자고 약속하면,

And~ 가 이름에 없으면 value 자리에는 자기 자신이 온다고 바로 연상할 수 있다.

class ChapteredGoodNameExample:
    ...

val chapteredGoodNameExampleKeyMap: Map<String, ChapteredGoodNameExample> = ...

 

비교적 쉽게 읽힌다.

여기까지가 적당히 하는 방법이다.

 

이제 내가 생각했을 때 잘하는 방법은 다음과 같이 이름을 짓는 것이다.

class ChapteredGoodNameExample:
    ...

val exampleKeyMap: Map<String, ChapteredGoodNameExample> = ...

 

어차피 사용하는 Example이 ChapteredGoodNameExample 밖에 없다면, Example 라고 해도 충분히 읽힌다.

오히려 이름이 간결하기 때문에 이런 호칭으로 부르는 게 아름다울 지경이다.

 

만일 ChapteredBadNameExample 도 사용해야 한다고 해보자.

class ChapteredGoodNameExample:
    ...
    
class ChapteredBadNameExample:
    ...
    
val goodNameExampleKeyMap: Map<String, ChapteredGoodNameExample> = ...
val badNameExampleKeyMap: Map<String, ChapteredBadNameExample> = ...

 

이런 식으로 구분하는 것이다.

여전히 어차피 사용하는 Example이 모두 Chaptered이기 때문에 당연히 잘 읽힌다.

 

그러나 상황이 바뀜에 따라 이름을

exampleKeyMap >> goodNameExampleKeyMap 처럼 늘렸는데, 이건 사실 보기가 좋지는 않다.

IDE가 발전해서 이름을 바꾸면 자동으로 다른 이름들도 바꿔준다고는 하지만,

어디에서 파셜(partial)된 건지 모르기 때문에 언제나 보수적으로 이름은 고정해야 좋다.

이 지점이 바로 호칭을 쓰는 방법이 적잖이 모호하다는 것이다.

 

그리고 나는 여전히 이름은 보수적으로 가져가야 한다는 쪽이다.

하지만 이렇게 지으면 결국 이름들이 서로 모호해지는 순간이 오는데, 위에서 설명한 good vs bad 예시와 같다.

이럴때는 보수적인 틀을 깨고 기존 코드의 이름을 바꿔놓는다.

여전히 나는 자신이 소중히 만든 코드를 남들이 쉽고 재미있게 읽어주길 바라는 마음이 크기 때문이다.

 

 

 

2. 이름을 제대로 써주기

마지막으로 최근에서야 발견한건데, 우선 다음과 같은 코드를 보자.

val mysqlConnection: MysqlConnection = mysqlConnector.connect(...)
mysqlConnector.reConnect(mysqlConnection)

 

너무 자연스럽게 작성된 거라 이해가 단번에 되었다.

 

근데 여기에는 엄청난(?) 배려가 있었는데 바로 mysqlConnector 변수의 탄생이다. 

val mysqlConnector = MysqlConnection(...)

 

분명히 짚고 넘어가면 ConnectionConnector는 서로 다르다.

Connector는 Connection을 만드는 역할을 한다.

예시가 MySQL이니까 달리 말하면 Connector가 연결 정보를 받고 데이터베이스에 연결을 수행하는 것이고

그 결과로 Connection을 반환해서, 이 Connection으로 데이터 통신을 하는 게 상식적이다.

 

만일 Connection이 스스로 연결도 하고, 이후에 데이터 통신도 하면 결국 단일 책임 원칙을 위반하는 꼴이다.

 

이건 MysqlConnection를 개발한 사람의 잘못이다.

물론 사용하는 사람도 싫겠지만서도 인터페이스 분리를 먼저 하는 게 맞겠지만,

mysqlConnector로 이름을 사용한 사람은 죄가 없다. 오히려 칭찬해야 한다.

결국 중요한 건 제 3자가 오해없이 잘 읽느냐이다.

 

이름을 그대로 짓는 것을 다 떠나서, 이름이 자기 자신의 역할을 충분히 표현하는 것이 가장 우선이다.

 

 

 

 

 


Summary

 

  • 다음과 같은 호칭은 피한다.
    1. 이름을 본인 입맛에 맞추기
    2. 어쩌다 되는 이름으로 바꾸기
    3. 같은 의미지만 다른 이름으로 만들기
  • 다음과 같은 호칭은 권장한다.
    1. 어차피 알거나 중복 이름은 제거해주기
    2. 이름을 제대로 써주기