3. 함수
어떤 프로그램이든 가장 기본적인 단위는 함수
이 장에서는 함수를 잘 만드는 법을 소개
[ 작게 만들어라! ]
- 함수를 만드는 첫째 규칙도 둘째 규칙도 ‘작게!’
- 저자가 이야기 해주는 한 자바 / 스윙 프로그램
• 각 함수가 너무도 명백
• 각 함수가 이야기 하나를 표현
• 각 함수가 멋지게 다음 무대를 준비
▶︎ 블록과 들여쓰기
- if / else문, while문 등에 들어가는 블록은 한 줄이어야 한다
• 대게 그 한 줄에서 함수를 호출한다
- 바깥을 감싸는 함수가 작아진다
- 호출하는 함수의 이름을 적절히 짓는다면, 코드를 이해하기도 쉬워진다
- 중첩 구조가 생길만큼 함수가 커져서는 안된다
[ 한 가지만 해라! ]
- 함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다
- 지정된 함수 이름 아래에 추상화 수준이 하나인 단계만 수행한다면, 그 함수는 한 가지 작업만 한다
• 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈
• 함수 내에 여러 섹션으로 나눠진다면 여러 작업을 한다는 증거
- 우리가 함수를 만드는 이유 : 큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위함
[ 함수 당 추상화 수준은 하나로! ]
- 함수가 확실히 ‘한 가지’ 작업만 하기 위해서는 함수 내 모든 문장의 추상화 수준이 동일해야 한다
- 함수 내 추상화 수준이 섞인다면?
→ 근본 개념인지 세부사항인지 구분하기 어려워 코드를 읽는 사람이 헷갈린다
- 내려가기 규칙
: 프로그램 코드는 위에서 아래로 이야기처럼 읽히며 함수 추상화 수준이 한 번에 한 단계씩 낮아진다
• 일련의 TO 문단을 읽듯 프로그램이 읽힌다
- 💡 추상화 (Abstraction)
• 추상화 : 핵심적 개념 / 기능을 간추려내어 이름을 붙이는 것 (함수로 만드는 것)
• 코드로 드러나는 정보가 많을 수록 추상화 수준이 낮다고 할 수 있다
// 추상화 수준 높음
includeSetupAndTeardownPages(pageData, isSuite);
// 추상화 수준 낮음
WikiPage testpage = pageData.getWikiPage();
String Buffer newPageContent = new StringBuffer();
includeSetupPages(testPage, newPageContent, isSuite);
...
includeTeardownPages(testPage, newPageContent, isSuite);
pageData.setContetn(newPageContent.toString());
[ Switch 문 ]
- switch 문은 작게 만들기 어렵다 🥲
- 각 switch 문을 저차원 클래스에 숨기고 절대 반복하지 않는 방법
• 다형성 이용
• 💡 다형성 (polymoorphism) : 하나의 객체가 여러 타입을 가질 수 있는 것
- switch 문을 사용하는 예제 코드 3-5
• switch문을 상속 관계로 추상 팩토리에 숨긴다
• 다형성 객체를 생성하는 코드에서 switch문 사용
• 다형성을 이용하여 실제 파생 클래스 함수를 실행한다
[ 서술적인 이름을 사용하라! ]
- 한 가지만 하는 작은 함수에 좋은 이름을 붙인다
• 함수가 작고 단순할 수록 서술적 이름을 고르기도 쉽다
- 이름이 길어도 좋다
• 길고 서술적인 이름 > 길고 서술적인 주석
- 여러 단어가 쉽게 읽히는 명명법 사용 + 여러 단어를 사용해 기능을 잘 표현하는 이름 선택
- 이름을 정하느라 시간을 들여도 괜찮다
• 여러 이름을 코드에 넣어보며 읽어보면 더 좋다
- 이름을 붙일 때에는 일관성이 있어야 한다
• 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용
[ 함수 인수 ]
- 함수에서 이상적인 인수 개수는 0개 (무항)
• 0개 > 1개 > 2개 >>>>>>> 3개 (삼항은 가능한 피하라)
- 인수는 개념을 이해하기 어렵게 만든다
• 코드를 읽는 사람이 발견할 때마다 의미를 해석해야 한다
- 테스트 관점에서도 어렵다
• 갖가지 인수 조합으로 테스트 케이스를 작성 🥲
- 출력 인수는 입력 인수보다 이해하기 어렵다
• 대개 함수에서 인수로 결과를 받을 것이라 예상하지 않는다
▶︎ 많이 쓰는 단항 형식
- 가장 흔한 경우
1. 인수에게 질문을 던지는 경우
ex. boolean fileExists(”MyFile”)
2. 인수를 뭔가로 변환해 결과를 반환하는 경우
ex. InputStream fileOpen(”MyFile”)
- 드물지만 유용한 경우 : 이벤트 함수
• 입력 인수만 있고 출력 인수는 없다
• 입력 인수가 시스템 상태를 바꾼다 → 조심할 필요 있다
• 이벤트라는 사실이 드러나도록 이름과 문맥을 주의
- 위 경우가 아니라면 단항 함수는 피한다
• 변환 함수에서 출력 인수 사용하면 혼란 일으킨다
▪︎ 입력 인수를 변환한다면 변환 결과는 반환값으로 돌려준다
▪︎ 입력 인수를 그대로 돌려주더라도, 변환 형태는 유지하기 때문에
▶︎ 플래그 인수
- 플래그 인수는 추하다
• 함수가 한꺼번에 여러 가지를 처리한다고 공표하는 셈!
▶︎ 이항 함수
- 이해 난이도 : 인수 2개 > 인수 1개
- 이항 함수가 적절한 경우
• ex. 직교 좌표계 점 : Point p = new Point(0, 0)
▪︎ 이런 경우 인수 2개는 한 값을 표현하는 두 요소
• ex. assertEquals(expected, actual)
▪︎ 인수 순서를 헷갈리는 경우 多
- 이항 함수는 위험이 따른다는 사실을 이해하고 사용
• 가능하면 단항 함수로 바꾸도록 노력
▶︎ 삼항 함수
- 이해 난이도 : 인수 3개 >>> 인수 2개
• 신중히 고려하여 사용하라
▶︎ 인수 객체
- 인수가 2-3개 필요하다면, 일부를 클래스 변수로 선언할 가능성 고려
Circle makeCircle(Point center, double radius);
Circle makeCircle(double x, double y, double radius);
- 변수를 묶으려면 이름을 붙여야 하므로 개념을 표현하게 됨! 😊
▶︎ 인수 목록
- 인수 개수가 가변적인 함수가 필요한 경우가 있다
- String.format(”%s worked %.2f hours.”, name, hours);
• 사실상 이항 함수
• 선언부 : public String format(String format, Object… args)
- 가변 인수를 취하는 함수는 단항, 이항, 삼항 함수로 취급 가능
▶︎ 동사와 키워드
- 함수의 의도 / 인수의 순서와 의도를 표현하기 위해서는 좋은 함수 이름 필요
- 단항 함수는 함수-인수가 동사-명사 쌍을 이뤄야 한다
• ex. writeField(name)
- 함수 이름에 키워드를 추가
• ex. assertEquals
→ assertExpectedEqualsActual(expected, actual)
: 인수 순서를 기억할 필요 ❌
[ 부수 효과를 일으키지 마라! ]
- 함수에서 한 가지를 수행하겠다고 하고, 남몰래 다른 짓을…!
• ex. 예제 코드 3-6 : checkPassword
내에서 세션 초기화
▪︎ 함수 이름만 보고 함수를 호출하면 세션 정보를 지워버릴 위험
▪︎ 시간적인 결합 - 세션을 초기화해도 괜찮은 경우에만 checkPassword
호출 가능
- 많은 경우 시간적인 결합이나 순서 종속성 초래
[ 출력 인수 ]
- 일반적으로 인수를 함수 입력으로 해석
- 출력 인수라는 사실을 알기 위해 함수 선언부를 찾아봐야 확실해짐
• 인지적으로 거슬린다 → 피해야 한다
- 객체 지향 언어에서는 출력 인수를 사용할 필요가 드물다
• 출력 인수로 사용하라고 설계한 변수 : this
• 함수에서 상태를 변경해야 한다면, 함수가 속한 객체의 상태를 변경해라
[ 명령과 조회를 분리하라! ]
- 객체 상태를 변경하거나 / 객체 정보를 반환하거나
• 둘 다 하면 안돼 !
- if (set(”username”, “unclebob”)) …
• 설정됨을 확인하는 코드인지 설정하는 코드인지 의미가 모호
[ 오류 코드보다 예외를 사용하라! ]
- if문에서 명령을 표현식으로 사용하는 경우
• 명령 / 조회 분리 규칙을 미묘하게 위반
• 여러 단계로 중첩되는 코드를 야기
• 오류 코드 반환 → 호출자는 오류 코드 곧바로 처리해야 함
if (delete(page) == E_OK) {
if (registry.deleteReference(page.name) == E_OK) {
...
} else {
logger.log("deleteReference from registry failed");
}
} else {
logger.log("delete failed"); return E_ERROR;
}
- 오류 코드 대신 예외 사용
• 오류 처리 코드가 원래 코드에서 분리 → 깔끔
▶︎Try / Catch 블록 뽑아내기
- try / catch 블록은 원래 추하다
• 코드 구조 혼란 & 정상 동작 - 오류 처리 동작을 섞는다
• 별도 함수로 뽑아내는 편이 좋다
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
}
catch (Exception e) {
logError(e);
}
}
- 정상과 오류 처리 동작 분리 → 코드의 이해와 수정에 용이
▶︎ 오류 처리도 한 가지 작업이다
- 오류를 처리하는 함수는 오류만 처리해야 마땅하다
▶︎ Error.java 의존성 자석
- 클래스든 열거형 변수든 오류 코드를 정의 → 의존성 자석
• Error enum 수정 → Error enum 사용하는 클래스 전부 다시 컴파일 & 다시 배치
- 예외를 사용
• 새 예외는 Exception 클래스에서 파생
• 재컴파일 / 재배치 없이도 새 예외 클래스 추가 가능
[ 반복하지 마라! ]
- 알고리즘 중복은 코드 길이를 늘릴 뿐 아니라 알고리즘이 변했을 때 여러 곳을 손보게 한다
• 오류 발생 확률도 ↑
- 중복은 소프트웨어에서 모든 악의 근원일 수도
- 중복을 없애거나 제어할 목적의 많은 원칙과 기법
• 관계형 DB의 정규형식 : 자료에서 중복 제거
• 객체 지향 프로그래밍 : 코드를 부모 클래스로 몰아 중복 제거
• 구조적 프로그래밍, AOP, COP …
[ 구조적 프로그래밍 ]
- 구조적 프로그래밍 원칙
• 함수의 return문은 하나
• 루프 안에서 break, continue 안돼 / goto 절대로 안돼
• 함수가 아주 클 때만 상당한 이익 제공
- 함수를 작게 만든다면
• return, break, continue 여러 차례 사용 OK
▪︎ 오히려 의도를 표현하기 쉬워진다
• goto ❌ : 큰 함수에서만 의미 있다
[ 함수를 어떻게 짜죠? ]
- 처음에는 길고 복잡하다
• 서툰 코드를 빠짐없이 테스트하는 단위 테스트 케이스도 작성
- 원하는 대로 다듬고 정리
• 코드를 다듬고, 함수를 만들고, 이름도 바꾸고 중복 제거
• 메서드를 줄이고 순서도 변경
• 전체 클래스를 쪼개기고
• 와중에도 코드는 항상 단위 테스트를 통과
- 최종적으로 이 장에서 설명한 규칙을 따르는 함수가 얻어진다!
[ 결론 ]
- 시스템은 시스템을 기술할 목적으로 프로그래머가 설계한 도메인 특화 언어로 만들어진다
• 그 언어에서 함수는 동사 / 클래스는 명사
- 프로그래밍의 기술은 언제나 언어 설계의 기술
• 좀 더 풍부하고 좀 더 표현력 강한 언어를 만들어 이야기 풀어가도록
• 시스템의 동작을 설명하는 함수가 바로 그 언어에 속한다
- 분명하고 정확하여 깔끔하게 맞아떨어지는 ‘함수’라는 언어로 이야기를 풀어나가자
'Books' 카테고리의 다른 글
[Clean Code] 2장 의미 있는 이름 (0) | 2023.09.18 |
---|---|
[Clean Code] 1장 깨끗한 코드 (0) | 2023.09.18 |