클린 코드 - 3장 함수
26 Mar 2021Intro
초서-독서 한 내용을 그대로 적는 곳이기 때문에 책을 읽지 않은 분들이 보기에 맥락이 애매할 수 있습니다.
초서 : 책을 읽는데 그치는 것이 아닌 손을 이용해 책의 중요한 내용을 옮겨 적음으로써 능동적으로 책의 내용을 수용하고 판단하여 새로운 지식을 재창조하는 과정. 메타인지 학습법
Index
- 들어가면서
- 1장 깨끗한 코드
- 2장 의미 있는 이름
- 3장 함수 «
- 4장 주석
- 5장 형식 맞추기
- 6장 객체와 자료 구조
- 7장 오류 처리
- 8장 경계
- 9장 단위 테스트
- 10장 클래스
- 11장 시스템
- 12장 창발성(創發性)
- 13장 동시성
- 14장 점진적인 개선
- 15장 JUnit 들여다보기
- 16장 SerialDate 리팩터링
- 17장 냄새와 휴리스틱
3장 함수
” 의도를 분명히 표현하는 함수를 어떻게 구현할 수 있을까 ? “
작게 만들어라!
” 함수를 만드는 첫째 규칙은 ‘작게’, 두 번째 규칙은 ‘더 작게!’ “
- 각 함수가 이야기 하나를 표현한다. (쓰임이 명백해야 한다)
- if 문 / else 문 / while 문 등에 들어가는 블록은 한 줄이어야 한다.
- 대게 여기서 함수를 호출한다. (바깥을 감싸는 함수가 작아져 가독성이 높아진다)
- 즉 중첩 구조가 생길만큼 함수가 커져서는 안 된다.
한 가지만 해라!
” 함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다. “
- 지정된 함수 이름 아래에서 ‘추상화 수준이 하나인 단계만 수행’ 한다면 그 함수는 한 가지 작업만 한다.
- 단일 책임 원칙 (SRP) 과 같은 말.
함수 당 추상화 수준은 하나로!
- 함수 내 모든 문장의 ‘추상화 수준이 동일’ 해야 함.
- 한 함수 내에 여러 추상화 수준이 섞여있으면 읽는 사람이 헷갈림.
- 추상화 수준이 높은 코드
getHtml()
- 추상화 수준이 낮은 코드
.append("\n")
- 추상화 수준이 높은 코드
- 위에서 아래로 ‘이야기’ 처럼 읽혀야 좋다.
- 위에서 아래로 한 번 내려갈 수록, 추상화 수준도 한 단계씩 낮아져야 함.
Switch 문
- 다형성을 이용하여 저차원 클래스에 숨기고 반복을 피해야 한다.
SRP 를 위반한 Switch 사용 함수 예시
public Money calculatePay(Employee e) throw InvalidEmployeeType {
switch (e.type) {
case COMMISSIOND:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
문제점
- 함수가 길다. (새 직원 유형을 추가하면 더 길어짐)
- 한 가지 작업만 수행하지 않는다.
- SRP를 위반한다. (코드를 변경할 이유가 여럿이기 때문)
- OCP를 위반한다. (새 직원을 추가할 때마다 코드를 변경해야 하기 때문)
- 제일 심각한 문제는 위 함수와 구조가 동일한 함수가 무한정 생성될 수 있다.
isPayDay(Employee e, Date date)
deliverDay(Employee e, Money pay)
추상 팩토리 사용하여 해결
public abstract class Employee {
public abstract boolean isPayDay();
public abstract Money calculatePay();
public abstract void deliverDay(Money pay);
}
---
public interface EmployeeFactory {
Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
---
public class EmployeeFactoryImpl implements EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
switch (r.type) {
case COMMISSIOND:
return new CommissionedEmployee(r);
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmployee(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
- 팩토리는
switch
문을 사용해 적절한Employee
파생 클래스의 인스턴스를 생성. calculatePay
,isPayDay
,deliverDay
와 같은 함수는 Employee 인스턴스를 거쳐 호출.- 다형성으로 인해 실제 파생 클래스의 함수가 실행됨.
서술적인 이름을 사용하라!
- 함수가 하는 일을 잘 표현해야 한다.
- private 함수 또한 서술적인 이름을 지어야 한다.
길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
길고 서술적인 이름이 길고 서술적인 주석보다 좋다.
- 이름을 붙일 때는 일관성이 있어야 한다.
- 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.
includeSetupAndTeardownPages
,includeSetupPages
,includeSuiteSetupPage
,includeSetupPage
등이 좋은 예다.-
“includeTearDownPages, includeSuiteTeardownPage 도 있나요?” 당연하다. ‘짐작하는 대로’다.
함수 인수
” 함수에서 이상적인 인수 개수는 0개(무항)다. “
- 인수(매개변수)는 개념을 이해하기 어렵게 만든다.
- 함수 이름과 인수 사이에 추상화 수준이 같아야 한다.
- 인수가 3개를 넘어가면 인수마다 유효한 값으로 모든 조합을 구성해 테스트하기가 상당이 부담스러워 짐.
” 출력 인수는 입력 인수보다 이해하기 어렵다. “
- 출력 인수는 매개변수로 받은 출력기능을 하는 객체이다.
appendFooter(StringBuffer report)
에StringBuffer
와 같은.
- 인수로 입력을 넘기고 반환값으로 출력을 받는다는 개념이 일반적이다.
- 대개 함수에서 인수로 결과를 받으리라 기대하지 않는다. (코드를 재차 확인하게 만든다)
- 객체 지향 언어에서는 출력 인수를 사용할 필요가 거의 없다.
- 출력 인수로 사용하라고 설꼐한 변수가 바로
this
이기 때문 public void appendFooter(StringBuffer report)
보다는report.appendFooter()
더 낫다.- 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다.
- 출력 인수로 사용하라고 설꼐한 변수가 바로
많이 쓰는 단항 형식
- 인수에 질문을 던지는 형식
boolean fileExists("myFileName")
- 인수를 뭔가로 변환해 결과로 반환하는 형식
InputStream fileOpen("myFileName")
- 이벤트 형식 (입력 인수로 시스템 상태를 변경)
passwordAttemptFailedNtimes(int attempts)
주의 해야할 단항 형식
- 위에서 설명한 경우가 아니라면 단항 함수는 가급적 피한다.
- 변환 함수에서 출력 인수를 사용하면 혼란을 일으킨다.
void includeSetupPageInto(StringBuffer pageText)
- 입력 인수를 변환하는 함수라면 변환 결과는 반환값으로 돌려준다.
StringBuffer transform(StringBuffer in)
- 플래그 인수는 추하다.
- 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈.
- 인수가 2~3개 필요하다면 독자적인 클래스 변수로 선언할 가능성을 짚어본다.
동사와 키워드
- 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다.
writeField(name)
- 함수 이름에 키워드를 추가하는 형식 (인수 순서에 따라)
assertEquals
보다는assertExpectedEqualsActual(expected, actual)
이 더 좋음.
부수 효과를 일으키지 마라!
” 함수에서 한 가지를 하겠다고 약속하고선 남몰래 다른 짓을 하지 마라. “
- 부수 효과는 시간적인 결합이나 순서 종속성을 초래한다.
- 시간과 결합한다는 의미는 동시성을 띄지 못함을 의미(시간에 종속되어).
- cf) 실용주의 프로그래머
명령과 조회를 분리하라!
- 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만.
- 객체 상태를 변경하거나 아니면 객체 정보를 반환하거나 (둘 중 하나!)
public boolean set(String attribute, String value)
이러한 함수는
if (set("username", "unclebob"))
과 같은 괴상한 코드가 나온다.- 코드만 봐서는 의미가 모호함. (set 단어가 명령인지 조회인지…)
- username 을 unclebob으로 설정(set)하는 것인지, 설정되어 있다면 인지…
- 해결책은 명령과 조회를 분리해 혼란을 애초에 뿌리뽑는 방법이다.
if(attributeExists("username"))
과setAttribute("username", "unclebob")
으로 나눠야함.
오류 코드보다 예외를 사용하라!
- 명령 함수에서 오류 코드를 if 표현식으로 중첩하여 반환하지 마라.
- 호출자는 오류 코드를 곧바로 처리해야 함.
- 오류 코드 대신 예외를 사용하라.
- try / catch 블록은 코드 구조에 혼란을 준다. (별도로 뽑아내는 것이 좋음)
- 오류 처리도 ‘한 가지’ 작업에 속한다. (오류를 처리하는 함수는 오류만 처리해야 마땅하다)
Error.java 의존성 자석
오류 코드를 반환한다는 이야기는, 클래스든 열거형 변수(enum)든, 어디선가 오류 코드를 정의한다는 뜻
public enum Error {
OK,
INVALID,
NO_SUCH,
LOCKED,
...
}
위와 같은 코드는 ‘의존성 자석’ 이다.
다른 클래스에서 Error enum을 import해 사용해야 하므로, 이 에러코드를 사용하는 클래스 전부를 다시 컴파일 하고 다시 배치해야 한다.
반복하지 마라!
- 중복을 없애면 모듈 가독성이 크게 높아진다.
구조적 프로그래밍
” 데이크스트라는 모든 함수와 함수 내 모든 블록에 입구와 출구 하나만 존재해야 한다고 말했다. “
함수를 어떻게 짤까?
” 글짓기와 비슷하다. (생각을 기록한 후 읽기 좋게 다듬는다)
처음에는 길고 복잡하다.
그 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스를 만든다.
그런다음 코드를 가다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다.
메서드를 줄이고 순서를 바꾼다. 때로는 클래스를 쪼개기도 한다.
이 와중에도 코드는 항상 단위 테스트를 통과한다.
- 들어가면서
- 1장 깨끗한 코드
- *2장 의미 있는 이름
- 3장 함수 «
- 4장 주석
- 5장 형식 맞추기
- 6장 객체와 자료 구조
- 7장 오류 처리
- 8장 경계
- 9장 단위 테스트
- 10장 클래스
- 11장 시스템
- 12장 창발성(創發性)
- 13장 동시성
- 14장 점진적인 개선
- 15장 JUnit 들여다보기
- 16장 SerialDate 리팩터링
- 17장 냄새와 휴리스틱