단순히 돌아가는 코드에 만족하는 프로그래머는 전문가 정신이 부족하다. 설계와 구조를 개선할 시간이 없다고 변명할지 모르지만 나로서는 동의하기 어렵다. 나쁜 코드보다 더 오랫동안 더 심각하게 개발 프로젝트에 악영향을 미치는 요인도 없다. 나쁜 일정은 다시 짜면 된다. 나쁜 요구사항은 다시 정의하면 된다. 나쁜 팀 역학은 복구하면 된다. 하지만 나쁜 코드는 썩어 문드러진다. 점점 무게가 늘어나 팀의 발목을 잡는다. 속도가 점점 느려지다 못해 기어가는 팀도 많이 봤다. 너무 서두르다가 이후로 영원히 자신들의 운명을 지배할 악성 코드라는 굴레를 짊어진다. -본문 중에서-
풀타임 직업 프로그래머를 한지 2년이 넘었다. 아직 부족함을 많이 느낀다. 지난 2년을 돌아보면 한마디로 '급급했다'라고 평가할 수 있다. 빠른 구현 일정에서 실용적 고민과 개선 등 스스로 칭찬해주고 싶은 부분들이 있다. 그러나 결과론적으로 내가 써온 코드들이 많이 부끄럽다. 이런 감정이 건강하다는 것은 안다. 그래도 부끄러움과 죄책감이 있는 건 어쩔 수 없다. 어찌 되었건 결과로 말하고 싶다.
돌이켜보면 무작정 부딪히며 코드를 쓰는 시간은 어느 정도 보냈지만, 코드 작성에 대해 고민해보는 시간은 상대적으로 적었다. 많이 코드를 써보는 것도 중요하지만, 그래도 이렇게 원론적으로 생각해보는 시간도 필요한 것 같다. 저자의 경험과 직관이 가득한 원칙들을 읽어나가며 많은 공감을 했다. 이제 지식을 충전했으니, 한 걸음씩 실천할 일만 남았다. 기술부채와 유저가치의 조화는 것은 늘 고민해봐야할 문제다. 조화를 위해 한가지 한가지 선택한다는 것은 쉬운일은 아니다.
-함수 추상화 부분이 한번에 한단계씩 낮아지는 것이 가장 이상적이다.(내려가기 규칙)
명령과 조회를 분리하라
// bad
if(set("username", "unclebob")) {
...
}
// good
if(attrivuteExists("usernam")){
setAttrivute("username", "unclebob");
}
오류 코드를 선언하고 사용하는 대신, 예외를 사용하라. try/catch 블록은 원래 추하다. 정상과 오류처리 동작을 뒤섞는다. 그러므로 try/catch 블록을 별도 함수로 뽑아내는 편이 좋다.
// bad
if (deletePage(page) == E_OK) {
if (registry.deleteReference(page.name) == E_OK) {
if (configKeys.deleteKey(page.name.makeKey()) == E_OK) {
logger.log("page deleted");
} else {
logger.log("configKey not deleted");
}
} else {
logger.log("deleteReference from registry failed");
}
} else {
logger.log("delete failed"); return E_ERROR;
}
// good
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
} catch (Exception e) {
logError(e);
}
}
private void deletePageAndAllReferences(Page page) throws Exception {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e) {
logger.log(e.getMessage());
}
코드로 의도를 표현하라
// bad
// 직원에게 복지 혜택을 받을 자격이 있는지 검사한다.
if ((emplotee.flags & HOURLY_FLAG) && (employee.age > 65) {
...
}
// good
if (employee.isEligibleForFullBenefits()) {
...
}
모든 함수에 Javadocs를 달거나 모든 변수에 주석을 달아야 한다는 규칙은 어리석기 그지없다. 이런 주석은 코드를 복잡하게 만들며, 거짓말을 퍼뜨리고, 혼동과 무질서를 초래한다. 아래와 같은 주석은 아무 가치도 없다.
/**
*
* @param title CD 제목
* @param author CD 저자
* @param tracks CD 트랙 숫자
* @param durationInMinutes CD 길이(단위: 분)
*/
public void addCD(String title, String author, int tracks, int durationInMinutes) {
CD cd = new CD();
cd.title = title;
cd.author = author;
cd.tracks = tracks;
cd.duration = durationInMinutes;
cdList.add(cd);
}
함수나 변수로 표현할 수 있다면 주석을 달지 마라
// 전역 목록 <smodule>에 속하는 모듈이 우리가 속한 하위 시스템에 의존하는가?
if (module.getDependSubsystems().contains(subSysMod.getSubSystem()))
주석을 제거하고 다시 표현하면 다음과 같다.
ArrayList moduleDependencies = smodule.getDependSubSystems();
String ourSubSystem = subSysMod.getSubSystem();
if (moduleDependees.contains(ourSubSystem))
예외 코드를 래핑해 호출하는 곳에서 처리하기 수월하게 만들 수 있다.
LocalPort port = new LocalPort(12);
try {
port.open();
} catch (PortDeviceFailure e) {
reportError(e);
logger.log(e.getMessage(), e);
} finally {
...
}
public class LocalPort {
private ACMEPort innerPort;
public LocalPort(int portNumber) {
innerPort = new ACMEPort(portNumber);
}
// 사용하는 곳에서는 PortDeviceFailure 를 처리하면 됨!
public void open() {
try {
innerPort.open();
} catch (DeviceResponseException e) {
throw new PortDeviceFailure(e);
} catch (ATM1212UnlockedException e) {
throw new PortDeviceFailure(e);
} catch (GMXError e) {
throw new PortDeviceFailure(e);
}
}
...
}
-위에서 함수 이름을 바꿔 given-when-then 이라는 관례를 사용했다는 사실에 주목한다. 그러면 테스트 코드를 읽기가 쉬워진다. 하지만 불행하게도 위에서 보듯이 테스트를 분리하면 중복되는 코드가 많아진다. (...) TEMPLATE METHOD 패턴을 사용하면 중복을 제거할 수 있다. given/when 부분을 부모 클래스에 두고 then 부분을 자식 클래스에 두면 된다.
켄트 벡은 다음 규칙을 따르면 설계는 '단순하다'고 말한다.
미신과 오해, 아래는 잘 알려진 미신과 오해에 대한 설명이다.
저자의 경험과 직관에 의한 원칙들을 나열한 장. 이 목록은 완전하지 않지만 가치를 피력할 뿐이다.
휴리스틱 : 알고리즘과 대비되며, 굳이 이분법적으로 접근하면 인간의 '직관'을 반영하는 사고방식으로, 시간이나 자료의 부족, 인지적 자원의 제약, 문제 특성 등의 이유로, 답을 도출하기 위한 정확한 절차를 사용하지 않고 경험과 직관에 의존해 '대충 때려맞추는' 방법.