STUDY/Books

[Clean Code] 3장 : 함수

hyunah 2021. 5. 11. 14:50

Clean Code 스터디 내용 정리; 3장 함수


함수를 잘 만드는 법을 소개한다. 의도를 분명히 표현하는 함수, 읽는 사람이 프로그램 내부를 직관적으로 파악할 수 있는 함수를 구현하려면 어떻게 해야 할까?






한 가지 일만 해야 한다.



1. 함수 내 모든 문장의 추상화 수준이 동일하게 한다.

이때 추상화란, 구체적인 것을 감추고, 보고 싶어하는 전체적인 특성을 드러내는 것이다. 예를 들어, 파일에 이름을 붙여 파일 내부를 보지 않더라도 그 파일의 역할이 무엇인지 파악할 수 있게 하는 것이나, 함수에 이름을 붙여 함수가 구체적으로 하는 일을 함수의 이름으로 대표해서 표현하는 것이 추상화에 해당한다.

한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈리게 된다. 특정 표현이 구체적인 개념인지 추상화된 개념인지 구분하기 어렵기 때문이다. 따라서, 함수 내에서는 추상화 수준이 하나인 단계만 수행해야 한다. 즉, 함수가 오직 한 가지 일만 하게 만들어야 한다.

// 추상화 수준 높음, html code를 가져오는 모든 과정을 대표함.
getHtml()

// 추상화 수준 낮음, "\n"라는 문자열을 끝에 붙이라는 구체적인 행위.
.append("/n")




2. 부수 효과를 일으키지 않는다.

  • 부수 효과는 함수에서 한 가지를 하겠다고 해놓고선 남몰래 다른 일까지 하는 해로운 거짓말이다.
  • 부수 효과는 시간적 제약을 만들어 함수가 특정 상황에만 호출이 가능하게 만든다. 이런 시간적 결합은 혼란을 일으키므로, 함수 이름에 분명히 명시해야 한다. 그렇지만 그런 경우에는 함수가 두 가지 이상의 일을 하게 된다. 그러므로 그냥 부수 효과를 일으키지 않아야 한다.


<예시>

1. checkPassword 함수 내에서 Session.initialize()를 호출한다.
2. 세션을 초기화해도 괜찮은 경우에만 호출이 가능해진다.
3. checkPasswordAndInitializeSession이라는 이름으로 변경한다.
4. 함수가 두 가지 이상의 일을 한다는 게 드러난다.
5. checkPassword 함수 내에서 Session.initialize()를 호출하지 않는다.





3. 명령과 조회를 분리한다.

  • 함수가 할 수 있는 행위는 객체 상태를 변경(명령)하거나 객체 정보를 반환(조회)하는 것이다.
  • 명령을 내리는 동시에 정보를 반환하게 만들면 함수의 역할에 혼란을 주기에, 명령과 조회를 분리해 혼란을 뿌리 뽑는 게 좋다.

// "username"이 "unclebob"으로 설정되어 있는지 확인하는 코드인지, 설정하는 코드인지 의미가 모호하다. (x)
if (set("username","unclebob"))... 

// 명령과 조회를 분리한다. (o)
if (attributeExists("username")) {
    setAttribute("username","unclebob");
}




4. 오류 처리를 분리한다.

  • 오류 처리도 한 가지 작업이기 때문에 정상 동작과 오류 처리 동작을 분리하여 오류를 처리하는 함수(try/catch)는 오류만 처리하게 만드는 것이 좋다.
  • 오류 코드보다 예외를 사용해야 오류 처리 코드를 원래 코드에서 분리할 수 있다.






간결하게 만든다.



1. 최대한 작게 만든다.

  • 함수의 if문/else문/while문 등에 들어가는 블록은 한 줄이어야 한다.
  • 중첩 구조가 생길만큼 함수가 커져서는 안 되며, 함수에서 들여쓰기 수준은 1단이나 2단을 넘어서면 안 된다.
  • 함수는 작게 만들수록 좋다.




2. 최대한 적은 인수를 사용한다.

  • 인수는 함수의 개념을 이해하기 어렵게 만들기 때문에 함수에서 인수는 적을수록 좋다.
  • 출력 인수와 플래그 인수는 최대한 피한다.
  • 이항, 삼항 함수는 개념을 이해하기가 더 어렵고 더 많은 위험이 따르기에 가능하면 일부를 독자적인 클래스 변수로 선언하는 방법을 사용해 단항 함수로 바꾸도록 한다.




3. 반복을 줄인다.

  • 중복되는 알고리즘이 있으면 코드 길이가 늘어날 뿐 아니라 알고리즘이 변했을 때 여러 곳에서 수정을 해야 해서 번거롭고 오류가 발생할 확률이 높다.
  • 여러 번 반복되는 알고리즘이 보인다면 반드시 중복을 없앤다.
  • switch문은 애초에 여러 가지를 처리하기에 작게 만들기도 어렵고 알고리즘이 중복되기 쉽다.
  • switch문을 저차원 클래스에 숨기고 절대 반복하지 않는 방법으로 사용하는 게 좋다.

// 각각의 함수에 Employ의 type에 따라 다른 함수를 호출하는 switch문 존재. 
// 동일한 구조의 함수가 중복됨 (x)
calculatePay(Employee e);
isPayday(Employee e, Date date);

// switch문을 추상 팩토리에 넣고 다형성을 사용함 (o)
public abstract class Employee {
    public abstract boolean isPayday();
    public abstract Money calculatePay();
}

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 HourlyEployee(r);
                ...
            default:
                throw new InvalidEmployeeType(r.type);
        }
    }
}






이름을 잘 짓는다.



1. 서술적인 이름을 사용한다.

  • 이름이 길어도 좋으니 여러 단어가 쉽게 읽히는 명명법을 사용하여 함수 기능을 잘 표현하는 이름을 만든다.
  • 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용하여 일관성을 유지한다.




2. 인수와의 관계를 고려한다.

  • 단항 함수인 경우, 함수와 인수가 동사/명사 쌍을 이루도록 한다.
  • 함수 이름에 인수 이름을 넣어 인수 순서를 기억할 필요가 없게 만드는 것도 좋다.

writeField(name) // (o)

assertEquals(extected, actual) // (△)
assertExpectedEqualsActual(expected, actual) // (o)





소프트웨어를 짜는 행위는 글짓기와 비슷하다. 한 번에 완벽하게 쓰려고 하지 말고, 먼저 생각을 투박하게 기록한 후에 퇴고하는 과정을 거친다. 함수를 짤 때도 처음에는 길고 복잡하고 더럽게 쓴 후에 코드를 다듬고 이름을 바꾸고 중복을 제거하여 최종적으로는 규칙을 따르는 함수가 얻어진다.

프로그래밍의 기술은 언제나 언어 설계의 기술이다. 진짜 목표는 시스템이라는 이야기를 풀어가는 데 있다는 사실을 명심하고, 함수가 분명하고 정확한 언어로 깔끔하게 맞아떨어져야 이야기를 풀어가기가 쉬어진다는 사실을 기억하자.

'STUDY > Books' 카테고리의 다른 글

[Clean Code] 6장 객체와 자료 구조  (0) 2021.05.21
[Clean Code] 5장 형식 맞추기  (0) 2021.05.18
[Clean Code] 4장 주석  (0) 2021.05.16
[Clean Code] 2장 : 의미 있는 이름  (0) 2021.05.07
[Clean Code] 1장 : 깨끗한 코드  (0) 2021.05.05