소프트웨어에서 이름은 어디에서든 사용합니다.
변수에도 이름을 붙이고, 함수, 인수, 클래스, 패키지 등에 이름을 붙입니다. 또한, 파일, 디렉터리, war, jar 등 여기저기 이름을 많이 붙이네요.
이렇듯 이름을 짓는 일은 많으며, 좋은 이름을 지으려면 시간이 걸리지만 좋은 이름으로 절약하는 시간이 더 많습니다.
좋은 이름을 지으면, 자신을 포함해 코드를 읽는 사람이 행복해 집니다.
이번 포스팅에서는 이름 잘 짓는 몇 개의 규칙에 대해 다뤄보려 합니다.
1. 의도를 분명히 밝혀라.
많은 개발자들이 "의도가 분명하게 이름을 지으라" 는 이야기를 많이 합니다.
의도가 분명한 이름은 정말로 중요합니다.
변수나 함수 클래스 이름은 다음과 같은 굵직한 질문에 모두 답해야 합니다.
- 왜 존재해야 하나?
- 수행 기능은?
- 사용 방법은?
따로 주석이 필요하다면, 의도를 제대로 나타내지 못함을 의미합니다.
이름 d 는 아무 의미도 드러나지 않습니다. 해당 변수가 표현하는 논리적인 이름이 필요합니다.
아래처럼 말이죠.
| int daysInAppraisalDeadline; | cs |
의도가 드러나는 이름을 사용하면, 코드 이해와 변경이 쉬워집니다.
한번, 다음 함수를 주목해주세요.
| public List<int[]> getThem() { ArrayList<int[]> list = new ArrayList<>(); for (int[] n : theList) { if (n[0] == 4) { list.add(n); } } return list; } | cs |
함수 function 은 복잡한 문장은 없어 보입니다. 하지만, 무슨 일을 하는지 짐작하기 어렵습니다.
문제는 코드의 단순함이 아닌 함축성에 있습니다. 코드 맥락이 명시적으로 드러나지 않습니다.
위에서 제공하는 코드는 아래와 같은 질문을 남깁니다.
1. theList 에 무엇이 들어있는가?
2. theList 에서 0번째 값이 어째서 중요한가?
3. 값 4는 무엇인가?
4. 함수가 반환하는 list 는 어떻게 사용되는가?
코드만 보고는 위 질문에 대한 답을 알 수 없습니다.
하지만, 다음과 같이 코드를 변경하면 어떨까요? 아래 코드는 단순함에 변화를 주지 않고, 이름만 변경했습니다.
| public List<int[]> getFlaggedCellS() { ArrayList<int[]> flaggedCellS = new ArrayList<>(); for (int[] cell : gameBoard) { if (cell[STATUS_VALUE] == FLAGGED) { flaggedCellS.add(cell); } } return flaggedCellS; } | cs |
이 코드를 보고, 위의 질문에 답을 해 볼 수 있습니다.
1. theList 에 무엇이 들어있는가?
-> gameBoard 로 변경되면서, 게임판임을 알 수 있습니다.
2. theList 에서 0번째 값이 어째서 중요한가?
-> 0 은 STATUS_VALUE 라는 값을 통해, 셀의 상태를 의미함을 알 수 있습니다.
3. 값 4는 무엇인가?
-> 4라는 상수에 FLAGGED 라는 이름을 붙임으로, 플래그가 된 상태임을 알 수 있습니다.
4. 함수가 반환하는 list 는 어떻게 사용되는가?
-> 반환하는 list 에 flaggedCellS 라는 이름을 붙임으로, 플래그 된 셀 목록을 구한다는 것을 알 수 있습니다.
각 개념에 이와 같이 이름만 붙여도 충분히 나아진 것을 알 수 있습니다.
단순함은 변하지 않았지만, 코드는 더욱 명확해졌습니다.
조금 더 리팩토링을 해볼 수 있을 것 같습니다.
int 배열 사용 대신 Cell 이라는 클래스를 만들고, Cell::isFlagged 라는 메소드를 사용하여 FLAGGED 라는 상수를 감춰도 좋을 것 같습니다.
| public List<Cell> getFlaggedCellS() { ArrayList<Cell> flaggedCellS = new ArrayList<>(); for (Cell cell : gameBoard) { if (cell.isFlagged) { flaggedCellS.add(cell); } } return flaggedCellS; } | cs |
2. 그릇된 정보를 피하라.
프로그래머는 코드에 그릇된 정보를 남기면 안됩니다.
그릇된 단서는 코드의 의미를 흐리기 때문이죠.
예를들어, 우리는 resumeList, appraisalList 와 같이 목록을 나타내는 변수명을 짓습니다.
프로그래머에게 있어 List 는 자료구조를 나타내는 특수한 의미이며, 해당 변수들이 List가 아니라면, 그릇된 정보를 제공하는 셈입니다.
간단하게 resumeS(s가 눈에 안뛸 수도 있기 때문에 대문자로 강조..), appraisalGroup 등으로 명명하는 것을 책에서는 추천하고 있습니다.
또한, 서로 흡사한 이름을 쓰지 않도록 주의할 것을 말합니다.
한 모듈에서는 getStringForHandlingOfResume 라 하고, 다른 곳에서 getStringForStorageOfResume 라 하면 두 모듈의 차이를 알 수 없습니다.
조금은 유머같지만, 알파벳 l, o 들의 문자들은 지양하는 것이 좋습니다.
| int i = l; int x = 1; int f = o; int fx = 0; | cs |
폰트나 IDE 에 따라, 위의 코드는 헷갈릴 수 있습니다.
3. 의미 있게 구분하라.
종종, 어떤 코드들은 이름에 연속된 숫자나 불용어(의미없는 글자)를 추가하는 경우가 있습니다.
이런 코드들은 모듈의 가독성을 꽤 나쁘게 만듭니다.
예를들어, 한번 아래 메소드 서명을 볼 수 있습니다.
연속적인 숫자를 덧붙인 이름(a1,a2... aN) 은 그릇된 정보를 제공하지도, 어떤 의도도 주지 못하는 이름입니다.
| public void copyPropertise(Object a1, Object a2) | cs |
위의 서명을 source, destination 등 의미있는 이름을 사용하는 것으로도, 충분히 어떻게 사용하면 되는지 나타낼 수 있습니다.
| public void copyPropertise(Object source, Object destination); | cs |
불용어를 사용하는 경우 역시, 어떤 정보를 주지 못하며 혼란을 줄 수 있습니다.
Product 라는 클래스가 있고 ProuductInfo, ProductData 라 부른다면, info 나 data 는 의미없는 정보입니다.
a, an, the 등의 접두어 역시 불용어입니다.
그렇다고, 접두어나 접미어의 사용이 나쁘다는 것이 아닙니다. product 라는 이름이 있다고, theProduct 라는 이름을 짓지 말자는 것입니다.
변수 이름에 variable 을 쓰거나, 테이블에 table 이라는 단어 역시 마찬가지로 불용어 입니다.
name 과 nameString 은 다를 것이 없습니다. name 이 숫자가 될 일은 없을테니까요...
4. 인터페이스와 구현 클래스
예를들어, 커피를 제작하는 Abstract Factory를 구현한다고 가정해보죠.
이 절과 관련은 없지만, Abstract Factory 가 무엇인지 궁금하다면, 이 곳을 참고! :-)
이 패턴에 따라 클래스를 그려보면, 인터페이스와 구현체로 나눠 제작하게 될 것입니다.
이 때, 인터페이스의 이름을 어떻게 할까요? ICoffeeFactory? CoffeeFactory?
책의 저자는 옛날 코드에서 많이 보이는 인터페이스에 붙이는 접두어(I)에 대하여, (잘해봤자) 주의를 흐트리고, (나쁘게는) 과도한 정보를 제공한다고 주장합니다.
굳이 다루는 클래스가 인터페이스라는 사실을 알리고 싶지 않고, 사용자는 CoffeeFactory 라고만 생각하면 좋겠다고 합니다.
책의 저자는 차라리 CoffeeFactoryImp 가 ICoffeeFactory 보단 좋다고 말합니다.
5. 클래스 이름
클래스 이름과 객체 이름은 명사나 명사구가 적합합니다.
Resume, Account 등이 좋은 예입니다. Data, Info 등과 같은 단어는 피하고 동사는 사용하지 않습니다.
6. 메소드 이름
메소드 이름은 동사나 동사구가 적합합니다.
postPayment, save, deletePage 등이 적합하며, javabean 규칙에 따라 접근자(getXXX), 변경자(setXXX), 조건자(isXXX) 를 사용하길 권장합니다.
생성자 같은 경우는 중복정의를 하기 보단 팩토리 메소드를 사용하길 권장합니다.
팩토리 메소드는 직접적으로 생성자를 이용하지 않고, 어떤 상태를 가진 객체를 생성하는 것을 의미합니다.
예를들어, 중복정의된 생성자 보단,
| Complex fulcrumPoint = new Complex(23.0); | cs |
아래와 같이, 의미를 가진 이름으로 표현된 팩토리 메소드를 이용하는 것이 좋습니다.
| Complex fulcrumPoint = Complex.FromRealNumber(23.0); | cs |
7. 한 개념에 한 단어를 사용하라.
추상적인 개념 하나에 단어 하나를 선택하고 이를 고수하기를 권장합니다.
예를들어, 똑같은 일을 수행하는 접근자 메소드를 클래스마다 get, fetch 등 제작기 부르면 혼란스러워 집니다.
메소드 이름은 독자적이고 일관적이어야 합니다. 그래야 주석을 뒤져보지 않고 프로그래머가 올바른 메소드를 선택할 수 있습니다.
마찬가지로 동일 코드 기반에서 controller, manager, driver 를 섞어쓰는 것을 지양합니다.
DeviceManager 와 DeviceController 는 근본적으로 다를까요? 둘 다 Controller 가 아닐지? 혹은 둘 다 Manager 가 아닐까요?
이름이 다르면 독자는 당연히 클래스도 다르고 타입도 다르다고 생각할 것입니다.
8. 해법 영역에서 가져온 이름을 사용하라.
코드를 읽는 사람 역시도 프로그래머입니다. 그렇기 때문에 전산용어, 패턴명, 알고리즘 명 등은 써도 무방합니다.
Factory 패턴에 익숙한 개발자는 CoffeeFactory 를 금방 이해하며, JobQueue 역시 특정 일을 하는 큐임을 인지할 수 있습니다.
9. 문제 영역에서 가져온 이름을 사용하라.
문제 영역에서 가져온 이름을 사용하면, 해당 분야의 전문가에게 의미를 물어 문제를 이해할 수 있습니다.
문제 영역의 개념과 깊은 문제를 해결하는 코드라면, 문제 영역에서 이름을 가져와야 합니다.
10. 의미있는 맥락을 추가하라.
스스로 의미가 분명한 이름이 없진 않습니다.
하지만 대다수 이름은 분명하지 못하며, 클래스, 함수 등에 공간을 넣어 맥락을 부여합니다.
모든 방법이 실패한다면, 마지막 수단으로 접두어를 사용합니다.
예를들어, 아래 코드를 확인해봅시다.
| public class TestClass { String firstName, lastName, streetName, city, state, zipCode; public static void main(String[] args) { } } | cs |
변수 명을 본다면, 해당 변수들이 주소를 나타낸다는 것을 알 수 있습니다.
하지만 어떤 메소드가 오직 state 하나만 사용한다면, 그 메소드만 보고 주소라는 사실을 바로 알긴 쉽지 않을 것 같아 보입니다.
그래서 다음과 같이 addr 이라는 접두어를 사용하여, addrState 라 하면 조금 더 의미가 명백해집니다.
| public class TestClass { String addrFirstName, addrLastName, addrStreetName, addrCity, addrState, addrZipCode; public static void main(String[] args) { } public String getAddrState() { return addrState; } } | cs |
하지만, Address 라는 클래스를 제작한다면 변수가 조금 더 큰 개념에 속한다는 것이 컴파일러에게도 명백해집니다.
| /** * 주소 클래스 * * Created by Doohyun on 2018. 1. 7.. */ public class Address { String firstName; String lastName; String streetName; String city; String state; String zipCode; } | cs |
11. 불필요한 맥락을 없애라.
사내에서 개발한 InJob 이라는 어플리케이션이 있었습니다.
저는 이 프로젝트에서 모든 유틸 클래스 및 기본이 되는 클래스들에 INJOB 이라는 접두어를 붙인 적이 있었습니다.
아래와 같이 말이죠...
| /** * inJob 에서 구현된 리소스 유틸 * * Created by Doohyun on 2018. 1. 7.. */ public class InJobResourceUtil { private InJobResourceUtil() {} } | cs |
어차피, InJob 이라는 안드로이드 프로젝트에서만 독릭접으로 사용될 코드이며, 이 클래스의 명은 ResourceUtil 로 변경하는 것이 옳다고 생각합니다.
비슷하지만, 조금은 다른 케이스로는 다음과 같은 사례를 볼 수 있습니다.
| /** * inJob 에서 사용하는 코드 정의 * * Created by Doohyun on 2018. 1. 7.. */ public class InJobCodeDefinition { public static class APPLICATION_TYPE { public static final String APP = "INJOB_APPLICATION_TYPE_APP"; public static final String WEB = "INJOB_APPLICATION_TYPE_WEB"; } } | cs |
위와 같은 경우 코드 클래스 자체가 다른 응용에서 사용할 수 있기 때문에, InJobCodeDefinition 이라는 이름을 사용하고 있습니다.
또한 코드의 value 역시 다른 솔루션과 구분을 짓기 위해 "INJOB_APPLICATION_TYPE_APP" 을 사용하지만, 클래스와 상수명에는 접두어가 없고 계층적인 구조를 사용하고 있습니다.
개념적으로 불필요한 맥락과 필요한 맥락을 나누고, 개념에 속하도록 제작이 된 좋은 사례인 듯 합니다. :-)
책의 저자는 위와 같은 규칙을 소개하며, 마치면서 다음과 같은 말을 했습니다.
사람들은 이름을 바꾸지 않으려는 이유는 다른 개발자가 반대할까 두려워서다.
여느 코드 개선 노력과 마찬가지로 이름 역시 나름대로 바꿨다가는 누군가 지책할지도 모른다.
그렇다고 코드를 개선하려는 노력을 중단해서는 안 된다.
이 문구를 읽었을 때, 3년차가 되면서 협업이라는 그늘 안에서 보수적이라는 두려움을 외면 시 했던 된 저를 보게 되었습니다.
남들이 안좋게 작성했기 때문에, 어쩔 수 없다는 핑계로 안좋은 코드를 이어갔었던 듯 합니다.
더불어 제가 코드를 작성하는 안좋은 버릇이 충분히 이 절에서 꽤 많이 보였습니다. 하하...
충분히 반성을 하게 된 포스팅인 듯 합니다.
이 글이 저처럼 조금 더 여러분들이 나아지는 것에 도움이 되었으면 좋겠습니다.