단순히 돌아가는 코드에 만족하는 프로그래머는 전문가 정신이 부족하다. 설계와 구조를 개선할 시간이 없다고 변명할지 모르지만 나로서는 동의하기 어렵다. 나쁜 코드보다 더 오랫동안 더 심각하게 개발 프로젝트에 악영향을 미치는 요인도 없다. 나쁜 일정은 다시 짜면 된다. 나쁜 요구사항은 다시 정의하면 된다. 나쁜 팀 역학은 복구하면 된다. 하지만 나쁜 코드는 썩어 문드러진다. 점점 무게가 늘어나 팀의 발목을 잡는다. 속도가 점점 느려지다 못해 기어가는 팀도 많이 봤다. 너무 서두르다가 이후로 영원히 자신들의 운명을 지배할 악성 코드라는 굴레를 짊어진다. -본문 중에서-
풀타임 직업 프로그래머를 한지 2년이 넘었다. 아직 부족함을 많이 느낀다. 지난 2년을 돌아보면 한마디로 '급급했다'라고 평가할 수 있다. 빠른 구현 일정에서 실용적 고민과 개선 등 스스로 칭찬해주고 싶은 부분들이 있다. 그러나 결과론적으로 내가 써온 코드들이 많이 부끄럽다. 이런 감정이 건강하다는 것은 안다. 그래도 부끄러움과 죄책감이 있는 건 어쩔 수 없다. 어찌 되었건 결과로 말하고 싶다.
돌이켜보면 무작정 부딪히며 코드를 쓰는 시간은 어느 정도 보냈지만, 코드 작성에 대해 고민해보는 시간은 상대적으로 적었다. 많이 코드를 써보는 것도 중요하지만, 그래도 이렇게 원론적으로 생각해보는 시간도 필요한 것 같다. 저자의 경험과 직관이 가득한 원칙들을 읽어나가며 많은 공감을 했다. 이제 지식을 충전했으니, 한 걸음씩 실천할 일만 남았다. 기술부채와 유저가치의 조화는 것은 늘 고민해봐야할 문제다. 조화를 위해 한가지 한가지 선택한다는 것은 쉬운일은 아니다.
추상적인 개념 하나에 단어 하나를 선택해 이를 고수한다. 예를 들어, 똑같은 메서들르 클래스마다 fetch, retrieve, get으로 제각각 부르면 혼란스럽다.
의미가 분명하지 못한 경우 클래스/함수/네임스페이스에 넣어 맥락을 부여한다. 모든 방법이 실패하면 마지막 수단으로 접두하를 붙인다. (인자에 addrFirstName, addLastName, addState 등으로 접두어를 붙어 맥락을 추가할 수 있다. 물론 Address 라는 class 를 생성하면 더 좋다.)
-함수 추상화 부분이 한번에 한단계씩 낮아지는 것이 가장 이상적이다.(내려가기 규칙)
서술적인 이름을 사용하면 개발자 머릿속에도 설계가 뚜렷해진다.
입력인수를 그대로 돌려주는 함수라 할지라도 변환 함수 형식을 따르는 편이 좋다. 적어도 변환 형태는 유지하기 떄문이다.
플래그 인수는 추하다.
side effect 를 일으키 마라.
명령과 조회를 분리하라
// 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))
첫 문단에는 전체 기사 내용을 요약하며, 기사를 읽으며 내려갈 수록 세세한 사실이 조금씩 드러나며 세부사항이 나오게 된다. 소스파일 이름(표제)는 간단하면서도 설명이 가능하게 지어, 이름만 보고도 올바른 모듈을 살펴보고 있는지를 판단 할 수 있도록 한다. 소스파일의 첫 부분(요약 내용)은 고차원 개념과 알고리즘을 설명한다. 아래로 내려갈수록 의도를 세세하게 묘사하며, 마지막에는 가장 저차원 함수(아마 Getter/Setter?)와 세부 내역이 나온다. 신문이 사실, 날짜, 이름 등을 무작위로 뒤섞은 긴 기사 하나만 싣는다면 아무도 신문을 읽지 않을 것이다.
개념은 빈 행으로 분리하라. 코드의 각 줄은 수식이나 절을 나타내고, 여러 줄의 묶음은 완결된 생각 하나를 표현한다. 생각 사이에는 빈 행을 넣어 분리해야한다. 그렇지 않다면 단지 줄바꿈만 다를 뿐인데도 코드 가독성이 현저히 떨어진다.
개념적인 친화도가 높을 수록 코드를 서로 가까이 배치한다. 앞서 살펴보았듯이 한 함수가 다른 함수를 호출하는 종속성, 변수와 그 변수를 사용하는 함수가 그 예다. 그 외에도 비슷한 동작을 수행하는 함수 무리 또한 개념의 친화도가 높다.
객체 지향 코드에서 어려운 변경은 절차적인 코드에서 쉬우며, 절차적인 코드에서 어려운 변경은 객체 지향 코드에서 쉽다. (...) 분별 있는 프로그래머는 모든 것이객체라는 생각이 미신임을 잘 안다. 때로는 단순한 자료구자와 절차적인 코드가 가장 적합한 상황도 있다.
디미터 법칙(law of demeter)은 잘 알려진 휴리스틱heuristic으로, 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다.
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);
}
}
...
}
몇 년 전 저자는 테스트 코드에 실제 코드와 동일한 품질 기준을 적용하지 않아야 한다고 명시적으로 결정한 팀을 코치해달라는 요청을 받았다. 팀원들은 서로에게 단위 테스트에서 규칙을 깨도 좋다는 허가장을 줬다. '지저분해도 빨리'가 주제어였다. 변수 이름은 신경 쓸 필요가 없고, 테스트 함수는 간결하거나 서술적일 필요가 없었고, 테스트 코드는 잘 설계하거나 주의해서 분리할 필요가 없었다. 그저 돌아만 가면, 그러니까 실제 코드를 테스트만 하면 그만이었다. 하지만 팀은 지저분한 테스트 코드를 내놓으나 테스트를 안 하나 오십보 백보라는, 아니 오히려 더 못하다는 사실을 깨닫지 못했다. 문제는 실제 코드가 진화하면 테스트 코드도 변해야 한다는 데 있다. 그런데 테스트 코드가 지저하면 할수록 변경하기 어려워진다. 이 경우실제 코드를 짜는 시간보다 테스트 케이스를 추가하는 시간이 더 걸리기 십상이다. 실제 코드를 변경해 기존 테스트 케이스가 실패하기 시작하면, 지저분한 코드로 인해 실패하는 테스트 케이스를 점점 더 통과시키기 어려워진다. 그래서 테스트 코드는 계속해서 늘어나는 부담이 되버린다. (...)테스트에 쏟아 부은 노력은 확실히 허사였다. 하지만 실패를 초래한 원인은 테스트 코드를 막 짜도 좋다고 허용한 결정이었다.
깨끗한 테스트 코드를 만들려면? 세 가지가 필요하다. 가독성, 가독성, 가독성.
BUILD-OPERATE-CHECK 패턴이 위와 같은 테스트 구조에 적합하다. 각 테스트는 명확히 세 부분으로 나눠진다. 첫 부분은 테스트 자료를 만든다. 두 번째 부분은 테스트 자료를 조작하며, 세 번째 부분은 조작한 결과가 올바른지 확인한다.
테스트 API코드에 적용하는 표준은 실제 코드에 적용하는 표준과 확실히 다르다. 단순하고, 간결하고, 표현력이 풍부해야 하지만, 실제 코드만큼 효율적일 필요는 없다. 실제 환경이 아니라 테스트 환경에서 돌아가는 코드이기 때문인데, 실제 환경과 테스트 환경은 요구사항이 판이하게 다르다
-위에서 함수 이름을 바꿔 given-when-then 이라는 관례를 사용했다는 사실에 주목한다. 그러면 테스트 코드를 읽기가 쉬워진다. 하지만 불행하게도 위에서 보듯이 테스트를 분리하면 중복되는 코드가 많아진다. (...) TEMPLATE METHOD 패턴을 사용하면 중복을 제거할 수 있다. given/when 부분을 부모 클래스에 두고 then 부분을 자식 클래스에 두면 된다.
클래스는 첫째! 작아야한다. 둘째! 작아야한다. 더 작아야 한다. 단 함수와는 다르게(함수는 물리적인 행 수로 측정)
단일 책임의 원칙 (이하 SRP)은 클래스나 모듈을 변경할 이유가 단 하나뿐이어야 한다는 원칙이다. 책임, 즉 변경할 이유를 파악하려고 애쓰다 보면 코드를 추상화 하기도 쉬워진다.
몇몇 함수가 몇몇 인스턴스 변수만 사용한다면 독자적인 클래스로 분리해도 된다!
시스템은 변경이 불가피하다. 그리고 변경이 있을 때 마다 의도대로 동작하지 않을 위험이 따른다. 깨끗한 시스템은 클래스를 체계적으로 관리해 변경에 따르는 위험을 최대한 낮춘다. (....) 잘 짜여진 시스템은 추가와 수정에 있어서 건드릴 코드가 최소이다.
concrete 클래스에 의존(상세한 구현에 의존)하는 클라이언트 클래스는 구현이 바뀌면 위험에 빠진다.
켄트 벡은 다음 규칙을 따르면 설계는 '단순하다'고 말한다.
깔끔한 시스템을 만들려면 단 몇 줄이라도 중복을 제거하겠다는 의지가 필요하다.
표준 명칭을 사용한다. 예를 들어, 디자인 패턴은 의사소통과 표현력 강화가 주요 목적이다. 클래스가 COMMAND나 VISITOR와 같은 표준 패턴을 사용해 구현된다면 클래스 이름에 패턴 이름을 넣어준다. 그러면 다른 개발자가 클래스 설계 의도를 이해하기 쉬워진다
중복을 제거하고, 의도를 표현하고, SRP를 준수한다는 기본적인 개념도 극단으로 치달으면 득보다 실이 많아진다. 클래스와 메서드 크기를 줄이자고 조그만 클래스와 메서드를 수없이 만드는 사례도 없지 않다. 그래서 이 규칙은 함수와 클래스 수를 가능한 한 줄이라고 제안한다.
단일 스레드에서 동작하는 코드는 작성하기 쉽다. 잘 동작하는 "것 처럼" 보이는 멀티 스레드 코드를 작성하기도 쉽다.
미신과 오해, 아래는 잘 알려진 미신과 오해에 대한 설명이다.
Concurrency는 항상 퍼포먼스를 향상시킨다. => Concurrency는 여러 스레드 혹은 여러 프로세서가 대기 시간을 공유할 수 있는 경우에만 퍼포먼스를 향상시킨다. 하지만 이러한 경우는 드물다.
Concurrent program 작성은 시스템의 디자인을 변경시키지 않는다. => "무엇"과 "언제"를 분리하는 작업은 보통 시스템의 구조에 큰 영향을 미친다.
Web나 EJB와 같은 컨테이너를 사용한다면 Concurrency 문제들은 신경쓸 필요가 없다. => 컨테이너가 어떤 일을 하는가에 대해 알아야 하며 concurrent update, 데드락을 해결하는 방법을 알아야 한다.
Concurrency 관련 버그는 재현하기 어렵기 때문에 종종 one-off로 취급된다.
공유 자원 문제를 해결하는 좋은 방법중 하나는 애초에 공유 자원을 사용하지 않는 것이다. 읽기 전용으로 사용될 경우 자원의 복사본을 사용하게 하는 방법이 있다. 경우에 따라서는 복사본을 여러 스레드에 전달, 작업을 수행하고 결과를 단일 스레드에서 수집해 사용하는 것도 가능하다.
문제는 돌연 발생할 것이다. 그렇지 않은 문제들은 보통 "한번만 발생하는" 문제로 치부된다. 이러한 one-off들은 보통 시스템에 부하가 걸린 경우, 혹은 무작위로 발생한다. 그러므로 스레드 관련 코드는 여러 설정, 환경에서 반복적이고 지속적으로 수행해 보라.
야 한다는 의미다.
일단 프로그램이 '돌아가면' 다음 업무로 넘어간다. '돌아가는' 프로그램은 그 상태가 어떻든 그대로 버려둔다. 경험이 풍부한 전문 프로그래머라면 이런 행동이 전문가로서 자살 행위라는 사실을 잘 안다.
TTD는 언제 어느 때라도 시스템이 돌아가야 한다는 원칙을 따른다. 다시 말해, TTD는 시스템을 망가뜨리는 변경을 허용하지 않는다.
단순히 돌아가는 코드에 만족하는 프로그래머는 전문가 정신이 부족하다. 설계와 구조를 개선할 시간이 없다고 변경할지 모르지만 나로서는 동의하기 어렵다. 나쁜 코드보다 더 오랫동안 더 심각하게 개발 프로젝트에 악영향을 미치는 요인도 없다. 나쁜 일정은 다시 짜면 된다. 나쁜 요구사항은 다시 정의하면 된다. 나쁜 팀 역학은 복구하면 된다. 하지만 나쁜 코드는 썩어 문드러진다. 점점 무게가 늘어나 팀의 발목을 잡는다. 속도가 점점 느려지다 못해 기어가는 팀도 많이 봤다. 너무 서두르다가 이후로 영원히 자신들의 운명을 지배할 악성 코드라는 굴레를 짊어진다.
저자의 경험과 직관에 의한 원칙들을 나열한 장. 이 목록은 완전하지 않지만 가치를 피력할 뿐이다.
휴리스틱 : 알고리즘과 대비되며, 굳이 이분법적으로 접근하면 인간의 '직관'을 반영하는 사고방식으로, 시간이나 자료의 부족, 인지적 자원의 제약, 문제 특성 등의 이유로, 답을 도출하기 위한 정확한 절차를 사용하지 않고 경험과 직관에 의존해 '대충 때려맞추는' 방법.