다음은 로버트 C. 마틴 Clean Code(클린 코드)를 읽고, 정리한 내용입니다.
작게 만들어라!
함수를 만드는 첫째 규칙은 '작게!'다. 함수를 만드는 둘째 규칙은 '더 작게!'다.
public static String renderPageWithSetupsAndTeardowns(
pageData pageData, boolean isSuite) throws Exception{
if(isTestPage(pageData))
includeSetupTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
블록과 들여쓰기
다시말해, if 문/else문/while 문 등에 들어가는 블록은 한 줄이어야 한다는 의미다.
한 가지만 해라!
함수는 한 가지를 해야 한다. 그 한가지를 잘 해야 한다. 그 한가지만을 해야한다.
지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면, 그 함수는 한가지 작업만 한다.
어쨌거나 우리가 함수를 만드는 이유는 큰 개념을 (다시 말해, 함수 이름을) 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서가 아니던가.
3-2
public static String renderPageWithSetupAndTeardowns(
PageData pageData, boolean isSuite
) throws Exception{
boolean isTestPage = pageData.hasAttribute("Test");
if(isTestPage){
WikiPage testPage = pageData.getWikiPage();
StringBuffer newPageContent = new StringBuffer();
includeSetupPages(testPage, newPageContent, isSuite);
newPageContent.append(pageData.getContent());
includeTeardownPages(testPage, newPageContent, isSuite);
pageData.setContent(newPageContent.toString());
}
return pageData.getHtml();
}
3-3
public static String renderPageWithSetupAndTeardowns(
PageData pageData, boolean isSuite
) throws Exception{
if(isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
목록 3-2도 추상화 수준이 둘이다. 그래서 목록 3-3 으로 축소가 가능했다. 하지만 의미를 유지하면서, 목록 3-3을 더 이상 줄이기란 불가능하다. if문을 includeSetupsAndTeadownsIfTestPate 라는 함수로 만든다면 똑같은 내용을 다르게 표현할 뿐 추상화 수준은 바뀌지 않는다.
따라서, 함수가 '한 가지'만 하는지 판단하는 방법이 하나 더 있다. 단순히 다른 표현이 아니라 의미있는 이름으로 다른 함수를 추출할 수 있다면, 그 함수는 여러가지 작업을 하는 셈이다.
함수 당 추상화 수준은 하나로!
함수가 확실히 '한가지' 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.
목록 3-1은 이 규칙을 확실히 위반한다. getHtml()은 추상화 수준이 아주 높다. 반면 String pagePathName = PathParser.render(pagepath);는 추상화 수준이 중간이다. 그리고 .append("\n")과 같은 코드는 추상화 수준이 아주 낮다.
한 함수 내에 추상화 수준을 섞으면, 코드를 읽는 사람이 헷갈린다. 특정 표현이 근본 개념인지 아니면 세부 사항인지 구분하기 어려운 탓이다. 하지만, 문제는 이 정도로 그치지 않는다. 근본 개념과 세부 사항을 뒤섞기 시작하면, 깨어진 창문처럼 사람들이 함수에 세부사항을 점점 추가한다.
위에서 아래로 코드 읽기: 내려가기 규칙
코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다. 즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한번에 한 단계씩 낮아진다. 나는 이것을 내려가기 규칙이라 부른다.
TO 설정 페이지와 해제 페이지를 포함하려면, 설정 페이지를 포함하고, 테스트 페이지 내용을 포함하고, 해제 페이지를 포함한다.
TO 설정 페이지를 포함하려면, 슈트이면 슈트 설정 페이지를 포함한 후 일반 설정 페이지를 포함한다.
TO 슈트 설정 페이지를 포함하려면 부모 계층에서 'SuiteSetUp' 페이지를 찾아 include문과 페이지 경로를 추가한다.
TO 부모 계층을 검색하려면, ....
함수 인수
최선은 입력 인수가 없는 경우이며, 차선은 입력 인수가 1개뿐인 경우다.
SetupTeardownIncluder.render(pageData)는 이해하기 아주 쉽다. pageData 객체 내용을 렌더링하겠다는 뜻이다.
동사와 키워드
함수의 의도나 인수의 순서와 의도를 제대로 표현하려면, 좋은 함수 이름이 필수다. 단항 함수(단 하나의 인자를 가진 함수)는 함수와 인수가 동사/명사 쌍을 이뤄야 한다. 예를 들어, write(name)은 누구나 곧바로 이해한다. 좀 더 나은 이름은 writeField(name)이다. 그러면 이름이 필드라는 사실이 분명히 드러난다.
마지막 예제는 함수 이름에 키워드를 추가하는 형식이다. 즉, 함수 이름에 인수 이름을 넣는다. 예를 들어, asserEquals 보다 assertExpectedEqualsActual(expected, actual)이 더 좋다. 그러면 인수 순서를 기억할 필요가 없어진다.
부수 효과를 일으키지 마라!
부수효과는 거짓말이다. 함수에서 한가지를 하겠다고 약속하고선 남몰래 다른 짓도 하니까. 때로는 예상치 못하게 클래스 변수를 수정한다.
3-6
public class UserValidator{
private Cryptograper cryptographer;
public boolean checkPassword(String userName, String password){
User user = UserGateway.findByName(userName);
if(user != User.NULL){
String codePhrase = user.getPhraseEncodedByPassword();
String phrase = cryptographer.decrypt(codePhrase, password);
if("Valid Password".equlas(phrase)){
Session.initialize();
return true;
}
}
return false;
}
}
다음 함수는 부수효과를 일으킨다. 바로 Session.initialize() 호출이다. checkPassword 함수는 이름 그대로 암호를 확인한다. 이름만 봐서는 세션을 초기화 한다는 사실이 드러나지 않는다. 그래서 함수 이름만 보고 함수를 호출하는 사용자는 사용자를 인증하면서 기존 세션 정보를 지워버릴 위험에 처한다.
만약 시간적인 결합이 필요하다면, 함수 이름에 분명히 명시한다. 3-6은 checkPasswordAndInitializeSession 이라는 이름이 훨씬 좋다. 물론 함수가 '한가지'한다는 규칙을 위반하지만.
출력 인수를 피해라!
출력인수란 함수에서 반환값 대신에, 인수를 통해 데이터를 외부로 전달하는 방식을 말합니다. 이러한 방식은 코드를 이해하고 추적하기 어렵게 만들 수 있다.
출력인수가 아닌 반환값을 사용하는 경우, 함수의 사용이 명확해지고, 함수 외부의 상태를 변경하기 않으므로 부작용이 줄어든다.
void updateUserAge(User user, int age) {
user.setAge(age); // user 객체는 출력 인수로, 외부 상태를 변경함.
}
User updateUserAge(User user, int age) {
User updatedUser = new User(user.getName(), age);
return updatedUser; // 새로운 상태를 가진 객체를 반환함.
}
함수에서 상태를 변경해야 한다면, 함수가 속한 객체 상태를 변경하는 방식을 택한다.
명령과 조회를 분리하라
if(set("username", "unclebob"))...
독자 입장에서 코드를 읽어보자. username이 unclebob으로 설정되어 있는지 확인하는 코드인가? 아니면 username을 unclebob로 설정하는 코드인가? 함수를 호출하는 코드로만 봐서는 의미가 모호하다.
set이 형용상인지 동사인지 분간하기 어려운 탓이다.
해결책은 명령과 조회를 분리해 혼란을 뿌리채 뽑는 방법이다.
if(attrbuteExists("username")){
setAttribute("username", "unclebob");
}
오류코드 보다 예외를 사용하라!
오류 코드 대신 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다.
try {
deletePage(page);
registry.deleteReference(page.name);
configkets.deleteKey(page.name.makeKey());
}
catch(Exception e){
logger.log(e.getMessage());
}
try/catch 블록 뽑아내기
try/catch 블록은 원래 추하다. 코드 구조에 혼란을 일으키며, 정상 동작과 오류 처리 동작을 뒤섞는다. 그러므로 try/catch 블록을 별도의 함수로 뽑아내는 편이 좋다
public void delete(Page page){
try {
deletePageAndAlReferences(page);
} catch(Exception e){
logError(e);
}
}
private void deletePageAndAlReferences(Page page) throws Exception{
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e){
logger.log(e.getMessage());
}
반복하지 마라!
목록 3-7은 include 방법으로 중복을 없앤다. 코드를 다시 한번 읽어본다.
중복을 없앴더니 모듈 가독성이 크게 높아졌다는 사실을 깨달으리라.
'Clean Code' 카테고리의 다른 글
5장 형식 맞추기 (0) | 2024.02.19 |
---|---|
4장 주석 (1) | 2024.02.11 |
2장 의미있는 이름 (0) | 2024.02.08 |
1장 깨끗한 코드 (0) | 2024.02.07 |
클린 코드를 향한 첫 걸음 (0) | 2024.02.07 |