1. 함수를 잘 만드는 법
길이가 길 뿔만 아니라 중복된 코드에, 괴상한 문자열에, 낯설고 모호한 자료 유형과 API가 많다. 함수를 읽고 이해하기 쉽게 하기위해서는 무엇이 고려되야 할까요?
2. 작게 만들어라!
함수를 만드는 첫째 규칙은 작게
이다. 함수를 만드는 두번째 규칙은 더 작게
이다.
자, 다음과 같은 코드가 주어졌는데 이것을 어떻게 짧게 만들 수 있을까요?
1 | public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception { |
한 함수당 3-5줄 이내로 줄이는것이 좋습니다.
1 | public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception { |
2.1. 블록과 들여쓰기
if문 else while문 등에 들어가는 블록은 한줄이여야 한다는 의미입니다. 중첩 구조가 생길만큼 함수가 커져서는 안 된다는 뜻이며 들여쓰기 수준은 1,2단을 넘어서면 안됩니다.
3. 한가지만 해라!
- 함수는 한 가지를 해야한다. 그 한가지를 잘 해야한다. 그 한 가지만을 해야한다.
- 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다.
- 반대로, 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다.
4. 함수당 추상화 수준은 하나로!
함수가 확실히 한 가지
만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 합니다.
추상화 수준이 높은 경우
1 | getHTMl(); |
위의 3가지 코드는 추상화 레벨이 다릅니다.
-
추상화 수준이 매우 높다
getHTM() -
추상화 수준이 높다
String pagePathName = PathParser.render(pagePath); -
추상화 수준이 낮다
Object.append("\n");
한 함수내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다.
개념에 세부 구현체가 추가되면 깨진 유리창 효과로 함수가 점점 비대해지며 심각한 레거시로 발전한다.
4.1. 위에서 아래로 코드 읽기: 내려가기 규칙
코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 한 함수에는 추상화 수준이 한 단계 낮은 함수가 온다.
4.2. Switch case
switch문은 작게 만들기 어렵다. case 분기가 단 두 개인 switch문도 내 취향에는 너무 길며, 단일 블록이나 함수를 선호한다. 또한 한 가지 작업만 하는 switch문도 만들기 어렵다
다형성을 이용하여 switch 문을 저차원 클래스에 숨기고 드러내지 않는다.
직원 유형에 따라 다른 값 계산해 반환하는 함수
1 | public Money calculatePay(Employee e) throws InvalidEmployeeType { |
다음과 같은 함수의 문제점은 무엇일까요?
- 함수가 길다.
- 한 가지 작업만 수행하지 않는다.
- SRP 를 위반한다. 코드를 변경할 이유가 여럿이기 때문이다.
- OCP 를 위반한다. 새 직원 유형을 추가할 때마다 코드를 변경해야 한다.
- 위 함수와 구조가 동일한 함수가 무한정 존재한다. isPayday(Employee e, Date date); 같은 경우
이것을 어떻게 유연하게 바꿀 수 있을까요?
switch 문을 추상 팩토리에 꽁꽁 숨긴다.
1 | public abstract class Employee { |
- 핵심은 로직이 퍼지지 않게 하며 일관성을 유지시키는 것이다
- 사용하는 곳에서 구현하는 것이 아닌 구현된 팩토리 메서드를 사용하게 하는 것이다
- 상속관계로 숨긴 후에는 절대로 다른 코드에 노출하지 않는다.
5. 서술적인 이름을 사용하라!
좋은 이름이 주는 가치는 아무리 강조해도 지나치지 않는다. 함수 이름을 정할때는 여러 단어가 쉽게 읽히는 명명법을 사용한다. 그 다음에 여러 단어를 사용해 함수 기능을 잘 표현하는 이름을 선택한다. 서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.
좋은 이름을 고른 후 코드를 더 좋게 재구성하는 사례도 없지 않다. 이름을 붙일때는 일관성이 있어야한다. 모듈내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.
6. 함수 인수
함수에서 이상적인 인수의 개수는 0개다. 가능한 다항은 피한다. 인수는 매우 어렵다. 인수는 개념을 이해하기 어렵게 만든것이다. 이것이 내가 우리 예제에서 인수를 거의 없앤 이유이다. 갖가지 인수 조합으로 함수를 검증한다고 하면 엄청나게 많은 조합의 가지가 생긴다.
6.1. 많이 쓰는 단항 형식
인수에 질문을 던지는 경우
boolean fileExists(“MyFile”);
인수를 뭔가로 변환해 결과를 변환하는 경우
InputStream fileOpen(“MyFile”);
이벤트 함수일 경우 이벤트라는 사실이 코드에 명확하게 드러나야 한다.
passwordAttemptFailedNtimes(int attempts);
6.2. 플래그 인수
함수를 넘길때 플래그 인수를 넣지마라, 함수에 여러가지 조건을 걸겠다는 의미와 같은 의미다.
6.3. 이항 함수
인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어렵다.
이항함수가 적절한 경우는 좌표 Point p = new Point(0,0)
가 있다.
6.4. 삼항 함수
인수가 3개인 함수는 인수가 2개인 함수보다 훨씬 더 이해하기 어렵다. 삼항 함수를 만들때는 신중히 고려해야한다.
6.5. 인수 객체
인수가 2-3개 필요하다면 독자적인 크래스 변수로 선언할 가능성을 짚어본다.
1 | Circle makeCircle(double x, double y, dobule radius) |
6.6. 인수 목록
1 | String.format("%s worked %.2f hours.", name, hours); |
가변 인수를 모두 동등하게 취급하면 List형 인수 하나로 취급할 수 있다.이로인해 사실상 이항 함수가 된다
1 | public String format(String format, Object... args) |
가변 인수를 취하는 모든 함수에 같은 원리가 적용된다.
6.7. 동사와 키워드
단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야한다.
1 | writeField(name); |
함수이름에 키워드(인수 이름)을 추가하면 인수 순서를 기억할 필요가 없어진다.
1 | assertExpectedEqualsActual(expected, actual); |
7. 부수 효과를 일으키지 마라!
부수효과는 거짓말이다. 때로는 예상치 못하게 클래스 변수를 수정한다. 때로는 함수로 넘어온 이눗나 시스템 전역 변수를 수정한다.
부수효과
1 | public class UserValidator { |
부수효과는 시간적인 결합을 초래한다. 특히, 부수 효과로 숨겨진 경우에는 더더욱 혼란이 커진다. Session.initialize() 는 함수명과 맞지 않는 부수 효과이다. 이름 그대로 암호를 확인한다.
7.1. 출력인수
일반적으로 출력인수는 피해야한다. 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 취한다.
1 | public void appendFooter(StringBuffer report) |
8. 명령과 조회를 분리하라
함수는 뭔가를 수행하거나 뭔가를 답하거나 둘중 하나만 해야한다.
set 이라는 함수가 굉장히 모호하다. setAndCheckIfExists 라고 하는게 훨씬 좋지만, 명령과 조회를 분리해 애초에 혼란이 일어나지 않도록 한다.
1 | public boolean set(String attribute, String value); |
어떤 의미를 하는지 의미가 모호하다.
9. 오류 코드보다 예외를 사용하라!
명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다.
1 | if (deletePage(page) === E_OK) |
상태 코드의 종속을 유발하고 중첩되는 if문과 여러 상태코드의 조합이 이루어지게 됩니다.
유지보수에 치명적이고 비즈니스 로직을 한 눈에 알기 어렵다. 또한 오류 코드를 만났을 경우 바로 해결해야만 하는 문제가 있다.
1 | if (deletePage(page) == E_OK) { |
정상 동작과 오류 처리 동작이 뒤섞이므로 굉장히 모호해진다. 오류 코드 대신 예외를 사용 하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다.
9.1. Try/Catch 블록 뽑아내기
Try/Catch 블록은 추하고 코드 구조에 혼란을 일으키며 정상동작과 오류 처리 동작을 뒤 섞는다.
1 | public void delete(Page page) { |
정상 동작과 오류 처리 동작을 분리하면 코드를 이해하고 수정하기 쉬워진다.
9.2. 오류 처리도 한 가지 작업이다.
함수는 한 가지
작업만 해야한다. 오류 처리도 한 가지
작업에 속한다. 즉, 함수에 키워드 try가 있다면 함수는 try문으로 시작해 catch/finally문으로 끝나야한다는 말이다.
9.3. Error.java 의존성 자석
오류 코드를 반환한다는 이야기는 클래스든 열거형 변수든 어디선가 오류 코드를 정의한다는 뜻이다.
1 | public enum Error { |
오류를 처리하는 곳곳에서 오류코드를 사용하게 되면 enum class를 쓰게 되는데 이런 클래스는 의존성 자석이 된다. 재컴파일 및 재배치 등 새 오류코드를 추가하거나 변경할 때 코스트가 많이 필요하다.그러므로 예외를 사용하는 것이 더 안전하다.
10. 구조적 프로그래밍
함수는 return문이 하나여야한다. 루프 안에서 break나 continue를 사용해선 안되며 goto는 절대로 절대로 안된다.
함수를 작게 만든다면 return, break, continue를 여러 차례 사용해도 괜찮다. goto는 피해라
11. 함수를 어떻게 짜죠?
- 서투른 코드를 작성한다.
- 이러한 코드에도 단위 테스트 케이스를 만든다.
- 코드를 다듬고 함수를 만들고 이름을 변경한다.
- 위의 과정에서 항상 단위 테스트를 통과해야한다.
- 반복을 진행한다.
12. 결론
모든 시스템은 특정 응용 분야 시스템을 기술할 목적으로 프로그래머가 설계한 도메인 특화 언어로 만들어진다. 함수는 그 언어에서 동사며, 클래스는 명사다. 프로그래밍 기술은 언제나 언어 설계의 기술이다. 결론적으로 작성한 함수가 분명하고 정확한 언어로 되어 있을 때 이야기를 풀어가기가 쉬워진다는 사실을 기억하라.