Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- H2 설치
- Spring
- MySQL
- 오블완
- Codeup
- java
- 알고리즘
- 롬복
- 클린코드
- mariadb
- 기초100제
- 파이썬
- Spring Boot
- go
- Vue.js
- Git
- Gradle
- 객사오
- 티스토리챌린지
- Python
- 코드업
- 클린 코드
- 스프링
- GitHub
- Postman
- thymeleaf
- spring security
- springboot
- golang
- JPA
Archives
- Today
- Total
nyximos.log
[Clean Code] 3장, 함수 본문
클린 코드, 애자일 소프트웨어 장인정신
Robert C. Martin
들어가면서
어떤 프로그램이든 가장 기본적인 단위가 함수다.
이 장은 함수를 잘 만드는 법을 소개한다.
작게 만들어라!
일반적으로 다음 코드보다 짧아야 한다.
public static String renderPageWithSetupAndTeardowns(
PageData pageData, boolean isSuite
) throws Exception {
boolean isTestPage = pageData.hasAttribute("Test");
if(isTestPage){
WikiPage testPage = pageData.getWikiPage();
StringBuffer newPageContent = new StringBuffer();
includeSetupPages(testPage, newPageContent, isSuite);
newPageContent.append(pageData.getContent());
includeTeardownPages(testPage. newPageContent, isSuite);
pageData.setContent(newPageContent.toString());
}
return pageData.getHtml();
}
다음 코드로 줄이는 것이 마땅하다.
public static String renderPageWithSetupsAndTeardowns(
PageData pageData, boolean isSuite) throws Exception{
if(isTestPage(pageData))
include SetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
블록과 들여쓰기
- if/else문, while문 등에 들어가는 블록은 한줄이어야 한다.
- 대게 거기서 함수를 호출한다.
- 그러면 바깥을 감싸는 함수 enclosing function가 작아질 뿐 아니라, 블럭 안에서 호출하는 함수 이름을 적절히 짓는 다면 코드를 이해하기도 쉬워진다.
- 함수에서 들여쓰기 수준은 1단이나 2단을 넘어서면 안 된다.
한 가지만 해라!
- 함수는 한 가지를 해야 한다. 그 한가지를 잘 해야 한다. 그 한가지만을 해야 한다.
- 우리가 함수를 만드는 이유는 큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서이다.
함수 당 추상화 수준은 하나로!
- 함수가 확실히 '한 가지' 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.
- 한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다.
위에서 아래로 코드 읽기: 내려가기 규칙
- 코드는 위에서 아래로 이야기 처처럼 읽혀야 좋다.
- 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.
- 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다.
Switch 문
- case 분기가 단 두 개인 switch 문도 내 취향에는 너무 길며, 단일 블록이나 함수를 선호한다.
- switch문을 저차원 클래스에 숨기고 절대로 반복하지 않는 방법은 있다.
물론 다형성 polymorphism을 이용한다.
직원 유형에 따라 다른 값을 계산해 반환하는 함수
public Money calculatePay(Employee e) throw InvalidEmployeeType{
switch(e.type){
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
이 함수에는 몇 가지 문제가 있다.
- 함수가 길다. 새 직원 유형을 추가하면 더 길어진다.
- '한 가지' 작업만 수행하지 않는다.
- SRP를 위반한다. 코드를 변경할 이유가 여럿이기 때문이다.
- OCP를 위반한다. (새 직원 유형을 추가할 때마다 코드를 변경하기 때문에)
- 위 함수와 구조가 동일한 함수가 무한정 존재하다.
- switch문을 추상 팩토리에 숨기자.
- 팩토리는 switch 문을 사용해 적절한 Employee 파생 클래스의 인스턴스를 생성한다.
- calculatePay, isPayday, deliverPay 등과 같은 함수는 Employee 인터페이스를 거쳐 호출된다.
- 그러면 다형성으로 인해 실제 파생 클래스의 함수가 실행된다.
public abstract class Employee{
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliveryPay(Money pay);
}
public interface EmployeeFactory{
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
public class EmployeeFactoryImpl implements 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);
}
}
}
서술적인 이름을 사용하라!
- 이름 길어도 괜찮다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
- 길고 서술적인 이름이 길고 서술적인 주석보다 좋다.
- 함수 이름을 정할 때는 여러 단어가 쉽게 읽히는 명명법을 사용한다.
- 그 다음 여러 단어를 사용해 함수 기능을 잘 표현하는 이름을 선택한다.
- 서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.
- 이름을 붙일 때는 일관성이 있어야 한다.
- 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.
(includeSetupAndTeardownPages, includeSetupPages, includeSuiteSetupPage, includeSetupPage 등)
함수 인수
- 함수에서 이상적인 인수 개수는 0개(무항)이다.
- 4개 이상(다항)은 특별한 이유가 필요하다. 특별한 이유가 있어도 사용하면 안된다.
- includeSetupPageInto(new PageContent) 보다 includeSetupPage()가 이해하기 더 쉽다.
많이 쓰는 단항 형식
- 함수에 인수 1개를 넘기는 이유중 하나는 인수에 질문을 던지는 경우다.
- 다른 하나는 인수를 뭔가로 변환해 결과를 반환하는 경우다.
- 다소 드물게 사용하지만 그래도 아주 유용한 단항 함수 형식이 이벤트이다. (입력 인수 🙆♀️ 출력인수🙅♀️)
프로그램은 함수 호출을 이벤트로 해석해 인수로 시스템 상태를 바꾼다.
플래그 인수
- 함수로 부울 값을 넘기는 관례는 정말로 끔찍하다.
이항 함수
- 인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어렵다.
- Point p = new Point(0,0)은 좋은 예지만, 여기서 인수 2개는 한 값을 표현하는 두 요소다.
- writeField(outputStream, name)에서 두 인수는 한 값을 표현하지도, 자연적인 순서가 있지도 않다.
- 첫 인수를 무시하게 될 수도 있고 이 무시한 코드에 오류가 숨어들 수도 있다.
- 따라서 위험이 따른다는 사실을 이해하고 가능하면 단항 함수로 바꾸도록 애써야 한다.
- writeField 메서드를 outputStream 클래스 구성원으로 만들어 outputStream.writeField(name)으로 호출한다.
- outputStream을 현재 클래스 구성원 변수로 만들어 인수로 넘기지 않는다.
- FieldWriter라는 새 클래스를 만들어 구성자에서 outputStream을 받고 write메서드를 구현한다.
삼항 함수
- 인수가 3개인 함수는 인수가 2개인 함수보다 훨씬 이해하기 어렵고 순서, 주춤, 무시로 야기되는 문제가 두 배 이상 늘어난다.
인수 객체
- 인수가 2-3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성을 짚어본다.
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
인수 목록
- 때로는 인수 개수가 가변적인 함수도 필요하다.
String.format("%s worked %.2f" hours.", name, hours);
동사와 키워드
- 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다.
- ex. write(name)보다 writeField(name)
- 함수 이름에 키워드(인수 이름)를 추가하면 인수 순서를 기억할 필요가 없어진다.
- ex. assertEquals보다 assertExpectedEqualsActual(expected, actual)이 더 좋다.
부수 효과를 일으키지 마라!
- 부수 효과는 시간적인 결합을 초래한다.
- 시간적인 결합은 혼란을 일으킨다. 부수 효과로 숨겨진 경우에는 더더욱 혼란이 커진다.
- 시간적인 결합이 필요하다면 함수 이름에 분명이 명시한다.
출력 인수
- 객체 지향 언어에서는 출력 인수를 사용할 필요가 거의 없다.
- 출력 인수로 사용하라고 설계한 변수가 바로 this이기 때문이다.
- 일반적으로 출력 인수는 피해야 한다.
- 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다.
명령과 조회를 분리하라!
- 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다. 둘 다 하면 안 된다.
- 객체 상태를 변경하거나 아니면 객체 정보를 반환하거나 둘 중 하나다. 둘다 하면 혼란을 초래한다.
오류 코드보다 예외를 사용하라!
- 명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다.
- 자칫하면 if문에서 명령을 표현식으로 사용하기 쉬운 탓이다.
- 아래 코드는 동사/형용사 혼란을 일으키지 않는 대신 여러 단계로 중첩되는 코드를 야기한다.
if(deletePage(page) == E_OK)
- 반면 오류 코드 대신 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다.
Try/Catch 블록 뽑아내기
- try/cahtch 블록을 별도 함수로 뽑아내는 편이 좋다.
- 정상 동작과 오류 처리 동작을 분리하면 코드를 이해하고 수정하기 쉬워진다.
오류 처리도 한 가지 작업이다.
- 함수는 '한 가지' 작업만 해야 한다.
- 오류 처리도 '한 가지' 작업에 속한다.
Error.java 의존성 자석
- 오류 코드를 반환한다는 이야기는, 클래스든 열거형 변수든, 어디선가 오류 코드를 정의한다는 뜻이다.
public enum Error {
OK,
INVALID,
NO_SUCH,
LOCKED,
OUT_OF_RESOURCES,
WAITING_FOR_EVENT;
}
- 위와 같은 클래스는 의존성 자석이다.
- 다른 클래스에서 Error enum을 import해 사용해야 하므로 변환한다면 Error enum을 사용하는 클래스를 전부 다시 컴파일하고 다시 배치해야 한다.
반복하지 마라!
- 중복은 코드길이가 늘어날 뿐 아니라 알고리즘이 변하면 여러곳을 손봐야한다.
- 오류가 발생할 확률도 높다.
구조적 프로그래밍
- 함수를 작게 만든다면 간혹 return, break, continue를 여러 차례 사용해도 괜찮다.
- 때로는 단일 입/출구 규칙보다 의도를 표현하기 쉬워진다.
- goto문은 큰 함수에서만 의미가 있으므로 작은 함수에서는 피해야만 한다.
함수를 어떻게 짜죠?
- 처음에는 길고 복잡하다. 들여쓰기 단계도 중복된 루프도 많고 인수 목록도 아주 길다.
이름은 즉흥적이고 코드는 중복된다. 코드를 빠짐없이 테스트 하는 단위 테스트 케이스도 만든다. - 그 다음 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다.
메서드를 줄이고 순서를 바꾼다. 때로는 전체 클래스를 쪼개기도 한다.
이와중에도 코드는 항상 단위 테스트를 통과한다. - 처음부터 탁 짜내지는 않는다. 그게 가능한 사람은 없으리라.
결론
- 진짜 목표는 시스템이라는 이야기를 풀어나가는 데 있다.
- 함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아 떨어져야 이야기를 풀어나가기가 쉬워진다.
'Books' 카테고리의 다른 글
[Clean Code] 6장, 객체와 자료 구조 (0) | 2022.02.22 |
---|---|
[Clean Code] 5장, 형식 맞추기 (0) | 2022.02.18 |
[Clean Code] 4장, 주석 (0) | 2022.02.16 |
[Clean Code] 2장, 의미 있는 이름 (0) | 2022.02.11 |
[Clean Code] 1장, 깨끗한 코드 (0) | 2022.02.10 |