-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
273 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(...) | ||
} | ||
``` | ||
|
||
<details><summary> 수정 후 </summary> | ||
|
||
```kotlin | ||
if (checkCompleted(actionGroup)) { | ||
actionGroup.complete() | ||
sendTalkMessage(actionGroup, kaffeineEarnAction) | ||
} | ||
``` | ||
|
||
</details> | ||
|
||
|
||
**출력 인수** | ||
|
||
일반적으로 우리는 인수를 **함수 입력**으로 해석한다. | ||
|
||
따라서 인수를 출력 인수로 사용하면 함수 선언부를 찾아봐야 하고, 그러한 행위는 인지적으로 거슬리게 된다. (코드를 보다가 주춤하는 행위) | ||
|
||
그러므로 출력 인수는 피해야 하고, **함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경**하도록 하자. | ||
|
||
--- | ||
|
||
## 명령과 조회를 분리하라 | ||
|
||
> 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다. | ||
즉, `객체 상태 변경` 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 는 시스템을 구현할 프로그램이 아니라 풀어갈 이야기로 여긴다. 이때 프로그래밍 언어라는 수단을 사용해 좀 더 풍부하고 표현력이 강한 언어를 만들며 이야기를 풀어간다. | ||
|
||
시스템에서 발생하는 모든 동작을 설명하는 함수 계층이 바로 그러한 언어! | ||
|
||
진짜 목표는 시스템이라는 이야기를 풀어가는 데 있다는 사실을 명심하자 | ||
|
||
함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아떨어져야 이야기를 풀어가기 쉬워진다. | ||
|