방명록
- [BOOK - 클린코드] Chapter 3. 함수2025년 02월 06일 01시 17분 17초에 업로드 된 글입니다.작성자: nickhealthy
프로그래밍 초창기에는 시스템을 루틴과 하위루틴으로 나누고 함수가 존재했다.
현재까지 살아남은건 '함수'밖에 없다.작게 만들어라!
함수를 만드는 첫 번째 규칙은 '작게' 만드는 것이다.
블록과 들여쓰기
if/else/while
문 등에 들어가는 블록은 한 줄이어야 한다.- 대개 거기서 함수를 호출한다. 그러면 바깥을 감싸는 함수(enclosing function)가 작아질 뿐 아니라, 블록 안에서 호출하는 함수 이름을 적절히 짓는다면 코드를 이해하기도 쉬워진다.
한 가지만 해라!
- 함수는 한 가지를 잘 해야 한다. 하지만 그 '한 가지'가 무엇인지 알기 어렵지만 다음과 같은 조건으로 판단할 수 있다.
- 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다.
- 또 다른 판단 방법으로는 단순히 다른 표현이 아니라, 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다.
- 다음과 같은 함수명은
renderPageWithSetupsAndTeardowns
함수 안에서는 다음과 같은 일들을 수행할 수 있다.- 페이지가 테스트 페이지인지 판단한다.
- 그렇다면 설정 페이지와 해제 페이지를 넣는다.
- 페이지를 HTML로 렌더링한다.
서술적인 이름을 사용하라!
- 함수가 작고 단순할수록 서술적인 이름을 고르기도 쉬워진다.
- 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
- 길고 서술적인 이름이 길고 서술적인 주석보다 좋다.
- 서술적인 이름을 사용하면 개발자 머릿속에도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.
- 이름을 붙일 때는 일관성이 있어야 한다.
- 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.
🔹 서술적인 이름 사용 예시
includeSetupAndTeardownPages includeSetupPages includeSuiteSetupPage includeSetupPage
함수 인수
- 인수 개수는 적을수록 좋다.(0 ~ 2개가 이상적)
- 테스트 관점에서 보면 인수는 더 어렵다.
- 갖가지 인수 조합으로 함수를 검증하는 테스트 케이스를 작성한다면 여러가지 케이스를 고려해야 한다.
단항 형식
함수에 인수 1개를 넘기는 이유로 가장 흔한 경우는 두 가지다.
- 인수에 질문을 던지는 경우
- ex)
boolean fileExists("MyFile")
- ex)
- 다른 하나는 인수를 뭔가로 변환해 결과를 반환하는 경우
InputStream fileOpen("MyFile")
은String
형의 파일 이름을InputStream
으로 변환한다.
이벤트 함수
다소 드물게 사용하지만 그래도 아주 유용한 단항 함수 형식이 이벤트 함수다.
- 이벤트 함수에는 입력 인수만 있다. 출력 인수는 없다.
- 프로그램은 함수 호출을 이벤트로 해석해 입력 인수로 시스템 상태를 바꾼다.
이항 함수
- 인수 개수가 적을수록 좋지만 좌표계 점 같은 것을 표현해야 할 땐 이항 함수가 적절하며 자연적인 순서도 있다.
- 반면,
assertEquals(expected, actual)
와 같은 함수는 자연적인 순서가 정해지지 않고, 인수 순서를 인위적으로 기억해야 하므로 좋지 못하다.
피해야 하는 형식
- 다음과 같은 단항 함수는 가급적 피한다.
void includeSetupPageInto(StringBuffer pageText)
- 변환 함수에서 출력 인수를 사용하면 혼란을 일으킨다.
- 입력 인수를 변환하는 함수라면 변환 결과는 반환값으로 돌려준다.
- 플래그 인수
- 함수로 부울 값을 넘기는 관례는 끔찍하다.
- 플래그가 참이면 이것을 하고, 거짓이면 저걸 한다는 말이므로 한 가지를 잘해야 한다는 원칙을 지키지 못한다.
명령과 조회를 분리하라!
- 함수는 상태를 변경(명령)하거나 정보를 반환(조회)하는 것 중 하나만 수행해야 한다.
- 예시:
if(set("username", "unclebob"))
->attributeExists("username")
,setAttribute("username", "unclebob")
로 분리시킨다.
오류 코드보다 예외를 사용하라!
- 명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다.
- 명령을 표현식으로 사용하기 쉽다.
- 예시:
if (deletePage(page) == E_OK)
- 오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야하므로 중첩된
if
문이 증가해 코드가 복잡해진다.
- 예외 처리를 사용하면 오류 처리 코드와 비즈니스 로직이 분리되어 가독성이 좋아진다.
try/catch 블록 뽑아내기
- 해당 블록은 정상 동작과 오류 처리 동작을 뒤섞기 때문에 코드 구조에 혼란을 준다.
- 그러므로
try/catch
블록은 별도 함수로 뽑아내는 것이 좋다. - 아래와 같이 정상 동작과 오류 처리 동작을 분리하면 코드를 이해하고 수정하기 쉽다.
🔹 예제 코드
// delete 함수는 모든 오류를 처리한다. public void delete(Page page) { try { deletePageAndAllReferences(page); } catch (Exception e) { logError(e); } } // 실질적인 페이지를 제거하는 함수(비즈니스 처리 로직) public 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()); }
Error.java(에러 처리 클래스) 의존성 자석
아래와 같은 클래스는 의존성 자석이다. 이유는 다음과 같다.
해당 에러 처리 클래스를 사용하는 모든 클래스들은
Error
클래스 변경 시 모두 다시 컴파일하고 다시 배포해야한다.- 프로그래머는 재컴파일/재배치가 번거롭기에 새 오류 코드를 정의하지 않고, 기존 오류를 재사용한다.
🔹 예제 코드
public enum Error { OK, INVALID, NO_SUCH, LOCKED, OUT_OF_RESOURCES, WAITING_FOR_EVENT; }
오류 코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생되므로 재컴파일/재배치 작업 없이도 새 예외 클래스를 추가할 수 있다.
반복하지 마라!
- 같은 코드를 여러 번 작성하지 말고, 중복되는 부분을 별도 함수로 추출하라.
- 많은 원칙과 기법이 중복을 없애거나 제어할 목적으로 나왔다.
- 관계형 데이터베이스의 정규 형식
- 객체 지향 프로그래밍에서 상속
- 구조적 프로그래밍, AOP(Aspect Oriented Programming), COP(Component Oriented Programming)
그럼 함수를 어떻게 짜죠?
- 논문이나 기사를 작성할 땐 먼저 생각을 기록한 후 읽기 좋게 다듬는다.
- 초안은 대개 서투르고 어수선하므로 원하는 대로 읽힐 때까지 말을 다듬고 문장을 고치고 문단을 정리한다.
- 프로그래밍도 마찬가지다.
- 들여쓰기 단계, 중복된 루프, 많은 인수 목록, 즉흥적인 이름 등을 작성한다.
- 다듬어지지 않은 서툰 코드를 빠짐없이 테스트하는 단위 테스트 케이스를 만든다.
- 이후 코드를 다듬고, 필요하다면 전체 클래스를 쪼개는 행위도 실행한다.
- 그럼에도 코드는 항상 단위 테스트를 통과하게 만든다.
결론
- 모든 시스템은 특정 응용 분야 시스템을 기술할 목적으로 프로그래머가 설계한 도메인 특화 언어(Domain Specific Language, DSL)로 만들어진다. 함수는 그 언어에서 동사며, 클래스는 명사다.
- 대가 프로그래머는 시스템을 구현할 프로그램이 아니라 풀어갈 이야기로 여긴다. 시스템에서 발생하는 모든 동작을 설명하는 함수 계층이 바로 그 언어에 속한다.
- 진짜 목표는 시스템이라는 거대한 이야기를 풀어가는 데 있다. 함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아 떨어지면 이야기를 풀어가기 쉽다.
다음글이 없습니다.이전글이 없습니다.댓글