1. 작게 만들어라!
함수를 만드는 첫째 규칙은 '작게', 두 번째 규칙은 '더 작게'
최대한 작게 만드는 것이 이 책에서 이야기하는 함수의 포인트이다
특히 if문, while 문 등에 들어가는 블록은 한 줄로 작성하는 것을 권장할 정도로 작게 만드는 것에 집착한다
즉, 중첩 구조가 생길 만큼 함수가 커져서는 안 된다는 뜻
함수에서 들여 쓰기 수준은 1단이나 2단을 넘어서면 안 된다
이렇게 작성하기를 권장하는 이유는 당연히 읽고 이해하기 쉽게 하기 위해서 이다!
2. 한 가지 해라!
하나의 함수는 한 가지만 처리해야 한다
그렇다면 '한 가지'의 범위는 무엇을 의미할까?
- 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다고 말할 수 있다
- 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다
예를 들어 if문을 수행할 때 또 다른 함수를 만들어서 if문을 수행한다면 추상화의 수준은 동일하기 때문에 의미가 없다
하지만 if문 안에서 여러 가지 작업을 수행한다고 생각해보자
여러가지 작업들을 각각 함수로 만들어서 처리하도록 변경하면 함수 내 모든 문장의 추상화 수준을 동일하게 유지할 수 있다
한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다
특정 표현이 근본 개념인지 아니면 세부사항인지 구분하기 어려운 탓이다
근본 개념과 세부사항을 뒤섞기 시작하면 사람들은 함수에 세부사항을 점점 더 추가할 것이다
3. 위에서 아래로 코드 읽기 (이 책에서의 표현으로는 내려가기 규칙)
코드는 위에서 아래로 이야기처럼 읽혀야 좋다
한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다
즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수진이 한 번에 한 단계씩 낮아진다
4. Switch문
switch문은 작게 만들기 어렵다
(if/else가 여럿 이어지는 구문 포함)
본질적으로 switch문은 N가지를 처리하도록 설계되어있기때문...
다형성을 이용하여 저차원 클래스에 숨기고 반복하지 않도록 하는 방법을 사용해야 한다
아래 예제를 통해 살펴보자
public Money calculatePay(Employee e) throws InvalidEmployeeType {
switch (e.type) {
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
위 함수에서 몇 가지 문제가 있다
- 함수가 길다
새 직원 유형을 추가하면 더 길어진다 - 한 가지 작업만 수행하지 않는다
- SRP(Single Responsibility Principle)을 위반한다
https://ko.wikipedia.org/wiki/%EB%8B%A8%EC%9D%BC_%EC%B1%85%EC%9E%84_%EC%9B%90%EC%B9%99 - OCP(Open Closed Principle)를 위반한다
https://ko.wikipedia.org/wiki/%EA%B0%9C%EB%B0%A9-%ED%8F%90%EC%87%84_%EC%9B%90%EC%B9%99
다음과 같은 문제점들을 해결하기 위해서는 switch문을 추상 팩토리에 숨기는 방법을 사용할 수 있다
팩토리는 switch 문을 사용해 적절한 Employee 파생 클래스의 인스턴스를 생성한다
calculatePay, isPayday, deliverPay 등과 같은 함수는 Employee 인터페이스를 거쳐 호출된다
그러면 다형성으로 인해 실제 파생 클래스의 함수가 실행된다
public abstract class Employee {
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}
public interface EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
public class EmployeeFactoryImpl implements EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
switch (r.type) {
case COMMISSIONED:
return new CommissionedEmployee(r);
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmployee(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
5. 서술적인 이름을 사용하라!
앞에서 읽었던 의미 있는 이름과 비슷한 내용이었다
이름은 항상 이름만으로 의미를 알아볼 수 있도록!
6. 함수 인수
함수에서 이상적인 인수 개수는 0개(무항)다
인수 개수는 적을수록 좋으며 3개(삼항)는 가능한 피하는 편이 좋고, 4개(다항) 이상이라면 특별한 이유가 필요하다
인수는 어렵기 때문이다. 인수는 개념을 이해하기 어렵게 만든다
코드를 읽는 사람에게 includeSetupPageInto(new PageContent)보다 includeSetupPage()가 이해하기 더 쉽다
includeSetupPageInto(new PageContent)는 함수 이름과 인수 사이에 추상화 수준이 다르고 코드를 읽는 사람이 현시점에서 별로 중요하지 않은 세부사항을 알아야 한다
특히 테스트 관점에서 볼 때 인수는 더 어렵다
갖가지 인수 조합으로 함수를 검증하는 테스트 케이스를 작성한다고 생각해보면 끔찍하다
최선은 입력 인수가 없는 경우이며, 차선은 입력 인수가 1개뿐인 경우다
ex) SetupTeardownIncluder.render(pageData)는 pageData의 객체 내용을 렌더링 하겠다는 뜻으로 이해하기 아주 쉽다
물론 예외도 존재한다
예를 들면 좌표의 인수를 받는 경우 등
Point p = new Point(0, 0);
7. 동사와 키워드
함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다
예를 들어 write(name)은 누구나 곧바로 이해한다
좀 더 나은 이름은 writeField(name)이다. 이름이 필드라는 사실이 분명히 드러난다
조금 더 응용해보자면 함수 이름에 키워드를 추가하는 형식이 있다
예를 들어 assertEquals보다 assertExpectedEqualsActual(expected, actual)이 더 좋다
인수 순서를 기억할 필요가 없어지기때문!
8. 부수 효과를 일으키지 마라!
함수에서 한 가지만 처리하도록 하라는 내용을 기억해보자
책에서 제시한 예제 코드를 보면 단번에 이해가 된다
public class UserValidator {
private Cryptographer cryptographer;
public boolean checkPassword(String userName, String password) {
User user = UserGateway.findByName(userName);
if(user != User.NULL) {
String codedPhrase = user.getPhraseEncodedByPassword();
String phrase = cryptographer.decrypt(codedPhrase, password);
if("Valid Password".equals(phrase)) {
Session.initialize();
return true;
}
}
return false;
}
}
여기서 부수 효과는 Session.initialize(); 호출이다
checkPassword 함수는 이름 그대로 암호를 확인하는 함수이다
이름만 봐서는 세션을 초기화한다는 사실이 드러나지 않는다
그래서 함수 이름만 보고 함수를 호출하는 사용자는 사용자를 인증하면서 기존 세션 정보를 지워버릴 위험에 처한다
차라리 함수명을 checkPasswordAndInitializeSession 이라는 이름을 사용하는 것이 훨씬 좋다
물론 이렇게 되면 함수가 '하나의 함수는 한 가지만 처리해야 한다'는 규칙을 위반하게 된다
9. 명령과 조회를 분리하라!
함수는 뭔가를 수행하거나 뭔가를 답하거나 둘 중 하나만 해야 한다. 둘 다 하면 안 된다!
객체 상태를 변경하거나 아니면 객체 정보를 반환하거나 둘 중 하나다. 둘 다 하면 혼란을 초래한다
10. 오류 코드보다는 예외를 사용하라!
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;
}
오류 코드를 작성하여 코드별로 에러 내용을 표시하기보다는 try/catch 문을 사용하여 오류 처리를 하는 것이 훨씬 깔끔하다
try {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
} catch(Exception e) {
logger.log(e.getMessage());
}
하지만 여기서도 더 깔끔하게 코드를 처리할 방법이 있다
try/catch 블록을 별도 함수로 뽑아내는 편이 좋다
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());
}
오류 처리도 한 가지 작업이다
함수는 한 가지 작업만 해야 한다는 규칙은 오류 처리도 포함되는 규칙이다
마무리 느낀 점
이번 챕터에서 가장 인상 깊었던 내용은 "부수 효과를 일으키지 마라"라는 부분이었다
소스를 짤 때 가장 많이 했던 실수 중 하나였던 것 같다
하나의 함수가 한 가지 처리만 해야 하는 이유를 단박에 이해하게 해 준 내용이었다
클린코드를 끝까지 완독하고 적용하려고 노력하다 보면 리팩토링뿐 아니라 TDD 공부에도 많은 도움이 될 것 같다
'개발 서적 > 클린코드' 카테고리의 다른 글
클린코드 - 7.오류 처리 (0) | 2022.07.05 |
---|---|
클린코드 - 6.객체와 자료 구조 (0) | 2022.06.05 |
클린코드 - 5.형식 맞추기 (0) | 2022.06.04 |
클린코드 - 4.주석 (0) | 2022.06.04 |
클린코드 - 1.깨끗한 코드 & 2.의미있는 이름 (0) | 2022.02.20 |