[Clean Code] Chapter 3 함수
잘 읽히는 함수를 만드는 규칙을 몇가지 소개하려 한다.
해당 포스팅 내용은 책 'Clean Code - Robert C. Martin' 출처로 합니다.
1. 작게 만들어라
함수를 만드는 첫째 규칙은 작게 둘째 규칙은 더 작게 이다.
이에 대한 근거나 증거를 대기에는 무리가 있지만, 긴 글보단 짧은 글이 더 읽기 쉬운 것은 당연하다.
일반적으로 if/ else/ while 문 등에 들어가는 블록은 한 줄이어야 한다. 그렇게 되면 바깥의 함수가 작아지게 되고,
호출하는 함수의 이름이 적절하다면 코드를 이해하기 쉬워진다.
즉 , 중첩구조가 생길 만큼 함수가 커져서는 안된다. 함수에서 들여쓰기는 1단, 2단이 적당하다.
2. 한가지만 해라
우리가 함수를 만드는 이유는 큰 개념을 다음 추상화 단계에서 여러 단계로 나눠 수행하기 위해서이다.
함수는 한가지를 해야한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야한다.
이 충고에서 문제는 그 '한 가지'가 무엇인가 이다.
하나의 함수에서 추상화 수준이 하나인 단계만 수행한다면 한 작업만 한다.
의미있는 이름으로 다른 함수를 추출할 수 있거나 여러 섹션으로 나뉜다면 그 함수는 여러 작업을 하고있는 것이다.
3. 함수 당 추상화 수준은 하나로
함수가 한 가지의 작업만을 하기 위해서는 함수 내 모든 문장의 추상화 레벨이 같아야 한다.
한 함수 내에 여러 추상화 수준이 나오면 코드가 복잡해 진다. 특정 표현이 근본개념인지 세부사항인지 헷갈리기 때문이다.
이에 더해 코드를 유지 보수할 때에도 깨진 유리창의 법칙처럼 다른 사람들도 다른 추상화 레벨을 더해 간다.
위에서 아래로 코드 읽기 : 내려가기 규칙
코드는 위에서 아래로 이야기 처럼 읽혀야한다.
호출 당한 함수는 호출한 함수의 한 단계 낮은 추상화 레벨이 되어야 한다.
4. Switch 문
switch 문은 본질적으로 N개를 처리하기 때문에 한 가지 작업만을 처리하게 작성하기 힘들다.
하지만 다형성을 이용해 각 switch문을 저차원 클래스에 숨겨 이를 반복하지 않을 수 있다.
목록 3-4는 SRP, OCP 원칙을 위해하고 있어 코드를 변경할 일이 잦고 변경이 쉽지 않다. 즉 유지보수에 문제가 있다.
가장 큰 문제는 동일한 구조의 함수가 다수 추가될 수 있다는 점이다.
이러한 문제는 목록 3-5 처럼 switch문을 다형성 객체를 생성하는 코드에 숨긴다.
팩토리는 Employee의 파생 클래스의 객체를 생성하고, 다양한 메서드들은 같은 이름으로 그 객체에 의해 실행된다.
5. 서술적인 이름을 사용하라
앞장에서도 말했듯 좋은 이름의 가치는 몇 번을 강조해도 지나치지 않다.
함수가 작고 단순할수록 이름을 붙이기도 쉽다. 그 함수가 하는 일을 서술적으로 적어주면 되기 때문이다.
서술적인 이름을 사용하면 개발자 머릿속에도 설계가 뚜렷해져 코드 개선에 도움을 준다.
이름을 붙일 때는 일관성이 있어야 한다. 모듈 내 함수 이름은 (뜻이 같다면) 같은 문구, 명수, 동사를 사용한다.
6. 함수 인수
함수에서 인수의 갯수는 적을 수록 좋다.
코드를 읽는 사람 입장에서 인수는 함수 이름과 추상화 대상이 달라 개념을 어지럽히고,
현 시점에 중요하지 않은 세부사항을 알아야 코드가 읽히게 한다.
테스트 관점에서도 많은 인수는 조합가능한 테스트 케이스를 늘려 부담을 준다.
출력인수는 입력인수보다 이해를 어렵게 한다. 보통 인수를 넣어 반환값의 출력을 기대한다
ex) void addOne(VO vo){ vo.set(vo.get(A) + 1) }, scanf()
- 많이 쓰는 단항 형식
함수에 인수 1개를 넘기는 경우는 인수에 질문을 던지는 경우와 인수를 뭔가로 변환해 반환하는 경우이다.
이 경우는 코드를 읽는 사람이 당연하게 받아 들인다.
ex) boolean fileExists("MyFile"), InputStream fileOpen("MyFile")
출력 없이 입력 인수로 시스템 상태를 바꾸는 이벤트 함수도 유용한 단항 함수이다.
위 경우가 아니면 단항 함수도 가급적 피한다. - 플래그 인수함수로 부울 값을 넘기는 것은 함수가 여러가지를 처리한다는 것이다. 참이면 A 거짓이면 B를 처리하기 때문이다.
render(boolean isSuit) 는 renderForSuite()와 renderForSingleTest()라는 함수로 나뉘어야 한다. - 이항 함수인수가 2개일 때는 1개일 때 보다 이해하기 어렵다. writeField(name)이 writeField(outputStream, name)보다 쉽다.
후자는 첫 인수를 무시해야 한다는 사실을 인지하는데 시간이 걸리고 결국 문제를 일으킨다. 무시한 코드에는 오류가 숨어든다.
물론 직교 좌표계 처럼 이항함수가 적절한 경우도 있다. 여기에는 자연스러운 순서도 있다. (x, y)
하지만 assertEquals(expected, actual)처럼 순서가 자연적이지 않고 외워야 하는 경우가 있다. 이는 별로 좋지 않은 경우다. - 삼항 함수삼항 함수를 만들 때에는 매우 신중해야 한다.
assertEquals(message, expected, actual) 은 매번 볼 때마다 주춤했다가 message를 무시해야 한다는 사실이 떠오른다 - 인수 객체인수가 2-3개 필요하다면 독자적 클래스 선언을 고려해 봐야한다.
makeCircle(double x, double y, double radius); makeCircle(Point center, double radius);
변수를 묵어 넘기면 이름을 붙이게 되므로 개념을 표현해 줄 수 있다. - 인수 목록인수 개수가 가변적인 함수가 필요한 경우 가변 인수를 동등하게 List 하나로 취급한다.
ex) String.format("%s worked %.2f hours", name, hours) => public String format(String format, Object... args) - 동사와 키워드좋은 함수 이름은 함수의 의도나 인수의 순서를 제대로 표현해 준다. 그러기 위해선 함수와 인수가 동사/명사 쌍을 이루어야 한다.
write(name)도 좋지만 writeFile(name)으로 하게 된다면 name이 필드라는 뜻이 명확해 진다.
함수 이름에 인수의 키워드를 넣어도 좋다. assertExpectedEqualsActual(expected, actual)은 인수의 자연스런 순서가 생긴다.
7. 부수 효과를 일으키지 마라
부수효과는 예상치 못하게 클래스 변수를 수정하거나 넘어온 인수나 시스템 전역변수를 변경한다. 굉장히 안좋은 경우이다.
많을 경우 시간적 결합(temporal coupling)이나 순서 종속성(order dependency)를 초래한다.
목록 3-6이 일으키는 부수효과는 Session.initialize()이다. 암호 확인하는 일만 있는데 세션까지 초기화 하는 것이다.
이런 함수는 시간적 결합을 초래한다. 즉 위 세션을 초기화해도 괜찮은 경우에만 호출이 가능하다.
출력 인수
일반적으로 인수는 함수의 입력을 해석된다. appendFooter(s)는 s를 바닥글로 첨부할까 s에 바닥글을 추가할까?
public void appendFooter(StringBuffer report) 선언부를 보면 s가 출력 인수라는 것을 알 수 있지만 인지적으로 거슬린다.
객체지향 이전에는 어쩔 수 없이 출력인수를 사용하는 경우가 있었지만 요즘은 거의 없다. 출력 인수로 사용하라고 설계한 변수가this 이기 때문이다. 위 함수는 report.appendFooter() 처럼 호출되는 것이 좋다.
출력인수를 사용하지 말고 함수가 속한 객체 상태를 변경하자
8. 명령과 조회를 분리하라
함수는 객체 상태를 변경하거난 객체 정보를 반환하거나 둘 중 하나만 해야한다.
public boolean set(String attribute, String value); 같은 함수는 set이 동사인지 형용사인지 분간하기 어렵다.
if (set("username", "unclebob")) => if (attributeExists("username")){ setAttribute("username", "unclebob") }
9. 오류코드보다 예외를 사용하라
명령 함수에서 오류 코드를 반환하는 방식은 위의 말(명령/조회 분리)를 미묘하게 위반한다. if문에서 명령을 표현식으로 사용하기 때문이다.
오류 코드를 반환하면 코드가 중첩되고 오류코드를 바로 처리해 줘야 한다는 문제가 있다. 이는 try catch로 예외처리 한다.
- Try/Catch 블록 뽑아내기
try/catch는 코드 구조에 혼란을 일으키고, 정상과 오류 처리 로직이 섞이게 된다. 그러므로 별도의 함수로 뽑아내는 것이 좋다.
try와 catch에서는 각각 함수 하나씩만 호출한 후 호출되는 함수를 따로 선언해 준다.
이렇게 정상 동작과 오류 처리 동작을 분리하면 코드를 이해하고 수정하기 쉬워진다. - 오류처리도 한 가지 작업이다
오류를 처리하함수는 오류만 처리해야 한다. 즉 함수 이름에 try가 있다면 함수는 try문으로 시작해 catch/finally 문으로 끝나야 한다. - Error.java 의존성 자석
오류 코드를 반환한다는 것은 어디선서 오류 코드를 정의한다는 뜻이다.
publuic enum Error{OK, LOCKED,NO_SUCH, ... SERVICE_ERROR}
위는 의존성 자석이다. 다른 클래서에서 Error enum을 import하므로 Error가 변하면 이를 사용하는 클래스 전부 다시 컴파일하고 다시 배치 해야 한다. 변경에 비용이 많이 든다.
오류 코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생되 재컴파일/배치 없이 예외 클래스를 추가할 수 있다.
10. 반복하지 마라
코드의 중복은 다른 코드와 섞이며 모양새가 달라져 금방 들어 나지는 않지만 코드의 길이가 늘어나고 변경을 어렵게 힌다는 점에서 문제다.
중복은 소프트웨어에서 모든 악의 근원이다. RDB, 상속, AOP, COP 모두 중복을 제거하기 위해 고안 되었다.
11. 구조적 프로그래밍
다익스트라의 구조적 프로그래밍 원칙은 모든 함수와 블록에 입구와 출구가 한만 존재해야한다고 말한다. 즉return이 하나만 존재해야 하며 break, continue를 사용하면 한되고 goto는 절대로 안된다.
구조적 프로그래밍은 함수가 작다면 별 이익이 없지만 함수가 커질수록 상당한 이익을 제공한다.
그러므로 함수를 작게 만든다면 return, break, continue를 여러번 사용해도 괜찮다. 오히려 단일 입/출구 규칙보다 의도 표현이 쉽다.
12. 함수를 짜는법
소프트웨어를 짜는 것은 글짓기와 비슷하다. 먼저 생각을 기록한 후 다듬는다.
함수도 같다. 처음에는 길고 복잡하다. 여기에 서툰 코드를 테스트 하느 단위 테스트 케이스도 만든다.
그후 코드를 다듬고 중복을 제거한 뒤 단위 테스트를 한다.
결론
모든 시스템은 특정 분야의 시스템을 기술할 목적으로 설계한 도메인 특화 언어로 만들어 진다.
함수는 그 언어에서 동사며, 클래스는 명사이다. 프로그래밍 기술은 언어 설계의 기술이다.
마스터 프로그래머는 시스템을 구현해야할 것이 아닌 풀어갈 이야기로 여겨 프로그래밍 어어 라는 수단으로 풍부하게 풀어간다.
이 장은 함수를 잘 만드는 기교를 소개했다. 체계가 잡힌 함수도 좋지만 진짜 목표는 시스템이라는 언어를 풀어가는데 있다.