처음 프로그래밍을 배우면 문법을 잘 지키는 것만으로도 벅찹니다. 열심히 코드를 쳤는데 console창에 뜨는 무수한 에러를 보면 한숨만 나옵니다.
하지만 점점 프로그래밍에 익숙해지면 요구사항에 맞게 코딩하는 거는 어느 정도 할 수 있게 되지만, 이게 끝이 아닙니다.
더 좋은 프로그래머가 되려면 코드를 단순히 옳바르게 쓰는 거뿐만 아닌, 읽기 쉽게 써야 합니다. 이런 읽기 쉬운 코드를 흔히 Clean Code, 즉 깨끗한 코드라 부릅니다.
Clean Code가 왜 중요하고, 어떻게 하면 Clean Code를 쓸 수 있을까요?
Clean Code는 왜 중요할까?
Clean Code가 중요한 이유는 내가 쓴 코드는 한 번 쓰고 끝이 아니라, 계속 살아 숨쉬기 때문입니다. 즉, 내가 쓴 코드는 나를 포함한 누군가가 언젠가 읽고 수정해야 하는데, 코드가 읽기 힘들면 유지보수에 시간이 많이 걸립니다.
Clean Code를 쓰기 위한 기본원칙
Clean Code가 중요한거는 직관적으로 와닿지만, Clean Code를 쓰는 게 마냥 쉽지만은 않습니다.
우선, 어떤게 Clean Code냐는 사람마다 생각이 다를 수 있습니다. 예를들어, 주석(Comment)에 관한 의견도 여러 가지가 있습니다.
- 주석도 결국 누군가가 유지보수해야 하는데 현실적으로 쉽지 않아. 주석은 최대한 적게 사용하고 주석 없이도 읽기 쉬운 코드를 쓰는 게 중요해
- 주석이 있으면 이 코드가 어떤 코드인지 미리 알 수 있고, 다른 여러 정보도 전달할 수 있어서 코드를 읽기가 더 쉬워져
여기서 핵심은, 2가지 입장 모두 나름대로 일리가 있다는 것입니다.
또, Clean Code를 쓰기 위해서는 시간을 들여야 하는데, 현실적으로 모든 프로젝트의 무한정의 시간이 주어진게 아니기 때문에 현실과 어느 정도 타협을 해야 합니다.
그럼에도 지금 당장 실천할 수 있는 어느정도 합의된 Clean Code의 원칙이 있는 거 같아 간단하게 소개하고 싶습니다.
원칙 1 - 변수 이름 잘 짓기
아래와 같이 누군가가 변수 이름을 지었습니다.
int d; // elasped time in days
d라는 이름의 int 변수에, 며칠이 지났는지를 저장하겠다는, 언뜻 아무 문제 없어 보이는 코드입니다.
하지만, 조금 더 밑에 아래와 같이 코드가 되어 있으면 어떨까요?
int v = d + e / 2;
암호 같지 않나요? 기억력이 엄청 좋지 않은 이상 많은 프로그래머들이 위로 올라가 주석을 보고 d의 의미를 다시 상기하고 다시 밑으로 내려와 읽어야 됩니다. 시간이 많이 걸릴 뿐만 아니라, 누군가가 d의 의미를 오해해서 잘못 수정한다고 생각하면 상상만으로 끔찍합니다.
변수의 이름은 그 의미가 잘 전달되야 합니다. 의미 없는 기호나 불필요한 줄임말은 최대한 안 쓰는 게 좋습니다.
int elapsedTimeInDays;
훨씬 깔끔하지 않나요? 변수 이름을 주석으로 설명해야 될 거 같다면 잘못 지은 이름인겁니다.
원칙 2 - 코드가 너무 길어지면 함수, 객체 등으로 '잘' 나누기
읽기 쉬운 책은 책의 목적을 정확히 하고, 챕터 별로 책의 전체적인 목적을 달성하기 위해 명확한 주제로 나눕니다. 읽기 쉬운 코드도 프로그램의 목적을 달성하기 위해 객체나 함수로 적절히 나눠야 합니다.
함수를 나누는 핵심은 1개의 함수는 1개의 목적을 달성해야 한다는 것입니다. 함수가 여러 개의 목적을 한 번에 달성하면, 재사용성이 낮고, dependency가 생길 뿐만 아니라, 하나의 함수를 이해할 때 읽어야 할 부분이 많아 읽기도 힘듭니다.
또, 매개변수는 가능한 한 적어야 합니다. 매개변수가 많으면 여러 단점이 있습니다.
- 여러 매개변수를 기억해야 해서 코드를 쓰고 읽기 힘들다
- 고려해야 할 테스트 케이스가 많아서 테스트하기가 힘들다
- 매개변수의 순서를 기억하기 힘들다
원칙 3 - 코드를 예쁘게 배치하기
코드를 배치하는 게 뭐가 중요한가 싶겠지만, Clean Code의 핵심은 읽기 쉬운 코드를 만드는 것이기 때문에 읽기 쉽게 코드를 배치하는 것도 중요합니다. 똑같은 글도 문단도 나뉘어 있고, 머리말/본문/결론으로 잘 나누어져 있으면 읽기 편하듯, 코드도 마찬가지입니다.
- 가로배치
일단, 가로로 너무 길지 않은 게 중요합니다. 아래와 같이 가로로 너무 긴 코드가 있다면 쉽게 읽기가 힘듭니다.
String answer = Arrays.stream(numbers).mapToObj(String::valueOf).sorted((a, b) -> (b + a).compareTo(a + b)).collect(Collectors.joining());
이럴 때는 똑같은 코드라도 줄을 띄어서 쓰는 게 읽기 훨씬 편할 겁니다.
String answer = Arrays.stream(numbers)
.mapToObj(String::valueOf)
.sorted((a, b) -> (b + a).compareTo(a + b))
.collect(Collectors.joining());
어느 정도 길면 줄을 띄어야 하는지에 관한 법칙이 있는 건 아니지만, 아래에서 소개할 Clean Code란 책에서는 120자가 넘으면 띄라고 제안했습니다.
이것뿐만 아니라 서로 연관된 코드는 붙여놓고, 서로 관련 없는 코드는 떨어트려 놓으면 읽기 더 쉽습니다.
if(arr.length == 1 || arr.length == 0) {
return new int[] {-1};
}
int min = arr[0];
for(int num : arr) {
if(min > num) {
min = num;
}
}
boolean flag = true;
int[] result = new int[arr.length - 1];
int count = 0;
for(int num : arr) {
if(num == min && flag) {
flag = false;
continue;
}
result[++count] = num;
}
return result;
위의 코드는 서로 연관된 로직끼리는 가로로 붙어있고, 다른 로직을 띄었더니 훨씬 더 읽기 쉽게 됐습니다.
- 새로 배치
코드의 새로 배치는 가로에 비해서는 조금 어려울 수 있지만, 기본 원리는 똑같습니다. 글에도 처음에는 이 글이 어떤 글인지 대략적인 소개를 하고, 점점 읽어 내려갈수록 자세히 설명하는 것처럼, 코드도 가장 위에 있는 함수는 알고리즘을 높은 수준에서 기술하고, 아래에 있는 함수에 세부적인 구현을 하는 식으로 작성하면 더 읽기 쉽습니다.
Clean Code에 관한 책
Clean Code를 배우기 위해서 가장 유명한 책은 로버트 C. 마틴이 지은 Clean Code라는 책입니다. 프로그래머의 필독서라고 알려져 있고, 저도 이 책을 통해 처음 Clean Code라는 개념을 접했습니다.
실질적인 예시와 함께 Clean Code를 쓰기 위해서는 어떻게 해야 하는지 자세히 알려주는 교과서 같은 책입니다. 꼭 한 번은 읽어 보시길 추천드립니다.
이 책 외에도 추천하는 책은 프로그래머의 뇌라는 책입니다. 이 책은 심리학적으로 읽기 쉬운 코드란 무엇이고, 프로그래머가 새로운 언어나 개념을 더 잘 공부하기 위해서는 어떻게 해야 하는지 과학적으로 알려줍니다.
Clean Code를 쓰기 위한 개인적인 원칙
이렇게 책을 읽고 Clean Code에 대해 생각해도, 직접 실천하지 않으면 의미가 없습니다.
그래서 Clean Code란 책을 읽고 감명받아 실제로 개인 프로젝트를 할 때나 코딩 테스트를 풀 때 Clean Code를 적용해 보려 했습니다.
여러 가지 시행착오를 겪으면서 저는 개인적으로 2가지만은 꼭 하고 있습니다.
1. 변수 이름이 길어지더라도 대충 무슨 변수인지 알 수 있도록 하기
2. 코드를 다 짜고 나서 항상 Refactoring하기
Refactoring은 코드의 작동을 변경하지 않고 코드의 구조만 바꾸는 것을 말합니다.
이상적으로는 처음 코드를 짤 때부터 완벽한 Clean Code로 짜는 게 좋겠지만, 현실적으로 요구사항을 분석해 제대로 작동하는 코드를 짜는 것만으로도 많은 생각을 해야 하기 때문에 Clean Code에 100% 에네지를 쏟기는 쉽지 않습니다.
그래서 저는 코드를 다 짜고 항상 제가 짠 코드를 보며 더 읽기 쉽게 할 수는 없을까 고민해 보고 다듬습니다.
이렇게 말하면 대단한 거를 하는 것처럼 보이지만, 변수 이름을 더 쉽게 바꾼다거나, 반복되는 부분을 함수로 만든다거나, 제가 직접 짠 로직에 대응하는 프로그래밍 언어의 내장 함수를 찾아보고 이로 대체한다거나 하는 정도입니다.
예를 들어 아직 Java의 HashMap 사용이 익숙하지 않을 때 HashMap에 key에 대응하는 값이 있으면 그 값을 1 더하고, 아니면 새로 값을 넣는 코드를 아래와 같이 만들었습니다.
Map<String, Integer> count = new HashMap<>();
if(count.get(key) == null) {
count.put(key, 1);
} else {
Integer tmp = count.get(key);
count.put(key, tmp + 1);
}
당연하지만, 위의 코드도 동작상으로는 문제가 없습니다. 하지만 간단한 동작인데 코드가 너무 길고, 또 변수 이름이 직관적이지 않다고 생각했습니다. 그래서 우선 변수 이름을 제 나름대로 더 가독성이 좋은 이름으로 바꿨습니다.
Map<String, Integer> countMap = new HashMap<>();
if(countMap.get(newKey) == null) {
countMap.put(newKey, 1);
} else {
Integer existingNewKeyValue = countMap.get(newKey);
countMap.put(newKey, existingNewKeyValue + 1);
}
그 전보다 변수들의 길이는 길어졌지만 읽기는 더 쉬워졌지 않나요?
하지만 여기서 끝이 아니라 key에 대응하는 값이 있으면 그 값을 1 더하고, 아니면 새로 값을 넣는 동작은 자주 있는 상황일 거 같아 Java의 HashMap에 내장함수로 있지 않을까 싶어 찾아봤습니다.
그랬더니 putIfAbsent() 라는 key에 대응하는 value가 없을 때만 넣는 내장 함수도 있었고, 반대로 putIfPresent()도 있었습니다. 그 중에서도 제 목적에 맞는 방식으로 merge()라는 내장 함수를 사용할 수 있을 거 같아 사용법을 익히고 아래와 같이 Refactoring 했습니다.
Map<String, Integer> countMap = new HashMap<>();
countMap.merge(newKey, 1, Integer::sum);
이 코드는 처음 짠 코드와 정확히 같은 동작을 합니다!
이제 누군가가 제 코드를 읽거나 수정할 때 훨씬 쉽게 할 수 있을 뿐만 아니라, 이 코드를 보고 제 자신도 굉장히 뿌듯했습니다. 이게 Clean Code의 힘이 아닐까요?
'👨💻 프로그래밍' 카테고리의 다른 글
깃허브 코파일럿 (Github Copilot) 1달 사용 후기 (0) | 2023.05.17 |
---|---|
여러가지 프로그래밍 패러다임 알아보기 (명령형, 함수형, 논리형 등) (0) | 2023.04.08 |
프로그래밍을 할려면 영어를 잘해야 할까? (0) | 2021.08.01 |
방구석에서 스탠퍼드 대학 컴퓨터 공학 학위 따는 법 (0) | 2021.06.21 |
코딩 스터디 나가고 느낀 점 (0) | 2021.05.27 |