본문 바로가기
독서/클린 코드

클린 코드 (Clean Code) - 3장 함수

by Sondho 2022. 2. 4.

노션 정리

 

3장 함수

책에서 기억하고 싶은 내용을 써보세요.

sondho.notion.site

 

😃 책에서 기억하고 싶은 내용을 써보세요.

작게 만들어라!

  • 작은 함수가 좋다.

블록과 들여쓰기

  • if 문 / else 문 / while 문 등에 들어가는 블록은 한 줄이어야 한다. 대개 거기서 함수를 호출한다. 그러면 바깥을 감싸는 함수(enclosing function)가 작아질 뿐 아니라, 블록 안에서 호출하는 함수 이름을 적절히 짓는다면, 코드를 이해하기도 쉬워진다.
  • 중첩 구조가 생길만큼 함수가 커져서는 안 된다.

한 가지만 해라!

  • 함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다.
  • 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다.
  • 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다.
  • 함수는 간단한 *TO 문단으로 기술할 수 있다.
    • *TO: LOGO 언어에서 사용하는 키워드 ‘TO’는 루비나 파이썬에서 사용하는 ‘def’와 똑같다. LOGO에서 모든 함수는 키워드 ‘TO’로 시작한다. 이는 함수를 설계하는 방식에 흥미로운 영향을 미쳤다.
  • 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다. [G34]
  • G34: 함수는 추상화 수준을 한 단계만 내려가야 한다.
    • 함수 내 모든 문장은 추상화 수준이 동일해야 한다. 그리고 그 추상화 수준은 함수 이름이 의미하는 작업보다 한 단계만 낮아야 한다.
  • 함수 내 섹션
    • 한 가지 작업만 하는 함수는 자연스럽게 섹션으로 나누기 어렵다.

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

  • 함수가 확실히 ‘한 가지’ 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.
  • 한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다. 특정 표현이 근본 개념인지 아니면 세부사항인지 구분하기 어려운 탓이다.
  • 위에서 아래로 코드 읽기: 내려가기 규칙
    • 위에서 아래로 프로그램을 읽으면서 함수 추상화 수준이 한 번에 한 단계씩 낮아진다.(내려가기 규칙)
    • TO 문단을 읽듯이 프로그램이 읽혀야 한다는 의미다.

Switch 문

  • 다형성(polymorphism)을 이용하면 저차원 클래스에 숨기고 절대로 반복하지 않을 수 있다.
  • 일반적으로 나는 switch문을 단 한 번만 참아준다. 다형적 객체를 생성하는 코드 안에서다. 이렇게 상속 관계로 숨긴 후에는 절대로 다른 코드에 노출하지 않는다.

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

  • 한가지만 하는 작은 함수에 좋은 이름을 붙인다면 이런 원칙을 달성함에 있어 이미 절반은 성공했다. 함수가 작고 단순할수록 서술적인 이름을 고르기도 쉬워진다.
  • 길고 서술적인 이름이 짧고 어려운 이름보다 좋다. 길고 서술적인 이름이 길고 서술적인 주석보다 좋다.
  • 함수 이름을 정할 때는 여러 다어가 쉽게 읽히는 명명법을 사용한다. 그런 다음, 여러 단어를 사용해 함수 기능을 잘 표현하는 이름을 선택한다.

함수 인수

  • 이상적인 개수는 0개(무항)다. 다음은 1개(단항)고, 다음은 2개(이항)다. 3개(삼항)는 가능한 피하는 편이 좋다. 4개 이상(다항)은 특별한 이유가 필요하다. 특별한 이유가 있어도 사용하면 안 된다.
  • 코드를 읽는 사람이 인수를 발견할 때마다 해석해야한다.
  • 함수 이름과 인수 사이에 추상화 수준이 다를 수 있다.
  • 코드를 읽는 사람이 현 시점에서 별로 중요하지 않은 세부사항을 알아야한다.
  • 테스트 관점에서 보면 갖가지 인수 조합으로 함수를 검증 하는 테스트 케이스를 작성할 때 보다 인수가 없을 때 더 간단하다.
  • 출력 인수는 입력 인수보다 이해하기 어렵다.
  • 많이 쓰는 단항 형식
    • 인수에 질문을 던지는 경우. e.g. boolean fileExists(”MyFile”)
    • 인수를 뭔가로 변환해 결과를 반환하는 경우.
    • e.g. InputStream fileOpen(”MFile”)은 String 형의 파일 이름을 InputStream으로 변환한다.
    • 다소 드물게 사용하지만 그래도 아주 유용한 단항 함수 형식이 이벤트다. 이벤트 함수는 입력 인수만 있다. 출력 인수는 없다.
    • 이벤트 함수에서 조심해서 사용해야한다. 이벤트라는 사실이 코드에 명확히 드러나야 한다. 그러므로 이름과 문맥을 주의해서 선택한다.
    • 변환 함수에서 출력 인수를 사용하면 혼란을 일으킨다. 입력 인수를 변환하는 함수라면 변환 결과는 반환값으로 돌려준다.
  • 플래그 인수
    • 함수로 부울 값을 넘기는 것은 함수가 여러 가지를 처리한다고 말하는 것과 같다.
  • 이항 함수
    • writeField(name)는 writeField(outputStream, name)보다 이해하크기 쉽다.
    • 좌표계의 경우는 이항 함수가 적절하다.
    • 이항 함수가 무조건 나쁘다는 소리는 아니다. 프로그램을 짜다보면 불가피한 경우가 생긴다. 하지만 그만큼 위험이 따른다는 사실을 이해하고 가능하면 단항 함수로 바꾸도록 애써야 한다.
  • 삼항 함수
    • 삼항 함수를 만들 때는 신중히 고려하라 권고한다.
    • 삼항 함수가 필요하다면 순서가 중요하다.
  • 인수 객체
    • 인수가 2-3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성을 짚어 본다.
    • e.g. Circle makeCircle(double x, double y, double radius); Circle makeCircle(Point center, double radius);
  • 인수 목록
    • 때로는 인수 개수가 가변적인 함수도 필요하다.
  • 동사와 키워드
    • 함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다.
    • 단항 함수는 함수와 인수가 동사/명사 쌍으로 이뤄야 한다.
    • 함수에 키워드를 추가하는 방식 assertEquals보다 assertExpectedEqualsActual(expected, actual)이 더 좋다. 그러면 인수 순서를기억할 필요가 없어진다.

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

  • checkPassword함수에 session.initialize()와 같은 호출은 하지마라 함수 이름만 보고 함수를 호출하는 사용자는 사용자를 인증하면서 기존 세션 정보를 지워버릴 위험에 처한다.
  • 만약 시각적인 결합이 필요하다면 함수 이름에 분명히 명시한다.
  • 출력 인수
    • 일반적으로 우리는 인수를 함수 입력으로 해석한다.
    • 객체 지향 언어에서는 출력 인수를 사용할 필요가 거의 없다. 출력 인수로 사용하라고 설계한 변수가 바로 this이기 때문이다.
    • 일반적으로 출력 인수는 피해야한다. 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다.

명령과 조회를 분리하라!

  • 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다. 객체 상태를 변경하거나 아니면 객체 정보를 반환하거나 둘 중 하나다.
  • e.g.
  • public boolean set(String attribute, String value); // 변경 전 if (set(”username”, “unclebob”))... ⇒ 동사/형용사의 혼란을 일으킨다. // 변경 후 if (attributeExists(”username”)){ setAttribute(”username”, “unclebob”); ... }

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

  • 명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다.
  • if (deletePage(page) == E_OK) 위 코드는 동사/형용사 혼란을 일으키지 않는 대신 여러 단계로 중첩되는 코드를 야기한다. 오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야한다는 문제에 부딪힌다.
  • Try / Catch 블록 뽑아내기
    • 블록을 별도 함수로 뽑아내는 편이 좋다.
  • 오류 처리도 한 가지 작업이다.
    • 함수는 ‘한 가지’ 작업만 해야 한다. 오류 처리도 ‘한 가지’ 작업에 속한다. 그러므로 오류를 처리하는 함수는 오류만 처리해야 마땅하다.
  • *Error.java 의존성 자석*
    • 오류 코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생된다. 따라서 재컴파일 / 재배치 없이도 새 예외 클래스를 추가할 수 있다.

반복하지 마라!

  • 코드가 미묘하게 달라 중복이 금방 드러나지 않더라도 중복은 문제다. 코드 길이를 늘어날 뿐 아니라 알고리즘이 변하면 중복된 코드를 전부 수정해야 한다.

구조적 프로그래밍

  • 에츠허르 데이크스트라 (Edsger Dijkstra)의 구조적 프로그래밍 원칙 ⇒ 모든 함수는 return 문이 하나여야 한다. 루프 안에서 break나 continue를 사용해선 안되며 goto는 절대로 안 된다.
  • 함수를 작게 만든 다면 간혹 return, break, continue를 여러 차례 사용해도 괜찮다. 오히려 때로는 단일 입 / 출구 규칙(single entry-exit rule)보다 의도를 표현하기 쉬워진다. 반면, goto 문은 큰 함수에서만 의미가 있으므로, 작은 함수에서는 피해야 한다.

함수를 어떻게 짜죠?

  • 소프트웨어를 짜는 행위는 여느 글짓기와 비슷하다.
  • 함수 짤 때도 마찬가지다. 처음에는 길고 복잡하다. 들여쓰기 단계도 많고 중복된 루프도 많다. 인수 목록도 아주 길다. 이름은 즉흥적이고 코드는 중복된다. 하지만 그 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스도 만든다. 그런 다음 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다. 메서드를 줄이고 순서를 바꾼다. 때로는 전체 클래스를 쪼개기도 한다. 이 와중에도 코드는 항상 단위 테스트를 통과한다.
  • 처음부터 탁 짜내지 않는다. 그게 가능한 사람은 없으리라.

결론

  • 모든 시스템은 특정 응용 분야 시스템을 기술할 목적으로 프로그래머가 설계한 도메인 특화 언어(Domain Specific Language, DSL)로 만들어 진다.
  • 함수는 그 언어에서 동사며, 클래스는 명사다.
  • 프로그래밍의 기술은 언제나 언어 설계의 기술이다.
  • 대가(master) 프로그래머는 시스템을 (구현할) 프로그램이 아니라 (풀어갈) 이야기로 여긴다. 프로그래밍 언어라는 수단을 사용해 좀 더 풍부하고 좀 더 표현력이 강한 언어를 만들어 이야기를 풀어간다.

 

🤔 오늘 읽은 소감은? 떠오르는 생각을 가볍게 적어보세요.

  • 한 번 다 읽더라도 일정한 시간을 두고 계속 정독해야 할 것 같다.
  • 이제 한 번 읽고 있지만 코드를 작성하면서 좀 더 많은 부분을 고려할 수 있게 해주었다.
  • 나의 가장 안 좋은 습관 중 하나가 코드를 작성하기 전에 너무 많은 부분을 생각해서 시작을 하지 못하는 것이다. ’13. 함수를 어떻게 짜죠?’ 를 읽고 내가 코드를 구현하는 속도가 왜 느린지 알게 되었다.

 

🔎 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.

  • p.47 Switch 문

댓글