본문 바로가기

IT/클린코드

[클린코드] 3장 함수


안녕하세요 남갯입니다


오늘은 클린코드 3장 함수에 대해 포스팅 해보려고 합니다.



프로그래밍의 초창기에는 루틴과 서브루틴으로 나누었다고 한다. 루틴은 한 함수를 동작하는것이고 서브루틴은 그 루틴에서 또 다른 루틴을 실행시킨것을 말한다. 이런 함수를 작성하면서 의도를 분명하게 표현하지 않으면 내용의 정보를 파악하기 어렵다. 어떻게 처음읽는 사람이 직관적으로 파악하게 만들 수 있을까?



작게만들어라

함수를 만드는 규칙은 작고 더 작게 만드는것이다. 저자는 20~ 3000천줄의 코드를 작성해본 결과 작은함수가 좋다고 말한다.

그렇다면 얼마나 짧은것이 좋다는 것일까?

함수의 길이는 최대한(4줄이하) 적게 만들어라. 


// 함수를 작게 만들어라
public static String renderPageWithSetupsAndTeardowns(
PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageDataf isSuite);
return pageData.getHtml();
}

위와 같은 코드로 코드를 직관적이고 코드 줄 수는 줄이는것이 마땅하다고 한다.


블록과 들여쓰기

위에서 말하고자 하는건 if / else문 / while 문중 들어가는 블록은 한 줄 이어야 한다는 의미다.

블록안에 호출하는 함수이름을 적절하게 지으면 코드를 이해하기 쉬워진다. 즉 중첨구조가 생길만큼의 함수크기를 가지지 말고

들여쓰기 수준은 1단 2단을 넘으면 안된다. 그래야 함수를 읽기 쉬워진다.


함수는 한가지만해라

함수의 동작은 한가지만을 해야한다. 하지만 여기서 말하는 한가지만을 알기가 어렵다고 생각될 수 도 있다. 여기서 말하는건

지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만을 수행한다면 한가지 작업을 하는것이다.   ...... 함수의 한가지 동작 여부판단 1

우리가 함수를 만드는 이유는 큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서인것이기 때문에 위의 코드는 한가지의 작업을 수행하는것이다.

단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러작업을 하는셈이다. ...... 함수의 한가지 동작 여부판단 2


함수당 추상화 수준은 하나로

함수가 한가지 작업을 하려면 함수 내의 모든 문장의 추상화 수준이 동일해야한다. 근본개념과 세부사항을 뒤섞기 시작하면 나도 모르게 함수에 세부사항들을 점점 추가하게 된다.


위에서 아래로 코드읽기(내려가기 규칙)

코드는 위에서 아래로 이야기처럼 읽혀야 좋다고 한다.


switch문 

스위치문은 여러개의 동작을 관리하기 때문에 작게 만들기 어렵다. 하지만 스위치문을 저차원 클래스에 숨기고 절대 반복하지 않는방법이 있다.

다형성을 이용한다.


public Money calculatePay(Employee e)
throws InvalidEmplᄋyeeType {
switch (e.type) {
case COMMISSIONED:
return caleulateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}


위의 함수에는 몇가지의 문제가 있다. 

1. 함수가 너무 길다.

2. 한가지의 작업만 수행하지 않는다.

3. SRP를 위반한다. (코드의 변경이유가 여럿이기 때문)

4. OCP 위반한다. (새 직원의 유형이 추가 될때마다 코드를 변경하기 때문)


더 큰 문제는 위와같은 코드를 무한정 존재한다는 것이다.

isPayday()

deliverPay()

위의 두개와 같은 모두 유해한 구조들 말이다.


이런 코드는 추상 팩토리를 통해 숨긴다.


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;
}

public class EmployeeFactorylmpl implements EmployeeFactory {
public Employee makeEmployee(EmployeeReco rd r) throws InvalidEmployeeType {
switch (r.type) {
case COMMISSIONED:
return new CommissionedEmployee(r);
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
ret니rn new SalariedEmployee(r);
default:
throw new InvalidEmp 'LoyeeType! r.type);
}
}
}


Employee를 가진 추상 클래스를 생성한 뒤,

factory 추상 팩토리를 생성하고 그것을 구현한 Impl 클래스를 만듭니다.

거기서 사용되는 switch는 다형적 객체를 생성하는것에 이용하고

그 객체들은 Employee 클래스를 상속받아 구현합니다.

이 코드는 위의 코드를 개선한 코드지만, 이런 규칙을 완벽하게 지키기는 어렵다고 합니다.


서술적인 이름을 사용하라.

좋은이름이 주는 가치는 강조해도 지나치지 않을 정도로 좋다. 코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드이다.

또한 한하지만 하는 작은 함수에 좋은 이름을 붙인다면 절반은 성공한 것이며, 함수가 작고 단순할 수록 서술적 이름을 고르기 쉬워진다.

그리고 이전 단원에서도 나왔듯이 일관적인 이름을 붙여야한다. include... include... 가 좋은 예이다.


함수 인수

함수의 가장 이상적인 인수는 적을수록 좋고 3항의 이상이면 피하는것이 좋고 4항 이상이면 특별한 이유가 필요하다.

인수는 어렵고 인수의 개념을 이해하기 어렵게 만든다.


많이쓰는 단항 형식

함수에 인수 한개를 넘기는 이유는 가장 크게 두가지이다.

1. 인수에 질문을 던지는 경우  (boolean fileExist)

2. 인수를 뭔가로 변환해 결과를 반환하는 경우

드물게는 함수형식의 이벤트까지 있고 세가지의 경우가 아니라면 단항함수는 가급적 피한다.

또한 함수의 인수로 StringBuffer를 넘기는 일을 비한다. 변환함수에서 출력인수를 사용하면 혼란을 일으킨다.


플래그인수

플래그인수는 부울값을 넘기게 되면 함수가 여러가지를 처리한다는 의미이다. 


이항함수

인수가 2개는 1개보다 이해하기 어렵다. 불가피한 상황 즉 Point(x,y)와 같은 상황을 사용해야 하는 부분이 아니라면 위험이 따른다는 사실을 이해하고 가능하면 단항으로 변경해야한다.


삼항함수

인수가 3개는 2개보다 훨씬 이해하기 어렵다. 따라서 


인수객체

인수가 2~3개가 필요하다면 독자적인 클래스 변수로 선언할 가능성을 짚어본다.


Circle makeCircle(double x, double y, double radius);
Circle makeCirele(Point center, double radius);

위는 개념을 잘 표현한것이다.


인수목록

때로는 인수가 가변적인 함수도 필요하다.

String.format 메서드가 좋은 예이다. 가변인수를 통해 리스트형 인수를 하나로 취급 할 수 있다.


동사와 키워드

함수의 의도나 인수의 순서와 의도를 제대로 표현하기 위해서는 좋은 함수 이름이 필수다.

단항함수는 함수와 인수가 동사/명사쌍을 이뤄야한다. ex write(name) 이름을 쓴다

더 좋은 표현방법은 writeFiled(name) 이름이 필드라는것을 알 수 있다.

함수이름에 키워드를 추가하면서 함수이름에 인수 이름을 넣는다

assertEquals 보단 assertExpectedEqualsActual(Expected, actual)이 더 좋다.


부수효과를 일으키지 마라

부수효과는 거짓말이다. 한가지를 하겠다는 약속해놓고 다른짓도 하는것이니까,



public class UserValidator {
private Cryptographer cryptographer;
public boolean checkPassword(St ring userName, String password) {
User user = UserGateway.findByName(userName);
if (user != User.NULL) {
String codedPhrase = user.getPhraseEncodedByPasswᄋrd();
String phrase = cryptographer.decrypt(codedPhrase, password);
if ("Valid Password" .equals(phrase)) {
Session.initialize();
return true;
}
}
return false;
}
}


여기서 문제되는 코드는 Session.initialize() 코드이다.

사용자는 checkPassword를 통해 비밀번호를 확인하는데 기존 세션정보를 지워버릴 위험이 생긴다.

따라서 이 코드는 checkPasswordAndInitializeSession 이라는 이름이 훨씬 좋다. but 한가지의 규칙을 위반한다.


출력인수

일반적으로 우리는 인수를 함수 입력으로 해석한다.

appendFooter(s) 라는 코드를 통해 무언가 s를 바닥글로 첨부할지 아니면 s에 바닥글을 첨부할지 모르지만 선언부를 보면 분명해진다.

public void appendFooter(StringBuffer report)

인수 s가 출력 인수라는 사실은 분명하지만 함수 선선부를 찾아보고 나서야 알게됐다.

객체지향에서는 출력인수가 불가피한 경우도 있었지만 객체지향 언어에서는 출력 인수를 사용할 필요가 거의 없다. 출력 인수로 사용하라고 설계한 변수가 바로 this이기 때문이다.

report.appendFooter() 형태로 호출하는것이 좋다.



명령과 조회를 분리하라.

함수는 뭔가를 수행하거나 뭔가를 답하거나 둘중 하나만을 해야한다

boolean set(String attribute, String value) 

이 함수는 이름이 attribute인 속성을 찾아 value로 설정한 후 성공하면 true, 실패하면 false를 반환한다.

이코드는

if(set("username", "namget"))....

독자입장에서는 무슨뜻인지 모호하다.

if(attributeExist("username")){

   setAttribute("username", "namget");

}


이러한 코드처럼 명령과 조회를 분리해서 혼란을 애초에 뿌리 뽑아야한다.


오류코드보다는 예외를 사용하라.

오류코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반하게 된다. if문에서 명령을 표현식으로 사용하기 쉬운탓이다.

오류 코드 대신에 예외 처리 코드를 사용하면 오류의 처리코드가 원래 코드에서 분리되서 코드가 깔끔해진다.


try catch 블록 뽑아내기

try catch는 코드 구조에 혼란을 일으키며 정상동작과 오류 처리 동작을 뒤섞는다. 따라서 try/catch 블록을 별도 함수로 뽑아내는것이 좋다.


오류처리도 한가지 작업이다.

함수는 한가지 작업을 하고 오류처리도 한가지 작업에 속한다.


Enum.java 의존성 자석


public enum Error {
OK,
INVALID,
N0_SUCH,
LOCKED,
0UT_0F_RES0URCES,
WAITING_FOR_EVENT;
}


위와같은 클래스는 의존석 자석이다. 다른클래스에서 Error enum을 import하면서 Error가 변경되면 다시 컴파일하고 배치해야한다.

따라서 오류코드 대신 예외를 사용하면 재컴파일/재배치 없이도 새 예외클래스를 추가할 수 있다.


반복하지마라

중복은 모든 악의 근원이다. 중복을 없앴더니 모듈 가독성이 크게 높아졌다. AOP나 COP는 모두 중복 제거 전략이다.


구조적 프로그래밍

모든 함수와 함수는 입구와 출구는 하나만 존재한다. 즉 return문이 하나여야 한다는 말이다.

break 혹은 continue나 goto는 절대로 안된다. 함수를 작게만든다면 return , break, continue를 여러차례 사요하면서 단일 입출구 규칙보다 의도를 표현하기 쉬워진다.


함수를 어떻게 짜죠?

함수는 처음에는 길고 복잡하고 들여쓰기 단계도 많고 중복된 루프도 많다. 인수 목록도 아주 길다.

이름은 즉흥적이고 코드는 중복된다. 하지만 코드를 테스트하는 단위케이스도 만들고 코드를 다듬고 함수를 만들고 이름을 바꾸고 중복을 제거한다.

메서드를 줄이고 순서를 바꾼다. 때론 전체클래스를 쪼갠다. 이후에는 규칙을 만족하는 함수가 얻어진다.


'IT > 클린코드' 카테고리의 다른 글

[클린코드] 6장 객체와 자료구조  (0) 2020.02.04
[클린코드] 5장 형식  (0) 2020.02.03
[클린코드] 4장 주석  (0) 2020.01.29
[클린코드] 2장 의미 있는 이름  (1) 2020.01.15
[클린코드] 1장 깨끗한코드  (0) 2020.01.12