From 379aabd5177fbab30057bbdaa66d6bd52888fa3d Mon Sep 17 00:00:00 2001 From: "now.water" Date: Thu, 31 Mar 2022 22:54:10 +0900 Subject: [PATCH] =?UTF-8?q?clean=20code=203=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/books/cleancode/3.md | 273 +++++++++++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 content/books/cleancode/3.md diff --git a/content/books/cleancode/3.md b/content/books/cleancode/3.md new file mode 100644 index 0000000..f4f821f --- /dev/null +++ b/content/books/cleancode/3.md @@ -0,0 +1,273 @@ +--- +title: '3장. 함수' +metaTitle: '만렙 개발자 키우기' +order: 2 +tags: ['Book'] +date: '2022-03-29' +--- + +## 작게 만들어라 + +> if, else, while 등에 들어가는 블록은 한 줄이어야 한다. + +중첩 구조가 생길만큼 함수가 커져서는 안된다. 또한 함수에서 들여쓰기 수준은 1단이나 2단을 넘어서면 안된다. + +그래야 함수는 읽고 이해하기 쉬워진다. + +--- + +## 한 가지만 해라 + +> 함수가 한 가지 작업을 수행한다는 것은, 추상화 수준이 하나인 단계만을 수행하는 것이다. + +**함수를 만드는 이유는 큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서다.** + +예를 들어, + +```kotlin +if (isTestPage) { + includeSetupsAndTeardown(pageData, isSuite) +} +``` + +라는 작업을 수행하는데, + +```kotlin +includeSetupsAndTeardownIfTestPage(pageData, isSuite) +``` + +라는 함수로 묶는다고 해서 추상화 수준은 바뀌지 않는다. 단지 똑같은 내용을 다르게 표현할 뿐. + + + +**함수가 한 가지만 하는지 판단하는 방법** + +: 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 것! + +한 가지만 하는 함수는 자연스럽게 여러 섹션으로 나누기 어렵다. + +--- + +## 함수 당 추상화 수준은 하나로 + +> 함수가 확실히 **한 가지** 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다. + +추상화 수준 예시 : `getHtml()` > `PathParser.render(pagePath)` > `.append("\n")` + +한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다. 왜냐하면 특정 표현이 근본 개념인지 아니면 세부사항인지 구분하기 어렵기 때문 + +근본 개념과 세부사항이 뒤섞이기 시작하면, 함수에 세부사항이 점점 더 추가되는 것이 가장 큰 문제 ! + + +**내려가기 규칙** + +위에서 아래로 이야기처럼 읽히는 것이 좋은 코드. 즉, 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 와야 함. + +**핵심**: 짧으면서도 한 가지만 하는 함수 + +--- + +## Switch 문 + +본질적으로 Switch 문은 N 가지를 처리한다. 하지만 Switch 문을 저차원 클래스에 숨기고 다형성을 이용해 반복하지 않도록 할 수 있다. + +책에서는 `Employee` 의 타입에 따라 `연봉` 을 계산하기 위해 Switch 구문을 예시로 들었는데, 이러한 경우 `Employee` 를 추상 클래스로 만들고 +각 타입의 `Employee` 들을 추상 팩토리 내부의 Switch 문을 통해 만들도록 바꾸었다. + +그 이후에 각각 파생 클래스에서 여러 공통 메소드들을 새로 재정의해서 사용함으로써, 실제 로직 상에 Switch 사용을 숨긴다는 의미로 나는 이해하였다. + +(그런데 Kotlin 이나 Typescript 를 사용하면 Switch 를 잘 쓸 일이 없을 거 같다는 생각을 했다. 점점 안 쓰는 추세인듯) + +--- + +## 서술적인 이름을 사용하라 + +> 코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드 + +한 가지만 하는 작은 함수를 만들고, 거기에 좋은 이름을 붙인다면 이러한 원칙을 절반은 성공 + +길고 서술적인 이름이 짥고 어려운 이름보다 좋다. 함수가 작고 단순할수록 서술적인 이름을 고르기 쉬워지며, 서술적인 이름은 서술적인 주석보다 좋다. + +서술적인 이름을 사용하면 개발자의 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다. + +그리고 이름을 붙일 때는 일관성이 있어야 한다. 문체가 비슷하면 이야기를 순차적으로 풀어가기 쉽기 때문 + +--- + +## 함수 인수 + +> 함수에서 이상적인 인수 개수는 0개(무항) + +인수는 개념을 이해하기 어렵게 만드므로, 최선은 입력 인수가 없는 경우이며 차선은 입력 인수가 1개 뿐인 경우다. + +**단항 형식** +> - 인수에 질문을 던지는 경우 +> +> - 인수를 뭔가로 변환해 결과를 반환하는 경우 +> +> - 이벤트 함수(드물게 사용) : 입력 인수로 시스템 상태를 바꾸는데, 이때는 이벤트라는 사실이 코드에 명확히 드러나야 한다. + + +**플래그 인수** + +> - 함수로 boolean 값을 넘기는 관례는 좋지 않다 -> 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈 + + +**이항 함수** + +> - 인수가 어떤 의미를 갖는지 이해해야 하는 시간이 필요하다. +> +> - 물론 적절한 경우도 존재 - 인수 간에 자연적인 순서가 있는 경우. ex) Point p = new Point(0, 0) +> +> - 하지만 인수 간에 순서를 잘못 이해할 가능성이 있으므로, 가능한 단항 함수로 바꿔서 써야 한다. + +**인수 객체** + +> - 인수가 2-3개 필요하다면 일부를 별도의 클래스 변수로 선언하는 것을 고려하기 + +변경 전 +```kotlin +fun makeCircle(x: double, y: double, radius: double) +``` + +변경 후 +```kotlin +fun makeCircle(center: Point, radius: double) +``` + +변수를 묶어 넘김으로써 결국은 이름을 붙여서 개념을 표현하게 됨! + +**동사와 키워드** + +> - 단항 함수는 함수와 인수가 동사/명사 쌍을 이루도록 이름 짓기 +> +> - 함수 이름에 키워드를 추가하여 인수 순서를 설명 (ex. `assertExpectedEqualsActual(expected, actual)`) + + +--- + +## 부수 효과를 일으키지 마라 + +> 부수 효과가 있는 함수는 `시간적 결합(temporal coupling)` 이나 `순서 종속성(order dependency)` 를 초래한다. + +만약 시간적인 결합이 필요하다면 (특정 상황에서만 호출 가능하다면), 함수 이름에 분명히 명시하자 + +**실제 사례:** + +`sendTalkMessage` 메서드 이름만 보고, 내부에서 ActionGroup 의 상태값을 바꿀 줄 예상하지 못했음..! + +```kotlin +fun sendTalkMessage(...) { + // DB 조회 후 모든 액션이 처리가 되었으면 액션 그룹을 Complete 후 메시지 전송 + if (checkAndCompleteActionGroup(...)) { + return + } + talkMessageService.sendMultiEarnMessage(...) +} +``` + +
수정 후 + +```kotlin +if (checkCompleted(actionGroup)) { + actionGroup.complete() + sendTalkMessage(actionGroup, kaffeineEarnAction) +} +``` + +
+ + +**출력 인수** + +일반적으로 우리는 인수를 **함수 입력**으로 해석한다. + +따라서 인수를 출력 인수로 사용하면 함수 선언부를 찾아봐야 하고, 그러한 행위는 인지적으로 거슬리게 된다. (코드를 보다가 주춤하는 행위) + +그러므로 출력 인수는 피해야 하고, **함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경**하도록 하자. + +--- + +## 명령과 조회를 분리하라 + +> 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다. + +즉, `객체 상태 변경` or `객체 정보 반환` 중 하나만 해야 한다. + +다음과 같은 메서드가 있다고 가정하자. +```kotlin +if (set("username", "now.water")) +``` + +그러면 두 가지 해석이 가능해진다. + +1. username 이 now.water 로 설정되어 있는지 확인하는 함수 +2. username 을 now.water 로 설정하는 함수 + +함수 호출 코드만 봐서는 의미가 모호하다. + +따라서 이러한 혼란을 해결하기 위해서는 명령과 조회를 분리해야 한다. + +**수정된 코드** +```kotlin +if (attributeExists("username")) { + setAttribute("username", "now.water") +} +``` + + +--- + + +## 오류 코드보다 예외를 사용하라 + +명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다. 왜냐하면 명령을 표현식으로 사용하기 쉽기 때문 + +ex. if (deletePage(page) == E_OK) + +이렇게 오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야만 한다. + +에러 코드를 사용하면 `Error enum` 을 사용하게 될텐데, `Error enum` 이 변하게 되면 이를 사용하는 클래스 전부를 다시 컴파이랗고 다시 배치해야 함 -> Error 클래스 변경이 어려워짐 + +반면 오류 코드 대신 **예외**를 사용하면 새 예외는 Exception 클래스에서 파생되기 때문에 재컴파일/재배치 없이도 새 예외 클래스를 추가 가능. + +또한 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다. + +이때 try-catch 블록에서 실행하는 메서드들을 별도 함수로 뽑아내어 사용하는 것이 좋다. -> 정상 동작과 오류 처리 동작을 분리하여 코드를 이해하고 수정하기 쉽게 만듬 + +--- + +## 반복하지 마라 + +중복된 코드를 사용하게 되면 코드 길이가 늘어날 뿐만 아니라, 알고리즘이 변할 경우 사용하는 모든 곳을 수정해줘야 한다. 이때 어느 한 곳이라도 빠뜨리면 오류가 발생.. (오류 가능성이 높음) + +중복은 소프트웨어에서 모든 악의 근원. 중복을 없애면 모듈 가독성이 크게 높아진다. + +--- + +## 구조적 프로그래밍 + +> 함수는 return 문이 하나여야 한다. + +루프 안에서 break 나 continue 를 사용해서는 안된다 (.. ?) -> 함수가 아주 클 때에 적용 + +함수를 작게 만든다면 간혹 return, break, continue 를 여러 차례 사용해도 괜찮다. + +--- + +## 함수를 짜는 방법 + +처음에는 함수를 만들 때 길고 복잡하게 만들게 되지만, 코드를 다듬으면서 이름도 바꾸고 중복을 제거하고 전체 클래스를 쪼개기도 한다. 이 와중에 항상 단위 테스트는 통과하도록. + +처음부터 깔끔한 함수를 만드는건 어렵다. + +함수는 그 언어에서 동사며, 클래스는 명사. + +Master Programmer 는 시스템을 구현할 프로그램이 아니라 풀어갈 이야기로 여긴다. 이때 프로그래밍 언어라는 수단을 사용해 좀 더 풍부하고 표현력이 강한 언어를 만들며 이야기를 풀어간다. + +시스템에서 발생하는 모든 동작을 설명하는 함수 계층이 바로 그러한 언어! + +진짜 목표는 시스템이라는 이야기를 풀어가는 데 있다는 사실을 명심하자 + +함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아떨어져야 이야기를 풀어가기 쉬워진다. +