어떤 프로그램이든 가장 기본적인 단위는 함수이다.

작게 만들어라!

  • 함수를 만드는 기본 규칙은 작게이다.
  • 함수 내부에 들어가는 if문/else문/while문 등의 들여쓰기는 1단이나 2단을 넘어서는 중첩 구조가 발생하면 안된다.

한 가지만 해라!

  • 함수는 한 가지를 해야 한다. 그 한가지를 잘 해야 하며, 그 한가지만을 해야 한다.
  • 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다.
  • 함수가 '한 가지’만 하는지 판단하는 방법이 하나 더 있다. 단순히 다른 표현이 아니라 의미있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다.
  • 함수를 여러 섹션으로 나눌 수 있다면 그 함수는 여러 작업을 하는 셈이다.

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

  • 함수가 ‘한가지’ 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.
  • 만약, 한 함수 내에 추상화 수준이 뒤섞이면 읽는 사람이 헷갈린다.
  • 코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 이것을 내려가기 규칙이라고 부른다.
  • 함수 추상화 수준이 아래로 읽으면서 한 단계씩 낮아지는 것이 이상적이다.

Switch문

  • switch문은 작게 만들기 어려우며, ‘한 가지’ 작업만 하도록 구성하기 힘들다.
  • 아래의 코드는 코드를 변경할 이유가 하나가 아닌 여럿이므로 SRP 이론을 위반한다.
  • 그리고 새 직원 유형을 추가할 때마다 코드를 변경하므로 OCP 이론을 위반한다.

[문제가 존재하는 코드]

1
2
3
4
5
6
7
8
9
public int calculatePay(Employee e){
switch(e.type){
case SALARIED :
return calculateSalariedPay();
case HOURLY :
return calculateHourlyPay();
...
}
}
  • switch문은 추상 팩토리에 숨긴다. 아무에게도 보여주지 않는다.

  • 팩토리는 switch문을 사용해 적절한 Employee 파생 클래스의 인스턴스를 생성한다.

  • calculatePay, isPayday, deliverPay 등과 같은 함수는 Employee 인터페이스를 거쳐 호출된다. 그러면 다형성으로 인해 실제 파생 클래스의 함수가 실행된다.

  • 즉, 다형성을 이용하여 해결할 수 있다.

[변경한 코드]

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

public class EmployeeImple implements EmployeeFactory {
@Override
public Employee makeEmployee(EmployeeRecord r){
switch(r.type){
case COMMISSIONED :
return new CommissionedEmployee(r);
case HOURLY :
return new HourlyEmployee(r);
case SALARIED :
return new SalariedEmployee(r);
}
}
}

class CommissionedEmployee extends Employee{
@Override
public boolean isPayday(){
...
}

@Override
public Money calculatePay(){
...
}

@Override
public void deliverPay(Money pay){
...
}
}

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

  • 함수 이름이 길어지더라도 서술적으로 한번에 이해가 가능한 이름이 오히려 깔끔하고 좋은 코드가 된다.
  • 길고 서술적인 이름이 길고 서술적인 주석보다 좋다.
  • 서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다. 좋은 이름을 고른 후 코드를 더 좋게 재구성하는 사례도 있다.
  • 이름을 붙일 때는 일관성이 있어야 한다. 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.
  • 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
2
3
4
5
6
7
8
9
10
11
12
13
if (deletePage(page) == E_OK) {
if (registry.deleteReference(page.name) == E_OK) {
if (configKeys.deleteKey(page.name.makeKey()) == E_OK) {
logger.log("page deleted");
} else {
logger.log("configKey not deleted");
}
} else {
logger.log("deleteReference from registry failed");
}
} else {
logger.log("delete failed"); return E_ERROR;
}

[변경한 코드]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
} catch (Exception e) {
logError(e);
}
}

private void deletePageAndAllReferences(Page page) throws Exception {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}

private void logError(Exception e) {
logger.log(e.getMessage());
}
  • deletePageAndAllReferences 함수는 예외를 처리하지 않는다.
  • 이렇게 정상 동작과 오류 처리 동작을 분리하면 코드를 이해하고 수정하기 쉬워진다.

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

  • 오류 처리도 ‘한 가지’ 작업에 속한다. 그러므로 오류를 처리하는 함수는 오류만 처리해야 마땅하다.
  • 아래는 Error.java
1
2
3
4
5
6
7
8
public enum Error { 
OK,
INVALID,
NO_SUCH,
LOCKED,
OUT_OF_RESOURCES,
WAITING_FOR_EVENT;
}
  • 위의 코드는 의존성 자석이다.
  • 오류를 처리하는 곳곳에서 오류 코드를 사용한다면 Error enum을 사용하게 된다. 즉, Error enum이 변하면 Error enum을 사용하는 클래스 전부를 다시 컴파일하고 다시 배치해야 하므로 비용이 크다.
  • 그러므로 오류 코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생된다. 따라서 재컴파일, 재배치 없이도 새 예외 클래스를 추가할 수 있다.

반복하지 마라!

  • 중복된 코드를 계속 만들지 마라.
  • 중복을 없애면 가독성이 높아진다.

구조적 프로그래밍

  • 다익스트라는 모든 함수와 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 한다고 말했다.
  • 함수는 return문이 하나여야 한다.
  • 루프 안에서 break나 continue를 사용해서는 안되며, goto는 절대로 안된다.

Reference