1. 들어가기
개발자라면 누구나 올바르게 작성된 코드를 읽으며 즐거움을 느끼거나, 엉망인 코드를 보며 한숨부터 나오는 상황을 겪어본 적이 있을 것입니다. 쉽게 작성된 코드는 무척 중요합니다. 우리는 코딩 대부분의 시간을 코드를 '작성'할때가 아닌 '분석'하는 과정에서 소비하기 때문입니다. <클린코드: 애자일 소프트웨어 장인정신>는 설계자가 작성한 코드가 분석되는 시간을 최소화하고 단순하고 직접적인, 의도대로 읽히는 코드를 작성할 수 있는 여러가지의 법칙을 소개합니다.
2. 구성
책은 초반, 중반, 후반 크게 세 가지의 구성으로 나뉘어집니다. 초반부에서는 클린코드 개념설명, 변수 및 함수의 작명법 등 기초적인 법칙에 대해 설명하고 있으며 중반부에서는 실제 오픈소스 코드를 리팩터링하는 과정이 서술되었습니다. 후반부에서는 소개한 클린코드를 정리하고 마무리합니다. 부록으로는 동시성 처리문제와 관련된 부가내용이 첨부되어 있습니다.
3. 책갈피
✅ 코드로 의도를 표현하라.
'내가 작성한 코드를 쉽게 이해시키기 위해 주석을 꼼꼼히 달자!' 라고 생각하고 있었기에 한 대 맞은듯한 기분이었습니다. 그러고보니, 애초에 코드를 이해하기 어려운 경우 주절주절 작성된 주석을 본다고 해서 딱히 이해에 도움이 되지 않았습니다. 개발자라면 불필요한 주석 대신 코드를 통해 코드를 설명하라는 것이 핵심입니다. 복잡한 조건식은 함수화해서 함수명으로 설명하고, 주절거리는 주석 대신 의미를 밝히거나 결과를 경고하는 등의 명확한 용도로 사용할 것을 권장합니다.
✅ 함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다.
각 함수가 수행하는 역할은 하나의 추상화된 역할이어야 합니다. '내려가기 규칙'에 의해 호출되는 함수마다 추상화 단계가 한단계씩 낮아져야합니다. 하나의 역할이라는 것은 단 한 건의 작업을 의미하는 것이 아닙니다. 아래 코드는 테스트페이지일 경우의 역할을 함수로 추상화했습니다. includeSetupAndTeardownPages() 내에서 여러 작업을 수행합니다.
private String renderPageWithSetupAndTeardowns(PageData pageData, bool isSuite){
if (isTestPage(pageData)) {
includeSetupAndTeardownPages(pageData, isSuite)
}
return pageData.getHtml();
}
/*
// 추상화된 함수 내에서 여러 역할을 수행한다.
private void includeSetupAndTeardownPages() throws Exception {
includeSetupPages();
includePageContent();
includeTeardownPages();
updatePageContent();
}
*/
✅ 서로 밀접한 개념은 세로로 가까이 둬야한다.
함수 연관 관계와 동작 방식을 이해하기 위해 이 함수에서 저 함수를 오가며 뺑뺑이를 돌았으나 결국은 미로 같은 코드 때문에 혼란만 가중된 경험이 있는가?
기존 소스코드를 분석할 때 어려움을 겪었던 이유는 함수 및 변수 선언의 위치가 규칙적이지 않은 경우가 많아 그것을 찾아 해메여야 했기 때문입니다. 다음과 같은 규칙을 통해 코드 구조 가독성을 향상 시킬 수 있습니다.
// 1. 변수 생성은 사용될 부분과 가장 가깝게 위치한다.
private static void readPreferences() {
InputStream is = null;
try {
is = new FileInputStream(getPreferencesFile());
setPreferences(new Properties(getPreferences()));
getPreferences().load(is);
} catch (IOException e) {
e.printStackTrace();
}
}
// 2. 개념적 유사성에 맞춰 함수를 모아두자.
static public void assertTrue() {
...
}
static public void assertFalse() {
...
}
3. 상위 함수가 호출하는 하위 함수는 상위 함수가 작성된 다음에 위치시킨다.
✅ 구현을 감추고 자료구조를 추상화하라.
실수든, 고의든 사용자의 자료구조 변수 접근을 통제하고 구현 변경을 차단해야합니다. 사용자는 구현 과정을 알 필요없이 자료를 조작할 수 있게끔 해야합니다.
// 구체적인 Point 클래스
public class Point {
public double x;
public double y;
}
// 추상적인 Point 클래스
// 내부 구현과 변수는 감추고 추상화된 함수를 제공해 개발자의 설계대로 사용된다.
public interface Point {
double getX();
double getY();
void setCartesian(double x, double y);
double getR();
double getTheta();
void setPolar(double r, double theta);
}
// 구체적 Vehicle 클래스
public interface Vehicle {
public getFuelTankCapacityInGallons();
public getGallonsOfGasoline();
}
// 추상적 Vehicle 클래스
// 백분율이라는 추상화 개념으로 반환해, 사용자는 데이터가 오게되는 과정은 모르게된다.
public interface Vehicle {
double getPercentFuelRemaining();
}
✅ F.I.R.S.T
- Fast = 테스트는 빨라야 합니다.
- Independent = 각 테스트는 서로 의존해서는 안됩니다.
- Repeatable = 테스트는 어떤 환경에서도 반복 가능해야 한다.
* 테스트를 위해 별도의 세팅이 필요해서는 안된다.
- Self-Validating = 테스트는 Bool 값으로 결과를 내야합니다. 성공 또는 실패.
* 수치 값 또는 문자열로 결과가 표시되어서는 안된다.
- Timely : 테스트는 적시에 작성해야 합니다.
✅ 디미터의 법칙
객체에서 여러 점을 찍으며, 낯선 객체에 까지 접근 할 필요가 없습니다. 한 객체가 알아야 할 다른 객체는 최소한으로 유지하고 메세지를 전달하면 값을 전달 받을 수 있어야 합니다.
// 디미터의 원칙 위반
// 객체를 타고 다니며, 기차충돌 현상
final String outputDi = ctxt.getOptions().getScratchDir().getAbsolutePath();
// 디미터의 원칙 준수
// 객체당 한번의 접근으로 데이터를 불러온다.
Options options = ctxt.getOptions();
File scratchDir = options.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
4. 정리하며
우선 아쉬움이 컸습니다. TDD나 동시성 처리와 관련된 부분은 아직 실전경험과 활용이 떨어져 이해하기 어려운 부분이 있었고 보완의 필요성을 느꼈습니다. 그럼에도 한번의 완독이었지만, 코드 작성 방법에 대해서 기본부터 배울 수 있었습니다. 실전 프로젝트나 개인 프로젝트를 진행함에 있어서도 적용 가능한 부분은 적극적으로 적용해 저의 스타일로 체득하고 싶습니다. 매년 1회 혹은 2회 정도 꾸준히 회독할 생각입니다. 개발자로서 경험이 쌓일수록 새롭게 읽히고 이해되는 부분들을 찾는 재미가 있을 것 같습니다.