[Clean Code] Ⅲ. 함수

Clean Code 3판을 읽고 정리한 글입니다

Ⅲ. 함수

함수

  • 프로그래밍 초창기 : 시스템을 루틴과 하위 루틴으로 나눔
  • 포트란, PL/1 시절 : 프로그램, 하위 프로그램, 함수로 나눔
  • 현재는 함수만 살아남음 : 가장 기본적 단위

함수 잘만드는 규칙

작게 만들어라

  • 첫째도 작게. 둘째도 더 작게!
  • 블록과 들여쓰기
    • if/while등에 들어가는 블록은 단 한줄이어야 한다
    • 대게 여기서 함수 호출
    • wraping하는 enclosing func가 작아짐
    • 블록안의 함수명이 적절하면 이해도 상승
    • 함수의 들여쓰기는 1단이나 2단을 넘어서면 안된다

한가지만 해라!

함수에 대한 선배들의 충고

  • 함수는 한 가지를 해야 한다
  • 그 한가지를 잘해야 한다
  • 그 한가지만을 해야한다

한가지의 범위 : TO

  • ex) To RenderPageWithSetupsAndTeardowns
    • 페이지가 테스트 페이지인지 확인
    • 테스트 페이지면 설정,해제 페이지를 넣는다
    • 테스트 페이지임에 상관없이 HTML 렌더링
  • 지정된 함수 이름 아래 추상화 수준이 한단계 : 한 가지 작업만 하는 함수
  • 섹션이 나누어지는 함수(선언,초기화,로직등)은 여러 작업을 한다는 증거

함수당 추상화 수준은 하나

  • 함수 내 모든 문장의 추상화 수준이 동일해야 한다
  • 다른 추상화 수준이 섞여 있으며 읽기에 헷갈림
  • 내려가기 규칙
    • 위에서 아래로 이야기처럼 읽혀야 좋다
    • 위에서 아래로 읽으면 함수 추상화가 한번에 한 단계씩 낮아짐

Switch문 처리

siwtch문은 작게 만들기 어려움

  • 본질적으로 N가지 처리
  • 블행히도 switch문을 완전히 피할 방법도 없음
  • 예제 : 직원 유형에 따라 다른 값을 계산해 반환하는 함수
    Payroll.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public Money calculatePay(Employee e)
    throws InvalidEmployeeType {
    switch (e.type) {
    case COMMISSIONED:
    return calculateCommissionedPay(e);
    case HOURLY:
    return calculateHourlyPay(e);
    case SALARIED:
    return calculateSalariedPay(e);
    default:
    return new InvalidEmployeeType(e.type);
    }

    }
    • 문제점
      1. 함수가 길다. 새 회원 유형이 추가되면 더 길어짐
      2. ‘한 가지’작업만 수행하지 않음
      3. SRP(Single Responsibility Principal) 위반 : 코드 변경 이유가 여럿
      4. OCP(Open Closed Principal) 위반 : 새회원 유형 추가할 떄마다 코드 변경
      5. 위함수와 구조가 동일한 함수가 무한정 존재
      • isPayday(Employee e, Date date);
      • deliverPay(Employee e, Money pay);
    • 해결 방법 : 다형성 객체를 생성하는 코드로 상속관계로 숨긴다
      1. switch문을 추상 팩토리에 숨기고 아무에게 보여주지 않음
      2. 팩토리는 switch문을 이용해 적절한 Employee 파생 클래스의 인스턴스 생성
      3. 함수들(calculatePay, isPayday, deliverPay)는 Employee 인터페이스를 거쳐 호출 → 다형성(polymorphism)으로 실제 파생 클래스의 함수가 실행됨
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class Employee {
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}
//-------------------------
public interface EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
switch (r.type) {
case COMMISSIONED:
return new CommissionedEmployee(r);
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmployee(r);
default:
return new InvalidEmployeeType(r.type);
}
}
}

서술적인 이름을 사용하라

  • 좋은 이름이 주는 가치는 매우 중요
  • 길고 서술적인 이름이 짧고 어려운 이름보다 좋음
  • 길고 서술적인 이름 > 길고 서술적인 주석
  • 좋은 이름을 고르고 코드를 더 좋게 재구성한 사례도 존재함
  • 일관성 있는 네이밍
    • 모듈내의 함수 이름은 같은 문구/명사/동사 사용

함수 인수

이상적 인수 : 0개

  • 3개: 가능한 피하라, 4개: 특별한 이유가 필요, 5개 특별한 이유가 있어도 안됨

인수는 어렵다

  • 개념을 이해하기 어렵게 만듬
  • 함수 이름과 인수 사이에 추상화 수준이 다름
  • 코드 읽는 사람이 인수까지 파악해야함

테스트 관점 더 어려움

  • 인수가 3개 이상이면 유효한 값으로 모든 조합 구성해 테스트하기 부담스럽다

출력인수는 더 어렵다

  • 개발자는 함수에 인수로 입력을 넘기고 반환으로 출력을 받는것에 익숙

최선은 0개의 인수, 차선은 1개의 인수

많이 쓰는 단항 형식

  • 가장 흔한 경우
    1. 인수에 질문을 던지는 경우
    2. 인수를 근거해서 먼가를 변환해 결과를 반환
  • 흔하지 않지만 유용한 경우 : 이벤트
    • 이벤트는 입력 인수만 있고 출력 인수가 없음
    • 이벤트는 이름과 문맥 주의해서 선택 : 이벤트라는 사실이 코드에 명확하게
  • 이외에는 가급적 단항 함수는 피할 것
    • 입력 인수를 변환하는 함수라면 변환 결과는 반환값으로 돌려줄 것

플래그 인수

  • 추하다
  • 함수로 부울값을 넘기는 관례는 끔찍
    • 부울 값에 따라 하는 일이 다름 → 함수가 여러가지를 처리한다고 공표

2항 함수

  • 2항 함수는 1항 함수보다 이해가 어려움
  • 2항 함수가 적절한 경우 : 직교 좌표계 점 Point p = new Point(0,0)
  • assertEquals(expected, actual)
    • 문제가 있음
    • 바꿔쓰는 경우가 허다
    • 자연적 순서가 없어서 인위적으로 기억해야 함
  • 2항이 무조건 나쁘다는 소리가 아님
    • 불가피하면 어쩔 수 없음
    • 하지만 그만큼 위험이 따르는 부분이라는 것을 인지 필요
    • 가능한 단항으로 바꾸도록 애써야 한다

3항 함수

  • 2항보다 더 이해 어려움 - 신중히 고려 필요
  • assertEquals(expected, actual, message)
    • 메시지 무시 하는 오버로딩도 있음
    • expected 위치 자꾸 헷갈림

인수 객체

  • 인수가 많다면 독자적인 클래스 변수로 선언?
  • VO나 DTO → 결국 개념을 표현

인수 목록

  • 3항이상의 가변 인수 의 경우 문제가 있음
  • 동사와 키워드
    • 함수의 의도, 인수의 순서, 의도 표현 - 좋은 함수 이름 필 수
    • 단항 함수 : 동사/명사 쌍
    • 함수 이름에 키워드 추가 하는 형식도 있음
    • assertExpectedEqualsActual(expected,actual) : 인수 순서 기억 필요 없음

부수 효과를 일으키지마라

  • Side effect는 거짓말 : 함수에서 1가지만 하겠다고 해놓고 다른 것도 함
  • 많은 경우 시간적 결합(temporal coupling), 순서 종속성(order dependency) 초래

출력 인수

  • 인수를 출력으로 쓰지말아라
    • 기본적으로 우리는 인수를 함수 입력으로 해석
    • 선언부를 보고 확인? 벌써 주춤 및 생산성 저하,
  • 객체 지향 언어에서는 출력 인수를 사용할 필요가 거의 없다
    • 이전 언어에는 출력인수가 불가피한 경우가 존재
    • 객체 지향에서는 this가 존재
    • ex) public void appendFooter(StringBuffer report)
      • appendFooter(s); : 출력을 인수에 넣었음
      • 다음과 같이 쓸 수 있도록 고치자 : report.appendFooter()
  • 함수에서(굳이) 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하자.

명령과 조회 분리 = CQS

  • 함수는 커맨드(무언가를 수행)하거나 쿼리(무언가에 답)하거나 둘중 하나만 할 것
    1. 코드 자체가 일단 괴상해짐(책의 내용처럼)
    2. 함수는 하나만 해야 하는 SRP를 지킬 수 있겠고
    3. 사이드 이펙트도 최소화할 수 있음

오류 코드 보다는 예외 사용

  • 명령 함수에서 오류코드 반환 → CQS 약간 위반

    • 오류 코드 반환시 호출자는 오류 코드를 곧바로 처리해야 함 → 코드 복잡
    • 예외를 사용하면 오류 처리가 원래 코드와 분리 → 코드가 깔끔해짐
  • try-catch 뽑아내기

    • 추함 : 블록 자체가 코드 구조에 혼란을 주고 정상/오류 동작을 뒤섞는다
    • 해당 블록을 별도 함수로 뽑아내자
    • catch 블록도 별도로 뽑아야 한다
      • 함수는 한가지 작업만 해야 한다
      • 오류 처리도 ‘한 가지’ 작업에 속함
      • 오류 처리 함수는 오류만 처리해야 함
      • 함수에 try가 있다면 함수는 try로 시작해서 catch/finally로 끝나야 한다
  • Error.java 의존성 자석

    • 오류코드를 반환한다 → (클래스,열거,변수 어떤 거든) 어디선가 오류 코드 정의한다
    • Error.java : 에러 코드를 모아놓은 enum class 라고 가정 → 의존성 자석
      • 다른 클래스에서 Error enum을 import
      • Error enum이 변하면 Error를 사용하는 모든 클래스 재컴파일 필요
      • 재컴파일/재배치 번거로움 → 새오류 코드 정의 싫어함 → 기존 코드 재 사용;
    • 예외는 Exception클래스에서 파생 → 재컴파일/재배치 없이 새 예외 클래스 추가 가능

반복하지 마라 : DRY(Don’t Repeat Yourself) 원칙

중복 = 모든 악의 근원

  • 길이가 늘어남, 변경시 중복 된만큼 케어, 바쁘릴시 오류가 발생할 확율도 높음
  • 많은 법칙/기법이 오직 중복을 없애거나 제어할 목적으로 존재
    • RDMBS의 정규형 : 중복 제거
    • OOP : 코드를 부모 클래스로 몰아넣음 → 중복 제거
    • AOP, COP : 어떤 관점으로 보면 이들 모두 중복 제거 전략
    • 하위 루틴 발명 이래로 모든 sw개발의 혁신은 중복을 제거하려는 지속적인 노력의 결과

구조적 프로그래밍

모든 함수와 함수 내 모든 블록에 입구와 출구는 하나만 존재해야 한다

Edsger Dijkstra(에츠허르 데이크스트라)Structured Programming(구조적 프로그래밍 원칙
  • 함수는 return이 하나이어야 할 것
  • 루프 안에서 break/continue 사용하면 안된다
  • goto는 절대로 사용하지 말 것

위 규칙은 함수가 큰 경우에 한해서 상당한 이득

  • 함수를 아주 작게 만든다면 간혹 return/break/continue 여러번 사용 무방
  • 때로는 오히려 단일 입/출구 규칙보다 의도 표현이 용이
  • goto는 큰함수에서만 의미 → 작은 함수에서 피해야 함

함수를 어떻게 짜는가?

sw작성 = 글짓기

  • 글짓기
    • 먼저 기록을 기록후 읽기 좋게 다듬는다
    • 초안은 대게 서투르고 어수선
    • 말을 다듬고 문장을 고치고 문단을 정리
    • 제대로 작성될 때까지
  • 함수 작성도 마찬가지
    • 처음엔 길고 복잡, 들여쓰기 단계도 많고 중복된 루프와 긴 인수 목록
    • 네이밍도 즉흥적, 중복 코드도 많음
    • 서투른 위의 코드를 빠짐없이 테스트 하는 단위 테스트 케이스 작성
    • 이후 코드 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거
    • 이 와중에 코드는 항상 단위 테스트를 통과
    • 최종적으로 이 챕터의 규칙을 따르는 함수를 결과물로.
    • 처음부터 짜내지 않는다. 그게 가능한 사람도 없다

결론

DSL

  • 특정 응용분야 시스템을 기술할 목적으로 설계된 도메인 특화언어

프로그래머 대가

  • 시스템은 구현할 프로그램이 아니라 풀어갈 이야기로 여긴다
  • 프로그램 언어를 수단으로 좀더 풍부하고 표현력이 강한 언어를 만들어 이야기를 풀어간다
  • 재귀라는 기교로 시스템에 발생하는 동작은 그 도메인에 특화된 언어를 사용해 자신만의 이야기

3.7 규칙적용 코드 예시

규칙적용 코드 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
public class SetupTeardownIncluder {
private PageData pageData;
private boolean isSuite;
private WikiPage testPage;
private StringBuffer newPageContent;
private PageCrawler pageCrawler;

public static String render(PageData pageData) throws Exception {
return render(pageData, false);
}

public static String render(PageData pageData, boolean isSuite)
throws Exception {
return new SetupTeardownIncluder(pageData).render(isSuite);
}

private SetupTeardownIncluder(PageData pageData) {
this.pageData = pageData;
testPage = pageData.getWikiPage();
pageCrawler = testPage.getPageCrawler();
newPageContent = new StringBuffer();
}

private String render(boolean isSuite) throws Exception {
this.isSuite = isSuite;
if (isTestPage())
includeSetupAndTeardownPages();
return pageData.getHtml();
}

private boolean isTestPage() throws Exception {
return pageData.hasAttribute("Test");
}

private void includeSetupAndTeardownPages() throws Exception {
includeSetupPages();
includePageContent();
includeTeardownPages();
updatePageContent();
}
private void includeSetupPages() throws Exception {
if (isSuite)
includeSuiteSetupPage();
includeSetupPage();
}

private void includeSuiteSetupPage() throws Exception {
include(SuiteResponder.SUITE_SETUP_NAME, "-setup");
}

private void includeSetupPage() throws Exception {
include("SetUp", "-setup");
}

private void includePageContent() throws Exception {
newPageContent.append(pageData.getContent());
}

private void includeTeardownPages() throws Exception {
includeTeardownPage();
if (isSuite)
includeSuiteTeardownPage();
}

private void includeTeardownPage() throws Exception {
include("TearDown", "-teardown");
}

private void includeSuiteTeardownPage() throws Exception {
include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");
}

private void updatePageContent() throws Exception {
pageData.setContent(newPageContent.toString());
}

private void include(String pageName, String arg) throws Exception {
WikiPage inheritedPage = findInheritedPage(pageName);
if (inheritedPage != null) {
String pagePathName = getPathNameForPage(inheritedPage);
buildIncludeDirective(pagePathName, arg);
}
}

private WikiPage findInheritedPage(String pageName) throws Exception {
return PageCrawlerImpl.getInheritedPage(pageName, testPage);
}

private String getPathNameForPage(WikiPage page) throws Exception {
WikiPagePath pagePath = pageCrawler.getFullPath(page);
return PathParser.render(pagePath);
}

private void buildIncludeDirective(String pagePathName, String arg) {
newPageContent
.append("\n!include ")
.append(arg)
.append(" .")
.append(pagePathName)
.append("\n");
}
}

추가

SOLID - 너무 유명한 법칙인데 계속 드문드문한 법칙

  • SRP(Single Responsibility Principle)

    • 단일 책임의 원칙
    • THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE.
    • 한 클래스는 하나의 기능을 가진다
    • 클래스가 제공하는 모든 서비스는 그 하나의 책임을 수행하는데 집중되어야 한다
    • 어떤 변화에 의해 클래스를 변경해야 하는 이유도 오직 하나뿐이어야 한다
    • 적용시 책임영역이 확실 → 책임 변경에서 다른 책임의 변경의 연쇄를 막음
    • 책임의 적절한 분배 → 코드 가독성 향상, 유지보수 용ㅇ이
    • 다른 원리들을 적용하는 기초
      • 리팩토링이 필요한 위험사항 해법은 직/간접적으로 SRP원리와 연관
      • 리팩토리 근본정신(항상 코드를 최상으로 유지)도 항상 객체 책임을 최상의 상태로 분배한다는 것에서 비롯되므로
    • 다른 것에 비해 단순하지만 실무에서 직접 적용해서 설계가 그리 쉽지는 않음
    • 의도적인 많은 연습과 경험이 필요
    • 무조건적인 책임 분리가 SRP가 적용되는 것이 아니다
      • 객체간의 응집력(cohesion)이 있다면 병합이 순작용의 수단 - 강 응집력 지향
      • 결합력(coupling)이 있다면 분리가 순 작용의 수단 - 약 결합력 지향
  • OCP(Open Close Principal)

    • 개방폐쇄의 원칙
    • YOU SHOULD BE ABLE TO EXTEND A CLASSES BEHAVIOR, WITHOUT MODIFYING IT.
    • sw 요소는 확장에 열려있고 변경에 닫혀있어야 한다
    • 객체지향 소프트웨어 설계라는 책에서 정의됨 - 1998, 버틀란트 메이어(Bertrand Meyer)
    • sw요소 : 컴포넌트, 클래스, 모듈, 함수
    • 변경을 위한 비용은 최소화, 확장을 위한 비용은 극대화
    • 객체지향의 장점을 극대화하는 아주 중요한 원리
    • 요구사항 변경이나 추가 사항시
      • 기존 요소는 수정이 일어나지 말아야한다
      • 기존 구성요소를 쉽게 확장해서 재사용할 수 있어야 한다
      • 밥아저씨
        • OCP는 관리 가능하고 재사용 가능한 코드를 만드는 기반
        • OCP를 가능하게 하는 중요 매커니믐 : 추상화, 다형성
  • LSP (The Liskov Substitution principle)

    • 리스코프 치환원칙
    • FUNCTIONS THAT USE POINTERS OR REFERENCES TO BASE CLASSES MUST BE ABLE TO USE OBJECTS OF DERIVED CLASSES WITHOUT KNOWING IT.
    • 서브 타입은 언제나 기반 타입으로 교체 가능해야 함
    • 서브 타입은 기반 타입이 약속한 규약을 포함하고 지켜야함
    • 클래스의 상속, 인터페이스 상속 이용해 확장성 획득
    • 다형성/확장성을 극대화하기 위해 인터페이스를 사용하는 것이 좋다
      • 상속을 통한 재사용은 기반/서브 사이에 IS-A 관계일 경우로만 제한 권장
      • 부모클래스를 작성한 개발자가 세웠던 가정, 추론 과정, 원리를 정확히 이해 필요
        이는 자식 클래스가 부모클래스에게 강하게 결합된 의미
      • 상속은 부모와 자식 클래스의 결합도가 매우 높음
      • 상속은 추상화를 이용을 해야 위의 문제가 해결됨
        • 자식의 클래스가 부모 클래스의 구현이 아닌 추상화에 의존하도록
    • 합성(컴포지션) 사용 가능 : 이펙티브 자바 18번 아이템
      • 구글에서 “이펙티브 자바 18” 검색
      • 데코레이터 패턴 처럼
  • ISP (Interface Segregation Principle)

    • 인터페이스 분리 원칙
    • CLIENTS SHOULD NOT BE FORCED TO DEPEND UPON INTERFACES THAT THEY DO NOT USE.
    • 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다
    • 가능한 최소한의 인터페이스만 구현
    • 특정 클래스 이용하는 클라이언트가 여러 개이며 각각 클래스의 특정 부분만 이용?
      → 여러 인터페이스로 분류해서 클라이언트가 필요한 기능만 전달한다
    • SRP가 클래스의 단일 책임이라면? ISP는 인터페이스의 단일 책임
  • DIP (Dependency Inversion Principle)

    • 의존성 역전의 법칙
      • A. HIGH LEVEL MODULES SHOULD NOT DEPEND UPON LOW LEVEL MODULES. BOTH SHOULD DEPEND UPON ABSTRACTIONS.
      • B. ABSTRACTIONS SHOULD NOT DEPEND UPON DETAILS. DETAILS SHOULD DEPEND UPON ABSTRACTIONS.
    • 상위 모델은 하위 모델의 의존하면 안되며 둘다 추상화에 의존해야 한다
    • 추상화는 세부사항에 의존해선 안되며 세부사항은 추상화에 따라 달라진다
    • 하위 모델의 변경이 상위 모듈의 변경을 요구하는 위계 관계를 끊는다
    • 실제 사용 관계는 그대로이나 추상화를 매개로 메시지를 주고 받으며 관계를 느슨하게
  • 추가 참조

간결한 함수

  • 분기는 가능한 추상 클래스 or Factory
    안전한 함수 - Side Effect

함수 리팩터링

  1. 기능을 구현하는 서투른 함수를 작성
  2. 테스트 코드 작성
  3. 리팩터링

Related POST

공유하기