
회사에서 모처럼 북 스터디가 생겨서 clean code를 읽게 됐다
그것도 clean code 2판!
2판은 아직 번역본이 출간되지 않아서 O'reilly에서 원서를 읽기로 했다
고등학교 3년, 대학년 4년 동안 원서로 전공 공부를 해왔기에, 다시 원서를 읽을 수 있는 기회가 생겨서 좋았다!
원서 공부의 장점은...
번역본은 생각보다 오역이 종종 있고, '그래서 이게 무슨 의도로 말한거지' 싶은 문장들이 있다
그래서, 번역본을 읽을 땐 원서를 항상 옆에 두고, 그 문장을 다시 찾아서 '아 원래 이런 의도였군'하고 이해 했었다.
그래도 안되면 gpt 돌려서 이해 될때까지 질문하면 된다.
gpt와 함께라면 두렵지 않아!
Part1: Code
Chapter2. Clean That Code!
First, make it work. Then, make it right.
- Kent Beck
결국은 완벽한 코드를 처음부터 짤 수 없으니, 일단 짜라는 뜻이다.
그 다음에 고치면 된다.
종근님께서 늘 내게 하셨던 말씀이기도 하다.
일단 짜! 일단 돌아가게 만들어! 그리고 고쳐!
코드를 작성하는 것도 결국 작문의 영역이라고 생각하면,
우리가 글을 쓸 때 초안을 완벽하게 쓰지 못하는 것과 같다.
내가 대학 시절 신문 기자로 일할 때도,
초안을 쓰는데는 1주일 미만의 시간이 걸렸지만 퇴고가 끝나는 데는 2주가 넘게 걸렸다.
이는 물론 다른 기자들의 퇴고를 기다리는 시간, 그 퇴고를 반영하는 시간, 그리고 그 퇴고본을 다시 퇴고하는 시간...을 포함한다.
그런데, 코드도 마찬가지 아닐까?
코드 초안을 올리면, 동료 개발자들의 pr 리뷰를 기다린다.
그리고, 그 리뷰를 반영해서 다시 리뷰 요청을 걸고, 기다린다.
결국 처음부터 완벽할 수는 없다.
This is how we humans work.
We produce something fairly ugly at first, and then we massage and manipulate it until it is better.
인간의 일하는 방식이 '처음에 못생긴 것을 일단 만들고, 더 나아질 때까지 다듬는 것'이라고 표현한 게 웃겼다.
예제: 로마 숫자 to 정수 변환기
저자는 Copilot을 이리저리 돌려서 로마 숫자를 정수로 변환하는 변환기를 만들었다.
roman = roman.replace("IV", "4");
roman = roman.replace("IX", "9");
roman = roman.replace("XL", "F");
roman = roman.replace("XC", "N");
// ...
case '4' -> numbers[i] = 4;
case 'F' -> numbers[i] = 40;
로마 문자를 자체적으로 정의한 문자열로 치환한다.
switch-case문이 줄줄이 늘어져 있으니 '그래서 이게 뭔데?' 싶다.
저자는 코드를 작성하는 데 걸린 시간과 거의 비슷한 시간을 들여서 코드를 청소했다.
private int doConversion() {
checkInitialSyntax();
convertLettersToNumbers();
checkNumbersInDecreasingOrder();
return numbers.stream().reduce(0, Integer::sum);
}
각 코드를 쪼개서 한 함수가 더 작은 역할을 담당하게 만든다.
1. 짧고 잘 명명된 함수
2. 잘 명명된 인스턴스 변수
3. 함수들이 호출 순서대로 나열됨
이 원칙을 지켜 더 깨끗한 코드를 만들었고, top-down으로 읽을 수 있어 가독성이 좋다고 말했다.
그런데 저자는 여기에서 또 하나의 관점을 제시한다.
내가 이해하고 있다는 사실 자체가, 다른 사람을 위한 설명을 망친다.
알고리즘을 완전히 이해한 상태에서 변수명이나 주석을 작성하면,
무의식적으로 '이 정도면 알겠지'라고 가정하게 된다.
하지만 처음 보는 사람(코드 리뷰어라던가)은 그 맥락이 없어 이상하게 느낄 수 있다.
일종의 딜레마인데,
알고리즘을 모르면 -> 이름을 못 짓고
알고리즘을 알면 -> 이름이 편향된다.
그래서 어떻게 해야 하는가?
1. 인정하고 주의하기
- 완벽한 이름은 없다는 걸 받아들인다.
- '지금 내가 너무 많이 알고 있어서, 이게 정답으로 보이나?'라고 의식적으로 생각한다.
2. 시간 두고 다시 보기
- 저자는 2달 후 자신의 코드를 다시 읽고 코멘트를 남겼다.
- 맥락을 까먹은 상태에서 다시 보면 진짜 명확한 변수명인지 알 수 있다.
3. 코드 리뷰 활용하기
- 다른 사람이 '이게 뭐에요?'라고 의문을 가지면, 방어하지 말고 열린 사고를 가진다.
- (왜냐하면, 아예 모르는 사람의 눈으로 본 게 더 명확할 수 있으니까!)
결국 clean code에 대한 '단일 표준'은 없고, 각 개발자 팀의 '독자적인 표준'을 만들어 그걸 따르면 된다는 것이다.
그런데, 이 '코드 표준'을 위한 문서를 작성하지는 말라고 말한다.
정해진 코드 표준을 따르는 코드가 곧 문서가 될 것이고,
코드 외부에 문서가 생기는 경우는 코드가 표준을 따르지 않는 이유를 설명하기 위함일 것이라고!
뭔가 작년에 네이버 인턴할 때 사수님이 해주셨던 말씀도 기억난다.
주석 달지 말고 코드 자체로 의도를 설명할 수 있도록 clear하게 작성하라고 하셨었다.
pr 1개에 리뷰 5번 넘게 했던 추억...
The Cleaning Process
저자의 코드 cleaning 과정은 아래와 같다
Step0: 여러 시도와 실패
저자는 처음에 State Machine 방식을 시도했다.
하지만, 좋은 방법이 아니라는 걸 깨닫고 버렸다.
좋은 아이디어를 많이 가지는 비결은, 많은 아이디어를 시도하고 나쁜 것들을 버리는 것이다
- Linus Pauling (미국의 화학자, 노벨상 두 번 받은 사람 ㄷㄷ)
Step1: 일단 작동하는 못생긴 코드를 짠다
위에서 언급한 치환식과 switch-case 문이 담긴 코드이다.
// 핵심 아이디어: IV, IX 같은 2글자 조합을 1글자로 치환
roman = roman.replace("IV", "4"); // 4
roman = roman.replace("IX", "9"); // 9
roman = roman.replace("XL", "F"); // 40 (왜 F? 그냥 아무 문자)
roman = roman.replace("XC", "N"); // 90
roman = roman.replace("CD", "G"); // 400
roman = roman.replace("CM", "O"); // 900
step2: Low-hanging fruit
나무 아래쪽이 달린 과일부터 따는 것처럼, '쉬운 것부터' 시작한다.
기존 코드는 모든 로직이 한 덩어리로 묶여있었다.
public static int convert(String roman) {
// 잘못된 조합 체크 (VIV, IVI 등)
if (roman.contains("VIV") ||
roman.contains("IVI") || ...) {
throw new InvalidRomanNumeralException(roman);
}
// digraph 치환
roman = roman.replace("IV", "4");
roman = roman.replace("IX", "9");
// ... 그 외 더 많은 로직들
return Arrays.stream(numbers).sum();
}
이 덩어리 안에서 각 작동부를 떼내어 독립 함수로 분리한다.
public static int convert(String roman) {
checkForIllegalPrefixCombinations(roman); // 잘못된 조합 체크
checkForImproperRepetitions(roman); // digraph 치환
// 뭐 이런 식으로...
}
step3: 인수 전달이 지저분하다 -> 객체로 변환한다
// roman을 계속 넘겨야 함
checkForIllegalPrefixCombinations(roman);
checkForImproperRepetitions(roman);
checkNumbersAreInOrder(numbers, roman); // 에러 메시지용으로 roman도 필요
동일한 인수 전달을 계속 해야해서, 이를 해결하기 위해 클래스로 만들어서 인스턴스 변수로 공유한다
public class FromRoman {
private String roman; // 공유!
private List<Integer> numbers; // 공유!
public FromRoman(String roman) {
this.roman = roman;
}
public static int convert(String roman) {
return new FromRoman(roman).doConversion();
}
private int doConversion() {
checkForIllegalPrefixCombinations(); // roman 안 넘겨도 됨
checkForImproperRepetitions();
convertToNumbers();
checkNumbersAreInOrder();
return numbers.stream().reduce(0, Integer::sum);
}
}
step4: 직관적인 코드 작성하기
roman = roman.replace("IV", "4");
roman = roman.replace("XL", "F"); // F가 40이라고?
// ...
case 'F' -> numbers[i] = 40; // 여기서야 알 수 있음
F가 40을 의미하는 건 코드 두 군데를 봐야 알 수 있다.
직관적이지 않다.
이를 해결하기 위해 digraph와 숫자 변환을 한 함수에서 처리한다.
private void convertLettersToNumbers() {
for (int i = 0; i < chars.length; i++) {
char nextChar = (i + 1 < chars.length) ? chars[i + 1] : 0;
switch (chars[i]) {
case 'I' -> {
// IV나 IX면 한 번에 처리
if (nextChar == 'V' || nextChar == 'X') {
numbers.add(values.get(nextChar) - 1);
i++; // 다음 문자 건너뛰기
} else {
numbers.add(1);
}
}
// ...
}
}
}
Rule of thumb: if you think something is clever and sophisticated, beware—it is probably self-indulgence
- The Design of Everyday Things (Donald Norman, UX/디자인 분야의 거장)
'와 이 로직 기발한데?'라는 생각이 들면 그건 남이 읽기 힘든 코드일 수 있다.
이 원문 자체를 한국 정서로는 확 와닿는 표현으로 바꾸기 힘드네요;
아무튼 기교 부려서 짜지 말고 기발한 코드보다 뻔한 코드를 짜는 게 좋다는? 뜻인 것 같습니다.
Step5: 중복 제거 & 매직넘버 제거
기존 코드는 같은 로직 코드가 반복된다.
그리고, -1, -10같은 숫자 값이 직접적으로 박혀있다. (매직넘버)
case 'I' -> {
if (nextChar == 'V' || nextChar == 'X') {
numbers.add(nextValue - 1); // 매직 넘버 1
i++;
} else numbers.add(1);
}
case 'X' -> {
if (nextChar == 'L' || nextChar == 'C') {
numbers.add(nextValue - 10); // 매직 넘버 10
i++;
} else numbers.add(10);
}
공통 로직을 추출해서 코드 반복을 줄인다.
그리고, 숫자에 이름을 붙여서 상수로 만든다.
case 'I' -> addValueConsideringPrefix('V', 'X');
case 'X' -> addValueConsideringPrefix('L', 'C');
case 'C' -> addValueConsideringPrefix('D', 'M');
private void addValueConsideringPrefix(char p1, char p2) {
if (nextChar == p1 || nextChar == p2) {
numbers.add(nextValue - value); // value는 인스턴스 변수
charIx++;
} else {
numbers.add(value);
}
}
그러니까, step0~5를 정리하자면 아래와 같다.
1. 테스트 통과 유지: 매 단계마다 테스트가 통과하는 상태를 유지한다.
2. 작은 단계: 한 번에 하나씩만 변경한다.
3. 쉬운 것부터: Low-hanging fruit
4. 과감히 버리기: 안되는 접근법은 빨리 버리기
번외: Grok3에 넣고 개선 요청함
저자가 25년 2월에 출판 준비를 마치고, 이 코드를 Grok3에 넣고 개선해달라고 했다고 한다.
정규식을 써서 로마 숫자 패턴을 정의하는 코드를 작성해줬고,
알고리즘을 깔끔하게 개선해줬다.
private static final String REGEX =
"^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$";
그럼, 그냥 Grok3으로 딸깍하면 되는 걸 뭐하러 clean code하고 test하고 개선했냐?라고 말할 수 있다.
하지만 코드를 최종 결정하는 건 LLM/AI가 아니라 '개발자'인 자신이다.
AI가 좋은 코드를 작성하는지 알기 위해서는 내가 결국 계속 공부해야 한다.
유능한 자동화 코드 생산기가 더 좋은 작업을 할 수 있도록 개선을 요청하고,
잘못된 생산물을 치워서 최종적으로 좋은 결과물을 내는 것은 감독관의 역할이다.
결국, AI가 존재하더라도, 우리는 계속 공부해야 한다.
빠르게 성장하는 AI를 잘 활용해서 생산량을 높이기 위해서는 결국 내가 AI의 고삐를 단단히 잘 잡을 수 있어야 하니까...
결론은 공부해야 한다!
일단 이렇게 2장 내용은 끝!
다음엔 3장으로 만나요~
작성 시작: 2025.12.16. 07:41
작성 종료: 2025.12.16. 09:33
'개발자 강화 > 개발 독서' 카테고리의 다른 글
| [Clean Code 2판] 1-7: Clean Functions (1) | 2026.01.06 |
|---|---|
| [Clean Code 2판] 1-6: Formatting (1) | 2025.12.25 |
| [Clean Code 2판] 1-5: Comments (0) | 2025.12.21 |
| [Clean Code 2판] 1-4: Meaningful Names (0) | 2025.12.19 |
| [Clean Code 2판] 1-3: First Principles (1) | 2025.12.18 |