기술서적

Clean Code - #3장. 함수

skysoo1111 2022. 4. 8. 15:39

작게 만들어라

얼마나 짧아야 좋을까? 작을 수록 좋다.

중첩 구조가 생길만큼 함수가 커지면 좋지 않다.
if/else, while문 등에 들어가는 블록은 한 줄이어야 한다.

다시 말해 블록 안이 두 줄 이상이 된다면 함수로 추출하고 함수 이름을 적절히 짓자.

한가지만 해라

함수는 한가지 일만 해야하며, 함수 내 추상화 수준은 하나로 맞추면 된다.

하지만 추상화 수준이 하나인 함수를 구현하기란 쉽지 않다. 핵심은 짧으면서도 한가지 일만 하는 함수이다.

switch 문

다형성을 이용하여 각 switch 문을 저차원 클래스에 숨기고 절대로 반복하지 않는 방법이 있다.

아래 Bad 코드에는 직원 타입 별로 Pay를 계산하는 switch 문이 존재한다. 
여기서 많은 문제가 있지만 가장 큰 문제는 아래 switch 함수와 동일한 구조의 함수가 무한정 존재할 수 있다. 유형별로 지급일이 다르다면? 등

❌ Bad
public Money calculate(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);
    }
}

✅ Good
public abstract class Employee {
    public abstract boolean isPayday();
    public abstract Money calculatePay();
    public abstract void deliverPay();
}
------
public interface EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord er) throws InvalidEmployeeType;
}
------
public class EmployeeFactoryImpl implements EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord er) throws InvalidEmployeeType {
        switch (er.type) {
            case COMMISSIONED:
                return new CommissionedEmployee(er);
            case HOURLY:
                return new HourlyEmployee(er);
            case SALARIED:
                return new SalariedEmployee(er);
            default:
                throw new InvalidEmployeeType(er.type);
    }
}

이렇게 switch 문은 다형적 객체를 생성하는 코드에 한해서 한번만 참아준다.

서술적인 이름을 사용하라

2장에서도 강조하는 내용이지만 이름을 정하는 것은 아무리 강조해도 지나치지 않다.

이름이 길어도 괜찮다. 길고 서술적인 이름이 길고 서술적인 주석보다 좋다.

함수 인수

이상적인 함수의 인수 개수는 무항(0개)이다. 이항(2개)까지는 괜찮고 삼항은 가급적 피하고 다항(4개 이상)은 특별한 이유가 필요하다.

인수가 3개 이상이 넘어가면 테스트 코드를 짜기에도 상당히 부담스럽다. 인수에 따른 갖가지 조합을 구성해서 테스트 해야 하기 때문이다.

❗️일반적으로 우리는 인수를 입력 인수로 해석한다. 출력 인수를 사용할 일은 거의 없다. 출력 인수가 필요한 경우 사용하는 것이 this 변수이다.

❗️많이 쓰는 단항 형식
1. 인수에 질문을 던지는 경우
2. 인수를 뭔가로 변환해서 결과를 반환하는 경우
3. 입력은 있지만 출력은 없는 이벤트 함수인 경우

위의 경우를 제외하고서 사용하는 단항 함수는 가급적 피하라.
void includeSetupPageInfo(StringBuffer pageText) 와 같이 변환 함수에서 출력 인수를 사용하면 혼란을 일으킨다.

StringBuffer transform(StringBuffer in) 과 같이 입력 인수를 변환하는 함수라면 변환 결과는 반환 값으로 돌려주자.

❗️플래그 인수는 추하다
함수가 한꺼번에 여러가지를 처리한다고 대놓고 공표하는 것이니까
true일 때는 이것, false일 때는 저것을 한다는 말이니까

❗️이항 함수
이항 함수가 무조건 나쁜 것은 아니다. 하지만 그만큼 위험이 따른 다는 것을 인지해야 한다.
예를 들어서 writeField(name)은 writeField(outputStream, name)보다 이해하기 쉽다. 
전자에 비해 후자는 첫번째 인수를 한번 생각해야 하고 무시해도 되는 인자라는 것을 인지한다. 하지만 언제나 무시한 코드에 오류가 숨어들 수 있다.

❗️인수 객체
인수가 2-3개 필요하다면 독자적인 클래스 변수로 선언하는 것을 고려해보자. 

❗️동사와 키워드
좋은 함수 이름은 함수와 인수가 동사/명사 쌍을 이루는 것이다.
writeField(name) 는 이름이라는 필드를 쓴다는 것을 바로 인지할 수 있다.

명령과 조회를 분리해라

함수는 뭔가를 수행하거나 답하거나 둘 중 하나만 해야 한다.

/* 함수 set은 attribute인 속성을 찾아 값을 value로 설정한 후 성공 여부를 반환하는 조회화 명령이 섞인 함수이다.
 * 따라서 아래 Bad 코드와 같이 보기에도 해석하기에도 괴상한 코드가 나온다.
*/
public boolean set(String attribute, String value);

❌ Bad
if(set("username", "unclebob"))...

✅ Good
if(attributeExists("username")) {
    setAttribute("username", "unclebob");
    ...
}

오류 코드보다 예외를 사용하라

오류 코드를 반환하는 형식은 여러 단계로 중첩되는 코드를 야기하고 오류 코드를 곧바로 처리해야 하는 문제에 부딪힌다.

반면 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다.

❌ Bad
if(deletePage(page) == E_OK) {
    if(registry.deleteReference(page.name) == E_OK) {
        if(configKeys.delete(page.name.makeKey()) == E_OK) {
            ...
        }
    }
}

✅ Good
try {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.delete(page.name.makeKey();
} catch (Exception e) {
    logger.log(e.getMessages());
}

✅✅ Better
/*
 * 더 좋은 것은 정상 동작과 오류 처리 동작을 분리하는 것이다.
 * 상위 함수인 delete()에서 모든 오류를 처리하고 정상 동작은 하위 함수인 deletePageAndAllReferences()에서 처리하므로 코드를 이해하고 수정하기 쉬워진다.
*/
public void delete(Page page) {
    try {
        deletePageAndAllReferences(page);
    } catch (Exception e) {
        logger.log(e.getMessages());
    }
}

private void deletePageAndAllReferences(Page page) throws Exception {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.delete(page.name.makeKey();
}

반복하지 마라

중복은 소프트웨어에서 악의 근원이다. 중복은 코드가 길어지고 수정 포인트를 늘리고 그만큼 오류 발생 가능성도 늘어난다.

함수도 글쓰기에 일환이다. 처음 생각을 그대로 기록하고 읽기 좋게 다듬는 과정을 거치는 것이다.
처음에는 길고 복잡하지만 코드를 분리하고 이름을 바꾸고 중복을 제거하고 클래스를 쪼개기도 하는 일련의 과정을 거쳐서 깨끗한 코드를 만들어 나가는 것이다.