본문 바로가기
클린코드

3. 함수

by 문자메일 2022. 10. 24.

작게 만들어라

함수는 작게 만들수록 좋다.

'''켄트가 코드를 보여줬을 때 나는 함수가 너무도 작아 깜짝 놀랐다. 그때까지 나는 장황하게 긴 스윙 프로그램 함수에 익숙했다. 그런데 Sparkle은 모든 함수가 2줄, 3줄, 4줄 정도였다. 각 함수가 너무도 명백했다. 각 함수가 이야기 하나를 표현했다. 각 함수가 너무도 멋지게 다음 무대를 준비했다. 이것이 답이다!'''

 

블록과 들여쓰기

if / else / while 문 등에 들어가는 블록은 한 줄이어야 한다.

그러면 바깥을 감싸는 함수가 작아질 뿐 아니라, 블록 안에서 호출하는 함수의 이름을 적절히 짓는다면, 코드를 이해하기도 쉬워진다.

이 말은 중첩 구조가 생길만큼 함수가 커져서는 안 된다는 뜻이다. 그러므로 함수에서 들여쓰기 수준은 1단이나 2단을 넘어서면 안 된다. 당연한 말이지만, 그래야 함수는 읽고 이해하기 쉬워진다.

 

한 가지만 해라

함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한가지만을 해야 한다.

지정한 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다.

함수가 '한 가지'만 하는지 판단하는 방법이 하나 더 있다. 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면, 그 함수는 여러 작업을 하는 셈이다.

 

함수 당 추상화 수준은 하나로

함수가 확실히 '한 가지' 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.

 

위에서 아래로 코드 읽기: 내려가기 규칙

코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다. 이것을 내려가기 규칙이라 한다.

 

 

Switch 문

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);
    }
}

위 함수에는 몇 가지 문제가 있다.

1. 함수가 길다. 새 직원 유형을 추가하면 더 길어진다.

2. '한 가지' 작업만 수행하지 않는다.

3. SRP를 위반한다. (2번과 같은 내용 아닌가?)

4. OCP를 위반한다. 새 직원 유형을 추가할 때마다 코드를 변경하기 때문이다.

    하지만 아마 가장 심각한 문제라면 위 함수와 구조가 동일한 함수가 무한정 존재한다는 사실이다.

    예를 들어, 다음과 같은 함수가 가능하다.

    isPasyday(Employee e, Date date);

    또는

    deliverPay(Employee e, Money pay);

   가능성은 무한하다. 그리고 모두가 똑같이 유해한 구조다.

 

위 문제를 해결한 코드가 아래 코드이다.

아래 코드는 switch문을 추상 팩토리에 꼭꼭 숨긴다. 팩토리는 switch문을 사용해 적절한 Employee 파생 클래스의 인스턴스를 생성한다.

calculatePay, isPayday, deliverPay 등과 같은 함수는 Employee 인터페이스를 거쳐 호출된다,

그러면 다형성으로 인해 실제 파생 클래스의 함수가 실행된다.

 

public abstract class Employee{
	public abstract boolean isPayday();
    public abstract Money calculatePay();
    public abstract void deliverPay();
}

public interface EmployeeFactory{
	public Employee makeEmployee(EmployeeRecord r) throw InvalidEmployeeType;
}

public class EmployeeFactoryImpl implements EmployeeFactory{
	public Employee makeEmployee(EmployeeRecord r) throw 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);
        }
    }
}

 

 

서술적인 이름을 사용하라

코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드로 불러도 된다.

이름이 길어도 괜찮다.

길고 서술적인 이름이 짧고 어려운 이름보다 좋다.

길고 서술적인 이름이 길고 서술적인 주석보다 좋다.

함수 이름을 정할 때는 여러 단어가 쉽게 읽히는 명명법을 사용한다.

그런 다음, 여러 단어를 사용해 함수 기능을 잘 표현하는 이름을 선택한다.

서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.

 

 

 

함수 인수

함수에서 이상적인 인수 개수는 0개(무항)다.

다음은 1개(단항)거, 다음은 2개(이항)다.

3개는 가능한 피하는 편이 좋고, 4개 이상은 특별한 이유가 필요하다.

 

'인수는 어렵다. 인수는 개념을 이해하기 어렵게 만든다.'

 

테스트 관점에서 보면 인수는 더 어렵다. 

인수가 없다면 간단하다. 1개~2개면 조금 복잡하다. 3개를 넘어서면 유효한 값으로 모든 조합을 구성해 테스트하기 상당히 부담스러워진다.

 

 

많이 쓰는 단항 형식

함수에 인수 1개를 넘기는 이유로 가장 흔한 경우는 두 가지다.

하나는 인수에 질문을 던지는 경우다.

boolean fileExists("MyFile")이 좋은 예다.

다른 하나는 인수를 뭔가로 변환해 반환하는 경우다.

InputStream fileOpen("MyFile")은 String 형의 파일 이름을 InputStream으로 변환한다.

 

단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다. 예를들어 write(name)은 누구나 곧바로 이해된다.

 

플래그 인수

플래그 인수는 추하다. 왜냐하면 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 표현하고 있기 때문이다.

ex) 플래그가 참이면 이걸 하고, 거짓이면 저걸 한다는 말

 

 

이항 함수, 삼항 함수

함수 인수가 많을 수록 이해하기 어려워지니 가능하면 함수 인수는 줄이는게 좋다.

인수가 2~3개 필요하다면 아래처럼 인수 묶음을 독자적인 클래스 변수로 바꿀 가능성을 찾아서 바꾸는게 좋다.

x, y를 Point라는 이름으로 묶으면서 사람이 이해하기 원할한 개념을 포함하게 된다.

 

AS-IS : Circle makeCircle(double x, double y, double radius);

TO-BE : Circle makeCircle(Point center, double radius);

 

 

부수 효과를 일으키지 마라!

부수 효과는 거짓말이다. 함수에서 한 가지를 하겠다고 약속하고선 다른 일을 추가로 하면 (다른 클래스 변수를 수정한다던가..) 해당 로직을 이해하는데 더 많은 노력이 들고, 다른 사람이 잘못 이해해서 사용하여 문제가 발생할 가능성도 높아진다.

 

 

명령과 조회를 분리하라!

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

객체 상태를 변경하거나 아니면 객체 정보를 반환하거나 둘 중 하나다.

아래와 같은 메서드는 이 원칙을 적용하면 잘못 된 것이다.

public boolean set(String attribute, String value);

 

 

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

오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야 한다는 문제에 부딪힌다.

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

 

 

Try/Catch 블록 뽑아내기

try/catch 블록은 원래 추하다. 코드 구조에 혼란을 일으키며, 정상 동작과 오류처리 동작을 뒤섞는다.

그러므로 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());
}

 

위에서 delete 함수는 모든 오류를 처리한다. 그래서 코드를 이해하기 쉽다.

한 번 훑어보고 넘어가면 충분하다. 

이렇게 정상 동작과 오류 처리 동작을 분리하면 코드를 이해하고 수정하기 쉬워진다.

 

오류 처리도 한 가지 작업이다.

함수는 '한 가지' 작업만 해야 한다. 오류 처리도 '한 가지' 작업에 속한다.

그러므로 오류를 처리하는 함수는 위 예제처럼 오류만 처리해야 마땅하다.

따라서 함수에 키워드 try가 있다면 함수는 try 문으로 시작해 catch/finally 문으로 끝나야 한다.

 

 

반복하지 마라!

중복은 소프트웨어에서 모든 악의 근원이다. 많은 원칙과 기법이 중복을 없애거나 제어할 목적으로 나왔다.

'클린코드' 카테고리의 다른 글

10. 클래스  (1) 2022.10.29
6. 객체와 자료구조  (1) 2022.10.27
5. 형식 맞추기  (0) 2022.10.26
1. 깨끗한 코드  (1) 2022.10.23
2. 의미있는 이름  (0) 2022.10.21

댓글