[CleanCode] 3장. 함수
어떤 프로그램이든 가장 기본적인 단위는 함수이다.
작게 만들어라!
- 함수를 만드는 기본 규칙은
작게
이다. - 함수 내부에 들어가는 if문/else문/while문 등의 들여쓰기는 1단이나 2단을 넘어서는 중첩 구조가 발생하면 안된다.
한 가지만 해라!
- 함수는 한 가지를 해야 한다. 그 한가지를 잘 해야 하며, 그 한가지만을 해야 한다.
- 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다.
- 함수가 '한 가지’만 하는지 판단하는 방법이 하나 더 있다. 단순히 다른 표현이 아니라 의미있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다.
- 함수를 여러 섹션으로 나눌 수 있다면 그 함수는 여러 작업을 하는 셈이다.
함수 당 추상화 수준은 하나로!
- 함수가 ‘한가지’ 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.
- 만약, 한 함수 내에 추상화 수준이 뒤섞이면 읽는 사람이 헷갈린다.
- 코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 이것을 내려가기 규칙이라고 부른다.
- 함수 추상화 수준이 아래로 읽으면서 한 단계씩 낮아지는 것이 이상적이다.
Switch문
- switch문은 작게 만들기 어려우며, ‘한 가지’ 작업만 하도록 구성하기 힘들다.
- 아래의 코드는 코드를 변경할 이유가 하나가 아닌 여럿이므로
SRP 이론
을 위반한다. - 그리고 새 직원 유형을 추가할 때마다 코드를 변경하므로
OCP 이론
을 위반한다.
[문제가 존재하는 코드]
1 | public int calculatePay(Employee e){ |
-
switch문은 추상 팩토리에 숨긴다. 아무에게도 보여주지 않는다.
-
팩토리는 switch문을 사용해 적절한 Employee 파생 클래스의 인스턴스를 생성한다.
-
calculatePay, isPayday, deliverPay 등과 같은 함수는 Employee 인터페이스를 거쳐 호출된다. 그러면 다형성으로 인해 실제 파생 클래스의 함수가 실행된다.
-
즉,
다형성
을 이용하여 해결할 수 있다.
[변경한 코드]
1 | public abstract class Employee{ |
서술적인 이름을 사용하라!
- 함수 이름이 길어지더라도 서술적으로 한번에 이해가 가능한 이름이 오히려 깔끔하고 좋은 코드가 된다.
- 길고 서술적인 이름이 길고 서술적인 주석보다 좋다.
- 서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다. 좋은 이름을 고른 후 코드를 더 좋게 재구성하는 사례도 있다.
- 이름을 붙일 때는 일관성이 있어야 한다. 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.
- ex) includeSetupAndTeardownPages, includeSetupPages, includeSuiteSetupPages, includeSetupPage 등이 좋은 예다.
함수 인수
- 함수에서 이상적인 인수 개수는 0개이다.
- 기본적으로 3개까지가 마지노선이라고 생각한다. 그 이상은 절대 오지 않도록 한다.
- 인수는 함수를 이해하는데 어렵게 한다.
- 테스트 관점에서 보더라도 인수는 어렵다. 갖가지 인수 조합으로 함수를 검증하는 것은 까다롭다.
[많이 쓰는 단항 형식]
- 인수에 질문을 던지는 경우.
- ex) boolean fileExists(“MyFile”)
- 인수를 뭔가로 변환해 결과를 반환하는 경우.
- ex) InputStream fileOpen(“MyFile”)
- 드물게 사용하지만 유용한 경우는 이벤트.
- 함수 호출을 이벤트로 해석해 입력 인수로 시스템 상태를 바꾼다.
- 이벤트라는 사실이 코드에 명확히 드러나야 하며, 조심해서 사용해야 한다.
- ex) passwordAttemptFailedNtimes(int attempts)
[플래그 인수]
- 플래그 인수는 추하다.
- 함수로 부울 값을 넘기는 관례는 끔찍하다. 이유는 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈이니까!
[이항 함수]
- 인수가 증가할수록 함수를 이해하기 어려워진다.
- ex) writeField(name)은 writeField(outputStream, nam)보다 이해하기 쉽다.
- 이항 함수가 적절한 경우도 존재한다.
- ex) Point p = new Poin(0,0)
- 직교 좌표계 점은 일반적으로 인수 2개를 취한다. 다만, 여기서 인수 2개는 한 값을 표현하는 두 요소라는 점을 알아야 한다.
- 이항 함수가 무조건 나쁘다는 것은 아니다. 프로그램을 짜면서 불가피한 경우도 생기지만, 그만큼 위험이 따른다는 사실을 이해하고 가능하면 단항 함수로 바꾸도록 애써야 한다.
[삼항 함수]
- 인수가 3개인 함수는 인수가 2개인 함수보다 이해하기 어렵다.
- 순서를 파악하면서 주춤하게 되고, 무시해도 된다는 걸 생각하는 등의 문제가 두 배 이상 늘어난다.
[인수 객체]
- 인수가 늘어난다면 별도의 인수 객체를 만들어 묶어서 보내는게 더 낫다.
- Ex) int x, int y => Point point
- 이는 눈속임이 아니다. x,y를 묶었듯이 변수를 묶어 넘기려면 이름을 붙여야 하므로 결국은 개념을 표현하게 된다.
[동사와 키워드]
- 함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다.
- 즉, 함수의 이름과 인자가 한꺼번에 이해 가능한 이름이 가장 좋다.
- ex) write(name), assertExpectedEqualsActual(expected, actual)
부수 효과를 일으키지 마라!
- 부수 효과는 거짓말이다.
- 함수에서 한 가지를 하겠다고 하고선 다른 일을 하지 마라.
- ex) checkPassword() 함수 내에서 암호를 확인하는 일 이외에 사용자 세션을 초기화 한다거나 하는 행위는 자칫하면 엄청 큰 버그를 일으킬 가능성이 크다. 따라서 이와 같은 부수 효과를 일으키지 않아야 한다.
명령과 조회를 분리하라!
- 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다.
- 즉, 객체 상태를 변경하거나 아니면 객체 정보를 반환하거나 둘 중 하나만 해야 한다.
오류 코드보다 예외를 사용하라!
- 오류 코드 대신 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다.
- try/catch 블록은 코드 구조에 혼란을 일으키며, 정상 동작과 오류 처리 동작을 뒤섞는다. 그러므로 try/catch 블록을 별도 함수로 뽑아내는 편이 좋다.
[문제가 있는 코드]
1 | if (deletePage(page) == E_OK) { |
[변경한 코드]
1 | public void delete(Page page) { |
- deletePageAndAllReferences 함수는 예외를 처리하지 않는다.
- 이렇게 정상 동작과 오류 처리 동작을 분리하면 코드를 이해하고 수정하기 쉬워진다.
[오류 처리도 한 가지 작업이다.]
- 오류 처리도 ‘한 가지’ 작업에 속한다. 그러므로 오류를 처리하는 함수는 오류만 처리해야 마땅하다.
- 아래는 Error.java
1 | public enum Error { |
- 위의 코드는 의존성 자석이다.
- 오류를 처리하는 곳곳에서 오류 코드를 사용한다면 Error enum을 사용하게 된다. 즉, Error enum이 변하면 Error enum을 사용하는 클래스 전부를 다시 컴파일하고 다시 배치해야 하므로 비용이 크다.
- 그러므로 오류 코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생된다. 따라서 재컴파일, 재배치 없이도 새 예외 클래스를 추가할 수 있다.
반복하지 마라!
- 중복된 코드를 계속 만들지 마라.
- 중복을 없애면 가독성이 높아진다.
구조적 프로그래밍
- 다익스트라는 모든 함수와 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 한다고 말했다.
- 함수는 return문이 하나여야 한다.
- 루프 안에서 break나 continue를 사용해서는 안되며, goto는 절대로 안된다.