작게 만들어라!
함수를 만드는 첫번째 규칙은 '가능한 작게' 이다.
왜 이런 이야기가 나오는 걸까? 논문이나 명확한 증거 자료가 있는 것은 아니지만, 적어도 프로그래밍이 등장한 이후부터 단 한번도 틀린 적이 없는 '진리'이기 때문이다.
또한, 함수의 깊이(depth)는 1단이나 2단을 넘어서면 안된다. 그래야 읽고 이해하기 쉬워진다.
한 가지만 해라!
한 메소드에서 여러 일을 처리하는 경우가 있다. 함수는 한 가지 일을 해야 한다. 한 가지 일만 잘해야 한다.
그렇다면 '한 가지'의 판단 기준은 무엇일까?
추상화 수준이 판단 기준이다.
지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한가지 작업만 한다. 우리가 함수를 만드는 이유도 큰 문제를 여러 문제로 나누어 해결하기 위함이다.
요리하는 것을 예로 들어 설명해보겠다.
'피자를 만드는 기능' 을 만들어야 한다고 해보자. 그렇다면 다음과 같이 함수를 도출할 수도 있다.
public Pizza makePizza() {
Dough pizzaDough = makeDough();
Sauce tomatoSauce = getTomatoSauce();
PizzaFrame pizzaFrame = pizzaDough.sauce(tomatoSauce);
putInOven(pizzaFrame);
waitUntilPizzaCooked();
Pizza pizza = takeOutOfOven();
return pizza;
}
1. 도우를 만들고, 2. 토마토 소스를 가져와 바르고, 3. 오븐에 넣고, 기다리고, 피자를 꺼낸다.
(피자에 재료 없이 소스만 발랐다니 이상하긴 한데, 대충 넘어가도록 하자)
위 함수가 한 가지 일만 수행하는 것 처럼 보이는가? 앞서 번호를 매겨서 기능들을 설명했다. 짐작하다싶이 위 함수는 한 가지 일만 하지 않는다. 여러 일을 처리하고 있다. 앞서 함수가 한 가지 일을 하는 기준은 무엇이라 했는가?
지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한가지 작업만 한다.
위 함수의 이름은 makePizza이다. 이 함수명에 맞게 일을 수행하고 있는가? 아니다. 그러면 위 함수를 한 가지 일만 수행하도록 변경해 보겠다.
public Pizza makePizza() {
return cookPizza();
}
// '피자를 만든다' 라는 추상화 수준이 하나이다.
private Pizza cookPizza() {
PizzaFrame pizzaFrame = makePizzaFrame();
Pizza cookedPizza = bakePizzaInOven(PizzaFrame);
return cookedPizza;
}
// '피자를 요리한다' 라는 추상화 수준이 하나이다.
private PizzaFrame makePizzaFrame() {
Dough pizzaDough = makeDough();
Sauce tomatoSauce = getTomatoSauce();
PizzaFrame pizzaFrame = pizzaDough.sauce(tomatoSauce);
return pizzaFrame;
}
// '피자 틀을 만든다' 라는 추상화 수준이 하나이다.
private Pizza bakePizzaInOven(PizzaFrame pizzaFrame) {
putInOven(pizzaFrame);
waitUntilPizzaCooked();
Pizza pizza = takeOutOfOven();
return pizza;
}
// '피자를 오븐에 굽는다' 라는 추상화 수준이 하나이다.
함수는 각각 자신의 역할 하나만을 가지고 있다. 즉, 한 가지를 잘한다고 볼 수 있다. 물론 위 예시에서 더 쪼개질 여지는 충분하다.
이처럼 함수는 '함수명'을 기준으로 추상화 수준이 하나여야 한다.
함수가 '한 가지'만 하는지 판단하는 또 다른 방법이 있다. 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면, 그 함수는 여러 작업을 하는 셈이다.
함수 당 추상화 수준은 하나로
함수 당 추상화 수준은 하나여야 한다.
위 피자 예제에서 추상화 수준을 분류해보자면,
피자를 만든다 -> 추상화 수준 높음
도우를 만든다 -> 추상화 수준 중간
소스를 바른다 -> 추상화 수준 낮음
한 함수 안에 추상화 수준이 같은 코드를 위치시켜라. 그렇지 않다면 코드를 읽기 어렵고, 깨어진 창문처럼 함수에 세부사항들이 점점 추가된다.
또한, 함수는 내려가기 규칙에 의해 위치해야 한다.
내려가기 규칙이란, 추상화 수준이 높은 순으로 함수들을 위치시키는 것을 의미한다.
이렇게 작성하면 추상화 수준을 일관되게 유지하기도 쉽고, 이야기처럼 읽히기도 쉽다.
Switch 문
switch문은 본질적으로 여러 케이스를 포함하는 구조이기 때문에, 작게 만들기 어렵다. 하지만 그렇다고 해서 switch 문을 완전히 피할 방법이 있는 건 또 아니다.
그렇지만 숨길 방법은 있다. switch 문을 저차원 클래스에 숨기는 것이다. 물론 이 과정에서 다형성이 사용된다.
public Money calculateMoney(Employee e) {
switch (e.type) {
case COMMISIONED:
return caclulateCommissionPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvaludEmployeeType(e.type);
}
}
위와 같은 경우는 SRP, OCP를 위반한다. 따라서 다형성을 이용한다. 미리 내부적으로 switch 문을 통해 타입을 계산해 파라미터로 넣어주면, 위 calculateMoney 함수는 switch 문을 제거할 수 있다.
그러나 불가피한 상황도 존재하고, 이 경우에는 규칙을 위반하게 될 수도 있다. 절대적인 Rule은 아니다.
서술적인 이름을 사용하라
서술적: 사건이나 생각 따위를 차례대로 말하거나 적는 것을 특징으로 하는
좋은 이름은 중요하다. 코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드이다.
또한 함수가 작고 단순할수록 서술적인 이름을 고르기도 쉬워진다.
이름이 길어도 괜찮다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
함수 이름을 정할 때는 여러 단어가 쉽게 읽히는 명명법을 사용한다.
또한 이름을 붙일 때에는 일관성이 있어야 한다. 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.
includeSetupAndTearDownPages, includeSetupPages, includeSuiteSetupPage 등이 좋은 예이다. 문체가 비슷하면 이야기를 순차적으로 풀어가기도 쉽다.
함수 인수
함수에서 가장 이상적인 인수 개수는 0개이다. 3개는 피하는 편이 좋으며, 4개는 특별한 이유가 있어도 사용하면 안된다. 인수가 존재하면 그것을 읽는 사람은 인수를 항상 해석하려고 애써야 한다.
테스트 관점에서 보면 더 어렵다. 인수가 없으면 테스트도 간단해진다.
출력 인수는 입력 인수보다 더 어렵다. 보통은 함수에서 결과값을 받으리라 기대하는데, 그렇지 않은 경우라면 코드를 재차 확인해야 한다.
출력 인수란, 인자로 받은 객체를 통해 결과를 내는 것을 의미한다. 즉, 다음과 같은 경우가 출력 인수의 예시이다.
public void makeSalaryDouble(Salary salary) {
salary.changeSalary(salary.getSalary() * 2);
}
이처럼 변환 함수에서 출력 인수를 사용하면 혼란을 일으킨다. 객체 지향 프로그래밍이 나온 뒤로 출력 인수를 사용할 이유는 거의 없다. 츨력 인수로 사용하라고 설계한 변수가 this이기 때문이다. 즉, 위와 같은 경우 아래와 같이 this를 사용하도록 수정할 수 있다.
salary.makeSalaryDouble();
플래그 인수는 사용하지 않는다. boolean 값을 인수로 넘기는 것은 절대 발생해서는 안된다. 함수가 여러 가지를 한번에 처리한다는 것을 공표하는 셈이니까.
이항 함수는 되도록 단항 함수로 바꾸도록 애써야 한다. 객체 설계를 바꿔보자.
인수가 2-3개가 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성을 고려해라. 다음은 예시이다.
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
가변 인수는 하나의 인수로 생각한다. 즉, 다음과 같은 경우 이항 함수로 볼 수 있다.
void dyad(String name, Integer... args);
부수 효과를 일으키지 마라
함수에서 한 가지를 하겠다고 약속하고선 남몰래 다른 일을 하면 안된다. 예상치 못하게 클래스 변수를 수정한다거나..
public boolean isSessionDeleted(User user) {
if (user.session() == NULL) {
return true;
}
user.extendSessionTime(1000);
return false;
}
위와 같은 예시를 보자. 세션이 삭제되었는지를 판단하는 함수인데, 세션이 삭제되지 않은 경우 세션 시간을 늘리고 있다.
이 함수를 사용하는 사람은 이름만 보고 행동을 예측할 수 없게 된다. 이런 부수 효과를 피해야 한다. 이는 혼란을 야기한다.
명령과 조회를 분리하라
함수는 무언가를 수행하거나, 무언가에 답하거나 둘 중 하나만 해야 한다. 둘 다 하면 안된다. 객체 상태를 변경하거나 아니면 객체 정보를 반환하거나 둘 중 하나이다.
public boolean set(String attribute, String value);
위 함수와 같은 경우 이름이 attribute인 속성을 찾아 값을 value로 바꾸고 성공하면 true를 반환한다. 이 경우 독자 입장에서 읽으면 쉽게 의미를 알아내기 힘들다. 함수를 구현한 개발자는 set을 동사로 의도했지만, 읽는 입장에서는 set이 형용사로 느껴지게 된다.
오류 코드 대신 예외를 사용하라
String으로 오류 코드를 상수화 해서 사용할 필요가 없다. try - catch를 통해 오류 메세지를 출력하도록 한다.
단, try - catch 블록은 보기에 그렇게 좋지 않다. 코드 구조에 혼란을 일으키기 때문에, 별도 함수로 뽑아내는 편이 좋다.
// before
try {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
} catch (Exception e) {
logger.log(e.getMessage());
}
// after
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
} catch (Exception e) {
logError(e);
}
}
private void deletePageAndAllReferences(Page page) {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e) {
logger.log(e.getMessage());
}
오류를 처리하는 함수도 함수다. 함수는 한 가지 작업만 해야 한다. 그러므로 오류만 처리해야 한다. 즉, 다음과 같은 경우를 불허한다.
try {
int transformed = Intger.parseInt(word);
} catch (Exception e) {
logger.log(e.getMessage());
}
tramsformed += 2;
...
반복하지 마라
중복은 소프트웨어에서 악의 근원이다. 많은 원칙과 기법이 중복을 없애거나 제거할 목적으로 나왔다. 관계형 데이터베이스에서 정규화가 나온 이유와도 상통한다.
함수를 어떻게 짜죠?
처음부터 완벽하게 나눠진 함수를 만드려고 하지 마라. 소프트웨어를 짜는 것은 글짓기와 비슷하다. 먼저 생각을 기록한 후, 읽기 좋게 다듬는다. 함수도 마찬가지이다.
처음에는 길고 복잡한 함수가 나오더라도, 다듬고 쪼개는 과정을 통해 좋은 함수들을 만들어낸다. 이 과정에서 단위 테스트가 계속 통과하는지 확인하기도 한다.
처음부터 완벽한 함수를 만드는 사람은 없다.
'클린 코드' 카테고리의 다른 글
[클린 코드] 7장: 오류 처리 (0) | 2022.12.02 |
---|---|
[클린 코드] 6장: 객체와 자료 구조 (0) | 2022.11.29 |
[클린 코드] 5장: 형식 맞추기 (0) | 2022.11.29 |
[클린 코드] 4장: 주석 (0) | 2022.11.28 |
[클린 코드] 2장: 의미 있는 이름 (0) | 2022.11.25 |