![[Clean Code] 3. 함수](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzyrXt%2FbtsBJgzNHDl%2FkLdB0Chs5cDP70xv32xGA1%2Fimg.png)
본 게시글은 도서, Clean Code를 읽고 정리한 글입니다.
https://product.kyobobook.co.kr/detail/S000001032980
1. 작게 만들어라!
함수는 작으면 작을수록 좋다.
if문, while문 등에 들어가는 블록은 한 줄이어야 한다. 대개 거기서 함수를 호출하기 때문이다.
그렇게 된다면 바깥을 감싸는 함수 (enclosing function)가 작아질 뿐만 아니라, 블록 내부에서 호출하는 함수의 이름을 적절히 짓는다면 이해하기도 더욱 쉬워진다.
중첩 구조가 생길 만큼 함수가 커져서는 안된다는 뜻이다. 그래야 읽고 이해하기 쉽다.
2. 한 가지만 해라!
지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면, 그 함수는 한 가지 작업만 한다.
단순히 다른 표현이 아니라, 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 수행하는 셈이다.
3. 함수 당 추상화 수준은 하나로!
함수가 확실하게 한 가지의 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.
한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다.
위에서 아래로 코드 읽기 : 내려가기 규칙
코드는 위에서 아래로 마치 이야기처럼 읽혀야 좋다. 그 말은 즉, 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다는 뜻이고, 이 것을 내려가기 규칙이라 지칭한다.
4. Switch 문
switch문은 case 분기가 단 두개인 것이라도 길게 느껴진다.
또한 한 가지 작업만 수행하는 switch 문을 만들기도 어렵다. 이는 switch 문의 본질을 흐리는 문제기 때문이다.
대신, 각 switch 문을 저차원 클래스에 숨기고 반복하지 않는 방법이 있다. 이 때, 다형성(Polymorphism)을 이용한다.
public Money calculatePay(Employee e) throws InvalidEmployeeType {
switch(e.type) {
case COMMISSIONED:
return calculateCommissionnedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
위 함수의 문제
1. 함수가 길다
2. 한 가지 작업만 수행하지 않는다.
3. SRP를 위반한다 : 코드를 변경할 이유가 여럿이기 때문
4. OCP를 위반한다 : 새 직원 유형을 추가할 때마다 코드를 변경해야하기 때문
5. 위 함수와 구조가 동일한 함수가 무한정 존재한다.
위 문제들을, switch 문을 추상 팩토리(Abstract Factory)에 숨기는 방식으로 해결 가능하다.
public abstract class Employee {
public abstract boolean isPayDay();
public abstract Money calculatePay();
public abstract void deliverPay();
}
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:
throw new InvalidEmployeeType(r.type);
}
}
}
다형성으로 인해 실제 파생 클래스의 함수가 실행된다.
위 처럼 상속 관계로 숨긴 후에는 절대 다른 코드에 노출하지 말자. (지키기 어렵기도 하지만.)
5. 서술적인 이름을 사용하라!
서술적인 이름이 무슨 뜻일까?
예를 들어 isTestable, includeSetupAndTearDownPages와 같이, 동사와 명사가 혼합된 형태다.
지난 2장에서 변수 이름의 가치에 대해 그렇게 중요하다고 말한 것 처럼, 함수 이름도 마찬가지다.
코드를 읽으면서 짐작하는 기능이 그대로 수행된다면 그게 진정한 클린 코드겠다.
필자는 역시 이름이 길어도 상관없다고 말한다. => 짐작이 가능한 수준에서 이름을 줄이는게 낫다고 난 생각한다.
주석조차 서술적인 주석이 좋다. 결국 읽으면서 이해하는 내용이 실제와 맞아야 하기 때문에, 자세히 적어야 한다.
또, 이름을 붙일 때는 일관성이 있어야 한다. 결국 모든 것은 사용자가 읽기 편하고 이해하기 쉬워야 하기 때문이다.
6. 함수 인수
함수에서 이상적인 인수의 개수는 무항, 즉 0개다.
한마디로 작을 수록 좋다.
인수가 많아지면, 테스트 코드를 작성하기 까다로워 진다.
그리고 함수가 리턴값을 가진다면, 이를 확인하기 위해 읽는 사람이 여러번 확인해야 할 우려가 있다.
리턴값을 없앨 수 있다면 없애라.
7. 부수 효과를 일으키지 마라!
부수 효과는 결국 하나의 함수에 여러 기능을 작동하도록 만드는 것이다.
public class UserValidator {
private Cryptographer cryptographer;
public boolean checkPassword(String userName, String password) {
User user = UserGateway.findByName(userName);
if(user != User.NULL) {
String codedPhrase = user,getPharaseEncodedByPassword();
String phrase = cryptographer.decrypt(codedPhrase, password);
if("Valid Password".equals(phrase)) {
Session.initialize();
return true;
}
}
return false;
}
}
위 함수에서 부수효과란 뭘까?
Session.initialize()를 말한다.
분명 checkPassword() 함수는 이름만 보면 패스워드를 체크하는 로직처럼 보인다,
그런데 비밀번호가 유효한 경우에 세션을 초기화해버리는데, 이 사실이 함수의 이름에는 드러나지 않는다.
자칫 잘못 호출하면 의도치 않은 세션의 유실이 발생할 수 있다는 것이다.
특히 부수효과의 경우에는 숨겨져있기 때문에, 더욱 위험하다.
8. 명령과 조회를 분리하라!
함수는 뭔가를 수행하거나, 답하거나 둘 중 하나만 해야된다.
또, 함수는 객체 상태를 변경하거나, 객체 정보를 반환하거나 둘 중 하나만 해야 한다.
public boolean set(String attribute, String value);
만약 위 함수는 이름이 attribute인 속성을 찾아서 값을 value로 설정한 후 성공하면 true를 반환하고 실패하면 false를 반환하는 함수라고 해보자.
if(set("username", "unclebob")) {
// ...
}
위처럼 코드를 활용하는 부분을 읽으면 도무지 감이 안온다.
if(attributeExists("username")) {
setAttribute("username", "unclebob");
}
위처럼 명령과 조회를 분리하여 애초에 혼란을 제거하자.
9. 오류 코드보다 예외를 사용하라!
오류 코드 대신 try-catch를 활용한 예외를 사용하면, 오류 처리 코드가 기존 코드에서 분리되므로 깔끔해진다.
그런데 사실, try-catch 함수 자체도 구조가 복잡해 보이고 이쁘지 않다.
이 함수 자체를 따로 빼는게 좋다.
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());
}
이처럼 정상 동작과 오류 처리 동작을 분리하면 코드가 너무 깔끔하다.
10. 반복하지 마라!
중복은 모든 SW에서 악의 근원이다.
중복을 방지하기 위해 여러 Tool들이 만들어지는 이유가 다 있다.
11. 구조적 프로그래밍
데이크스트라(Dijkstra)는 모든 함수와 함수 내 모든 블록에 입구와 출구가 하나씩만 존재해야 한다고 주장한다.
즉, return문은 하나여야 하고, loop 안에 break와 continue를 사용하면 안되고, goto는 절대로 사용하지 말라고 주장한다.
그러나 위 규칙은 함수가 거대화 될 때 유용하다고 볼 수 있다.
우리는 기본적으로 Clean Code를 위해 함수를 작게작게 나눈다.
작은 함수에서는 위 구조적 프로그래밍 방식보다, return/break/continue가 여러 이점을 가져오는 경우가 많다.
다만 goto는 피하자.
12. 함수를 어떻게 짜야할까
마치 글짓기와 같다.
처음에는 길고 복잡하다. 이 때부터 단위 테스트를 먼저 작성한다. 그 다음, 이것들을 하나하나씩 줄여 나가는 것이다.
코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거하고, 메서드를 줄이고 순서를 바꾼다.
이렇게 다듬는 과정에서도 매번 단위 테스트는 반드시 통과해야 한다.
처음 짤 때부터 이상적인 코드, 함수를 짜는 사람은 없다.
'Software Engineering > Clean Code' 카테고리의 다른 글
[Clean Code] 7. 오류 처리 (0) | 2024.02.19 |
---|---|
[Clean Code] 6. 객체와 자료 구조 (1) | 2024.01.25 |
[Clean Code] 5. 형식 맞추기 (1) | 2024.01.24 |
[Clean Code] 4. 주석 (1) | 2024.01.23 |
[Clean Code] 2. 의미 있는 이름 (0) | 2023.11.22 |
개발자가 되고 싶어요.