2024. 4. 5. 12:15ㆍ좋은 코드 (Good Code)/좋은 이름 (Good Name)
Easy Naming
이번 글은 조금 어려운 이야기를 하려고 한다.
내용 뿐만 아니라 마음도 살짝 어렵다. 이 글을 곡해하는 것보다 더 난감한 일이 없기 때문이다.
좋은 프로그래밍 책들을 보면 코드를 역할 또는 모듈 단위로 코드를 분리하라고 한다.
나는 이를 쉽게 생각해서 같은 장소에 있는 코드는 같은 세상에 있다고 해석한다.
즉, 같은 장소에 있는 코드가 갑자기 저 세상 코드가 되면 안되는 것이다.
이때 세상을 먼저 정의하면
이름 짓기가 하늘의 별따기 처럼 쉬워진다.
이름 읽기도 누워서 떡먹기 처럼 바뀐다.
그러나 내가 경계하는 건, 잘못된 세상을 만드는 것이다.
Our World
예를 들어 상품 원화 가격을 달러로 변환하는 코드를 개발한다고 하자.
fun convertWonToDollar(won: int, exchange_rate: float) -> float:
...
간단하다.
자, 이제 사용자가 다음과 같이 요구했다고 해보자
천원 단위로 입력할게요.
조금 당황했지만, 그래도 쉽게 이어나가려고 한다.
fun convertWonToDollar(won: int, exchange_rate: float) -> float:
...
fun convertThousandWonToDollar(thousand_won: int, exchange_rate: float) -> float:
...
자 이제 사용자가 다음과 같이 또 요구한다고 해보자
그냥 원 단위도 제가 알아서 입력할게요
자 이제 조금 심각한 고민에 빠진다.
이전에 작성했던 코드를 버리느냐, 아니면 수정하느냐 기로에 놓여있다.
나는 자신있게 말한다. 뭐가 되었든 부질없는 고민이다.
fun convertWonToDollar(won: int, exchange_rate: float) -> float:
...
fun convertThousandWonToDollar(thousand_won: int, exchange_rate: float) -> float:
...
fun convertThousandWonToDollarByWonUnit(won: int, won_unit: int, exchange_rate: float) -> float:
...
처음에는 간단했던 코드가 이제 조금 복잡한 코드로 바뀌었다.
잘못되었냐고? 아니다. 개발자는 나름대로 충분한 표현을 가지고 함수를 만들었다.
따라서 이름에 죄는 없다.
사용자 입장에서도 뭘 쓸지 구분할 수 있다.
그럼에도 저렇게 모아놓으면 어딘가 불편한 건 맞다.
그 이유는 코드들이 저 세상 코드임에도, 억지로 한 세상에 모여놓았기 때문이다.
이제 원인을 알았으니 해결책도 쉽다.
class WonToDollarExchanger:
fun convertWonToDollar(won: int, exchange_rate: float) -> float:
...
class ThousandWonToDollarExchanger:
fun convertWonToDollar(won: int, exchange_rate: float) -> float:
...
class UnitWonToDollarExchanger:
won_unit: int
fun convertWonToDollar(won: int, exchange_rate: float) -> float:
...
그냥 각자 다른 세상에 살게하면 된다.
내가 사용자 입장이라면, 이름도 쉽게 읽히며 그 자체로 이해하기 쉬울 것이다.
모두 convertWonToDollar 라는 이름을 가지고 있는데 왜 그럴까?
그건 바로 우리가 어린 아이일 때부터 보고 배운 정리 습관 때문이다.
냉장고에는 당연히 음식이 있지 옷이 걸려있지 않다.
반대로 옷장에는 옷이 걸려있지 음식이 걸려 있지 않을 것이다.
우리는 무의식적으로 옷의 세상, 냉장고의 세상 등으로 가두고 벗어나는 것들을 생각하지 않는다.
예를 들어
apple이 냉장고에 있습니다.
라고 말하면 사과가 냉장고에 있다고 생각한다.
apple이 옷장에 있습니다.
라고 말하면 애플 모양 티셔츠가 옷장에 있다고 생각한다.
다시 예제로 돌아와서, ThousandWonToDollarExchanger 라는 이름을 생각해보자
그냥 천원을 달러로 바꾸는 환율 변환기다.
이 클래스 안에 있는 코드들이 과연 만원을 달러로 바꾸는 일을 할까? (만일 그렇다면 해당 개발자에게 돌을 던져야 한다.)
결국 어떤 세상에 있는 코드는 너무 당연하게 그 세상에 맞춰 잘 돌아갈 것이라고 생각한다.
그리고 그렇게 만들어진 수 많은 세상을 연결하고, 조합하면 결국 구조가 만들어진다.
그렇다. 프로젝트 구조를 만드는 방법은 그 어떤 것보다 쉽다.
가장 작은 것부터 출발해서 하나씩 차곡차곡 올리는 것이다.
같은 이름이라도 다른 세상에 살면, 자연스럽게 그 세상에 어울리는 역할을 가지는 것이다.
Name Structure
코드가 구조를 가지면, 남부럽지 않은 프로젝트가 탄생한다.
다만 어려운 점은 어디까지 세상의 범주로 만들 지이다.
나는 극단적으로 깊게 만든 구조와 얕은 구조 모두 보았다.
얕은 구조야 일전에 보여준 것과 같고
fun convertWonToDollar(won: int, exchange_rate: float) -> float:
...
fun convertThousandWonToDollar(thousand_won: int, exchange_rate: float) -> float:
...
fun convertThousandWonToDollarByWonUnit(won: int, won_unit: int, exchange_rate: float) -> float:
...
깊은 구조는 아래처럼 만드는 것이다.
class Exchanger:
exchange_rate: float
fun convert(self) -> float:
...
class WonToDollarExchanger(Exchanger):
won: int
exchange_rate: float
fun convert(self) -> float:
...
class ThousandWonToDollarExchanger(Exchanger):
won: int
exchange_rate: float
fun convert(self) -> float:
...
class UnitWonToDollarExchanger(Exchanger):
won: int
exchange_rate: float
won_unit: int
fun convert(self) -> float:
...
차이가 보이는 가?
변하는 것과 변하지 않을 것을 함수에 어떻게 제공하느냐에 다르다.
모두 제공하면 전자처럼 되는 것이고, 아무 것도 제공하지 않으면 후자처럼 된다.
사실 뭐가 되었든 간에 두 방식 모두 잘못은 없다.
다만 코드를 아름답게 만드려면, 변하는 것은 함수에게 변하지 않을 것은 클래스에게 주는 게 좋다.
class Exchanger:
fun convert(self, won: int, exchange_rate: float) -> float:
...
class WonToDollarExchanger(Exchanger):
fun convert(self, won: int, exchange_rate: float) -> float:
...
class ThousandWonToDollarExchanger(Exchanger):
fun convert(self, won: int, exchange_rate: float) -> float:
...
class UnitWonToDollarExchanger(Exchanger):
won_unit: int
fun convert(self, won: int, exchange_rate: float) -> float:
...
그리고 어차피 환율을 줄꺼라면, dollar 이름를 굳이 명시할 필요가 없다.
사용자가 엔화 환율을 줘도 어차피 동일한 계산식을 통해 결과가 나올 것이기 때문이다.
class Exchanger:
fun convert(self, won: int, exchange_rate: float) -> float:
...
class WonExchanger(Exchanger):
fun convert(self, won: int, exchange_rate: float) -> float:
...
class ThousandWonExchanger(Exchanger):
fun convert(self, won: int, exchange_rate: float) -> float:
...
class UnitWonExchanger(Exchanger):
won_unit: int
fun convert(self, won: int, exchange_rate: float) -> float:
...
내가 생각한 이상적인 모습이다.
물론 UnitWonExchanger가 인기가 많을 것이니, 미래에는 해당 클래스만 살아남을 수 있다.
단, 전략 패턴을 너무 좋아하다보니 Exchanger를 인터페이스처럼 만들어봤는데 굳이 Exchanger를 상속 받지 않아도 괜찮다.
class WonExchanger:
fun convert(self, won: int, exchange_rate: float) -> float:
...
class ThousandWonExchanger:
fun convert(self, won: int, exchange_rate: float) -> float:
...
class UnitWonExchanger:
won_unit: int
fun convert(self, won: int, exchange_rate: float) -> float:
...
사용자 입장에서 읽고 이해하기 쉽게 코드를 만들 수 있다.
val won = ...
val exchange_rate = ...
won_exchanger = WonExchanger()
val exchanged_dollar: int = won_exchanger.convert(won=won, exchange_rate=exchange_rate)
Package Name
무수히 많은 코드를 리뷰해보니 사람은 무릇 짧은 이름을 좋아한다는 걸 느꼈다.
won_exchanger.convert(won=won, exchange_rate=exchange_rate)
보다는
exchanger.convert(won=won, exchange_rate=exchange_rate)
를 더 선호한다.
그러나 나는 여전히 전자로 써야 맞다고 생각한다.
exchanger만 쓰면 결국 이게 뭔지 거꾸로 올라가서 찾아보아야 하기 때문이다.
지금부터 할 이야기는 내가 글귀 처음에 말했던 경계를 넘을 수도, 안 넘을 수도 있는 이야기이다.
바로 이름에 대한 힌트를 패키지에 넘기는 것이다.
쉽게 말해 파일 경로이다.
# /price/won/exchange_service.py
...
exchanger.convert(won=won, exchange_rate=exchange_rate)
이렇듯 세상을 정의하는 가장 최상위 방법은 바로 패키지(파일 경로)이다.
won의 세상에서 환율 변환하는 대상이 원화가 아님을 상상하기 어렵다.
이렇게까지 해서 짧은 이름을 만드는 것을 달성한다면, 나름 동의한다.
물론 아래처럼 쓸 수도 있다.
# /price/won_exchange_service.py
...
won_exchanger.convert(won=won, exchange_rate=exchange_rate)
어쨌든 구조야 밑에서부터 차근차근 쌓아올리면 쉽게 만들 수 있지만,
그 구조가 지속 가능한지, 즉 시간이 많이 지나도 유지될 수 있는 지는 어떻게 쌓느냐에 따라 다르다.
만일 본인이 원화와 관련된 다른 서비스들을 만들꺼라면 전자처럼 만드는 것이 좋다.
하나 디렉터리에 서로 다른 코드 파일들을 쌓아두면 관리하기 어렵기 때문이다.
만일 본인이 엔화나 유로를 환산하는 서비스도 만들꺼라면 후자처럼 만드는 것이 좋다.
겨우 코드 파일 하나 달랑 만들자고 디렉터리를 그 수 만큼 늘리면 좋지 않기 때문이다.
결국 이런 저런 고민하다가 결국 폐쇄된 코드를 억지로 끄집어서 바꾸는 경우도 심심치 않게 목격한다.
이게 바로 내가 경계하는 부분이다.
파일 경로조차도 결국 세상이다.
하루 아침에 내가 살고있는 세상이 터져서 다른 세상으로 바뀐다고 해보자. 감당하겠는가?
내가 만든 처음 만든 구조가 싫든 좋든 어쨌든 변경에는 닫혀있어야 한다.
변화무쌍한 코드는 어느 누구도 좋아하지 않을 뿐더러 사용하지도 못한다.
Advices for Developers
이름 하나 잘 짓자고 이렇게까지 고민하고 설계해야 되나 싶을 수 있다.
(당연히 나에게도 하고 싶은 이야기이다)
근데 제발 고민해야 된다.
추상화 수준을 적절히 고민하고 올바른 이름과 구조를 그려봐야 한다.
그 결과가 당연히 자기주관적이므로 꼭 다른 사람들에게 공유하고 피드백을 들어야 한다.
만일 솔로 개발자라면, 자아를 여러 개 만들어서 누군가는 사용자 입장에서, 누군가는 제 3의 개발자로써 생각해야 한다.
이렇게까지 해도 완벽한 이름을 만들 수는 없다.
우리가 일상적으로 사용하는 String 클래스도 언어나 플랫폼에 따라 str, text, varchar 등 얼마나 다르게 부르는가.
중요한 건 아름다움이다.
많은 고민과 계획, 그리고 합치를 얻어낸 이름은 그 존재만으로도 너무 고맙고 사랑스럽다.
1년 전에 만든 이름도 - 오늘날에 다시 읽을 수 있다.
이름도 모르는 누군가가 만든 코드도 당연히 읽고 사용할 수 있다.
아, 얼마나 재미있는 세상인가.
Summary
- 이름에 구조를 만들자
- 그 구조에 어울리는 패키지 이름을 만들자.
'좋은 코드 (Good Code) > 좋은 이름 (Good Name)' 카테고리의 다른 글
10. 깔끔한 정리가 필요하다. (0) | 2024.07.03 |
---|---|
9. 이름은 하나다 (1) | 2024.04.10 |
7. 이름은 바뀌지 않는다. (0) | 2024.03.16 |
6. 쓰다가 마는 건 용납하지 못한다 (0) | 2024.03.03 |
5. 이름을 만들어주자 (0) | 2024.02.25 |