Clean Code - 함수

Park Suyong·2022년 1월 6일
0

Study

목록 보기
6/12

작게 만들어라!

"함수를 만드는 규칙은 '작게!' 이다. 각 함수가 명백하고 명확하게 하나의 작업만 수행할 수 있도록 한다."

블록과 들여 쓰기

즉, if 문 / else 문 / while 문 등에 들어가는 블록은 한 줄이어야 한다는 의미이다. 대부분 그곳에서 함수를 호출하게 된다.

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

결론적으로, 중첩 구조가 생길 만큼 함수가 커져서는 안 된다. 그러므로, 함수에서 들여쓰기 수준은 1단 혹은 2단을 넘어서는 안 된다. 그래야 함수는 읽고 이해하기가 쉬워 진다.

한 가지만 해라!

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

그렇다면 한 가지란 무엇일까? 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면, 그 함수는 한 가지 작업만 한다.

우리가 함수를 만드는 이유는 큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서이다.

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

짚고 넘어가기 - 추상화 수준

쉽게 말해 여러 기능을 수행하는 함수를 각각의 기능을 수행하는 함수로 분리하고, 각각의 기능을 수행하는 함수들을 단계별로 수행하는 것을 만들고 연결시켜 준다. 이렇게 되면 각각의 기능을 수행하는 함수가 추상화 수준이 된다.

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

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

한 함수 내에서 추상화 수준이 섞이게 되면 코드를 읽는 사람이 헷갈릴 수 있다. 어떤 표현을 했을 때, 그 표현이 근본적인 것인지 혹은 세부사항과 관련된 것인지 헷갈리기 때문!

내려가기 규칙

코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.

즉, 위에서 아래로 코드를 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다.

Switch 문

"본질적으로 switch 문은 N가지를 처리한다. 따라서 한 가지 작업만 하는 switch 문을 만들기 어렵다. 하지만, 다형성을 사용한다면 각 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. 한 가지 작업만 수행하지 않는다.
  4. 코드를 변경할 이유가 여럿 있으므로 SRP(Single Responsibility Principle)을 위반한다.
  5. 새 직원 유형을 추가할 때마다 코드를 변경해야 하므로 OCP(Open Closed Principle)을 위반한다.
  6. 가장 심각하다고 볼 수 있는 문제로, 위 코드는 급여를 계산하는 함수라고 볼 수 있으나 급여일 함수와 급여 전달 메소드 또한 위와 유사한 구조로 무한정 존재할 수 있다는 것이다.

이를 해결하기 위한 코드를 확인해 보도록 하자. 아래와 같다.

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

public class EmployeeFactorylmpl 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);
        }
    }
}

위 코드의 경우, switch 문을 추상 팩토리에 숨겨 놓은 것을 확인할 수 있다. 팩토리는 switch 문을 사용해 적절한 Employee 파생 클래스의 인스턴스를 생성하게 된다.

이로써 calculatePay, isPayday, deliverPay 등과 같은 함수는 다형성으로 인해 실제 파생 클래스의 함수가 실행되게 된다.

서술적인 이름을 사용하라!

"서술적인 이름은 함수가 하는 일을 좀 더 잘 표현하므로 훨씬 좋은 이름이 된다. 길어도 좋다. 여러 단어를 통해 함수 기능을 잘 표현하는 이름을 선택하자."

이 뿐만 아니라, 이름을 붙일 때는 일관성을 필요로 한다. 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.

함수 인수

"함수의 이상적인 인수의 개수는 0개이다. 1개, 2개 순서다. 3개는 가능한 최대한 피하고, 4개는 특별한 이유가 있어도 사용하지 말자."

코드를 읽는 사람 입장에서도 인수는 없을수록 이해하기 편하다.
includeSetupPageInto(newPageContent) 보다 includeSetUpPage()가 훨씬 이해하기 편하다.

테스트 측면에서도 마찬가지다. 인수가 1개인 경우는 괜찮다. 하지만 2개 이상일 경우 테스트 경우의 수가 더 많아지게 되고, 모든 조합을 고려한 테스트가 어려워지게 될 수 있다.

많이 쓰는 단항 형식

함수에 인수 1개를 넘기는 이유로 가장 흔한 이유는 아래와 같다.

1. 인수에 질문을 던지는 경우
2. 인수를 뭔가로 변환해 결과를 반환하는 경우
3. 추가적으로, 이벤트인 경우 
	* 이벤트라는 사실에 코드에 명확히 드러나야 한다.
    	* 이름과 문맥을 주의해서 선택해야 한다.

위와 같은 이유가 아니라면 단항 함수는 가급적 피해야 한다. 또한, 입력 인수를 변환한다면 그것을 반환해야 혼란을 줄일 수 있다.

플래그 인수

함수로 bool 값을 넘기는 것은 상당히 추하다. 함수가 하나의 일을 처리하지 않고 여러 가지 일을 처리한다는 의미이기 때문!

따라서 bool 값을 처리하는 함수를 만드는 것이 아니라 함수를 또 다시 나눠서 사용하는 것이 좋다.

이항 함수

인수가 1개인 함수보다 이해하기 더 어렵다. 함수에 필요한 인수라면 그것을 전달하는 방식보다 함수 내부에서 선언해서 사용하는 방식을 최대한 사용하자.

다만, 이항 함수가 적절한 경우도 물론 존재한다. 2차원 좌표를 찍어야 하는 경우가 대표적인 그 예시이다.

아주 당연하게 여겨지는 이항 함수 asssertEquals(expected, actual)에도 문제가 있다. 인자의 순서를 헷갈려 에러를 만들어낼 수 있다.

이함 함수가 무조건적으로 나쁘다는 것은 아니지만, 그만큼 위험 부담이 따른다는 것이다. 그렇기에 최대한 단항 함수로 바꾸는 것이 좋다.

삼항 함수

인수가 3개인 함수는 앞서 단항, 이항 함수보다 훨씬 더 어렵다. 야기될 수 있는 문제가 훨씬 더 많기 때문이다.

인수 객체

인수가 2-3개 필요한 경우 그 일부를 독자적인 클래스 변수로 선언할 수도 있을 것이다.

인수 목록

인수의 개수가 가변적인 함수도 필요하다. public String format(String format, Object... args) 와 같은 방식이다. 다만, 가변 인수를 취하는 함수는 단항, 이항, 삼항 함수로만 취급할 수 있다.

동사와 키워드

단항 함수는 동사와 명사가 쌍을 이뤄야 한다. 가령, write(name)은 이름을 쓴다는 의미에서 충분히 좋지만 writeField(name)가 더 좋은 이름이다.

또한, 함수 이름에 키워드를 추가하는 방법도 좋다. 예를 들어, assertEquals 보다 assertExpectedEqualsActual(expected, actual)이 더 좋다. 이러면 인수 순서를 기억할 필요가 없어진다.

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

"부수 효과는 거짓말이다. 이는 시간적인 결합(temporal coupling)이나 순서 종속성(order dependency)을 초래할 수 있다."

시간적인 결합(temporal coupling)이란, 반드시 A 뒤에 B가 와야 한다는 의미이다. 어떤 메소드를 사용할 때 예기치 않게 특정 상황에서는 맞지 않는 메소드를 사용하게 될 수도 있다는 것이다.

명령과 조회를 분리하라!

"함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다. 둘 다 하는 경우 혼란을 초래할 수 있다."

명령과 조회를 분리한다면 혼란 자체를 뿌리뽑을 수 있다.

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

"오류 코드를 사용하는 것보다 예외 처리를 사용하는 것이 좋다."

try catch 블록을 사용하면 기존의 에러 코드를 사용하는 것보다 코드가 깔끔해 진다. 하지만, try catch를 사용하면 코드 구조에 혼란을 일으킬 수 있다. 따라서, try catch 블록을 별도 함수로 뽑아내는 편이 좋다.

반복하지 마라!

"중복은 모든 소프트웨어에서 악의 근원이다. 코드 길이가 늘어날 뿐 아니라 알고리즘이 변하게 될 경우 여러 곳을 손봐야 한다."

구조적 프로그래밍

"함수가 아주 큰 경우가 아니라면 return, break, continue를 여러 번 사용해도 괜찮다. 다만, goto는 절대 안된다."

profile
Android Developer

0개의 댓글