프로그래밍 초창기에는 시스템 루틴과 하위루틴으로 나눴다. 포트란과 PL/1 시절에는 시스템을 프로그램, 하위 프로그램, 함수로 나눴다. 지금은 함수만 살아 남았다. 어떤 프로그램이든 가장 기본적인 단위가 함수이다.
지금까지 경험을 바탕으로 , 오랜 시행착오를 바탕으로 나는 작은 함수가 좋다고 확신한다.
그렇다면 얼마나 짧아야 좋을까 ?
"1999년 나는 오레곤 주에 사는 켄트 벡을 방문했다. 우리는 같이 앉아 프로그램을 짰는데, 도중에 그가 Sparkle이라는 자그만 자바/스윙 프로그램을 보여왔다. 신데렐라 영화에서 요정 할머니가 휘두르는 마법의 지팡이처럼 화면에 반짝이를 뿌려주는 프로그램이었다. 마우스를 움직이면 커서를 따라가며 반짝이가 뿌려졌다. (중략)
켄트가 코드를 보여줬을때 나는 함수가 너무도 작아 깜짝 놀랐다. 그때까지 나는 장황하게 긴 스윙 프로그램 함수에 익숙했다. 그런데 Sparkle은 모든 함수가 2줄,3줄,4줄 정도였다. 각 함수가 너무도 명백했다. 그리고 각 함수가 이야기 하나를 표현했다. 각 함수가 너무도 멋지게 다음 무대를 준비했다."
또 중첩구조가 생길만큼 함수가 커져서는 안된다. 그러므로 함수에서 들여쓰기 수준은 1단이나 2단을 넘어서면 안된다.
"함수는 한가지를 해야한다. 그 한가지를 잘해야한다. 그 한가지만을 해야한다."
하지만 그 한가지는 무엇으로 채워야 할까?
지정된 함수 이름 아래에서 추상화 수준이 하나인 단계를 하면되는데 , 개인적으로 '추상화'를 한다는것은 어렵게 느껴진다. 그렇다면 추상화는 어떻게 해야할까?
컴퓨터 과학에서 추상화(abstraction)는 복잡한 자료, 모듈, 시스템 등으로부터 핵심적인 개념 또는 기능을 간추려 내는 것을 말한다.
그렇다면 다시 돌아가서, '설정페이지'와 '해체 페이지'를 '테스트 페이지'에 넣는다고 할때, 아래와 같이 세가지를 한다고 할 수 있다.
1. 페이지가 테스트 페이지인지 확인한다.
2. 그렇다면 설정페이지와 해체페이지를 넣는다.
3. 페이지를 HTML로 렌더링한다.
위의 간단한 3가지 단계를 하나의 문장으로 지정된 하나의 함수 아래, 이어 붙인것을 TO renderPage With Setups And Teradowns, 이하 'TO'문단으로 아래와 같이 기술할 수 있다.
"페이지가 테스트 페이지인지 확인한다, 그렇다면 설정페이지와 해체 페이지를 넣는다. 페이지를 HTML로 렌더링한다."
이들 아래 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한가지 작업만 한다. 어쨌거나 우리가 함수를 만드는 이유는 큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서가 아니던가?
함수가 단 '한가지'만 하는지 판단하는 방법이 더 있다. 단순히 다른 표현이 아니라 의미있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러작업을 하는 셈이다.
함수 내 섹션 을 나눠 하는 방법인데, 이방법으로 한 함수에 여러 작업을 할 수 있다. 한가지 작업만 하는 함수는 자연스럽게 섹션으로 나누기 어렵다.
함수가 확실히 '한가지' 작업만 하려면 함수내 모든 문장의 추상화 수준이 동일 해야한다. 한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다. 특정 표현이 근본 개념인지 아니면 세부사항인지 구분하기 어려운 탓이다. 하지만 문제는 , 근본개념과 세부사항을 뒤섞기 시작하면 깨어진 창문처럼 사람들이 함수에 세부사항을 점점 추가한다.
코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 한 함수 다음에는 추상화 수준이 낮은 한 단계 낮은 함수가 온다. 즉 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한번에 한단계씩 낮아진다. 이것을 내려가기 규칙이라 부른다. 달리말해 TO문단 읽듯 프로그램이 읽혀야 한다는 의미다.
하지만 추상화 수준이 하나인 함수를 구현하기란 쉽지 않다. 하지만 매우 중요한 규칙이다. 핵심은 짧으면서도 '한가지'만 하는 함수다. 어떻게 해야할까 ?
"코드를 읽으면서도 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다."
TO문단을 통해 서술적인 이름을 지었다. 좋은 이름이 주는 가치는 아무리 강조해도 지나치지않다. 이름이 길어도 괜찮다. 겁먹을 필요가 없다. 서서술적인 이름이 짧고 애매한 이름보다 좋다. 또, 길고 서술적인 이름이 길고 서술적인 주석보다 좋다. 함수 이름을 정할때는 여러 단어가 쉽게 읽히는 명명법을 사용한다. 그런다음 여러 단어를 사용해 함수기능을 잘 표현하는 이름을 선택한다. 이름을 정하느라 시간을 들여도 괜찮다. 이런저런 이름을 넣어 코드를 읽어 보면 더 좋다. 또 서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.
이름을 붙일때도 일관성이 있어야한다. 모듈 이름내에 함수 이름은 같은 문구, 명사 동사를 사용해야한다.
가장 이상적인 인수개수는 0개이다. 그다음 1개(단항), 다음은 2개(이항), 3개(삼항)은 가능한한 피하는 편이 좋다. 4개(다항)은 특별한 이유가 있더라도 사용하면 안된다.
사실 인수는 어렵다. 인수는 개념을 이해하기 어렵게 만든다. 그것이 예제에서 인수를 없앤이유다. 갖가지 인수 조합으로 함수를 검증하는 테스트 케이스를 작성한다고 상상해보자.! 인수가 유효한값으로 모든 조합을 구성해 테스트하기 상당히 부담스러워진다. 또 '출력 인수'는 입력한 인수보다 이해하기 더 어렵다. '출력 인수'는 독자가 허둥지둥 코드를 재차 확인하기 바쁘다.
최선의 입력은 인수가 없는 경우이며, 차선은 인수 입력이 1개인 경우이다.
단항 형식은 정말 자주 쓰인다. 함수 인수를 한개로 넘기는 가장 흔한 이유는 두가지이다. 첫번째는 인수에 질문을 던지는 경우이다. 두번째는 인수를 뭔가로 변환해 결과를 만드는 경우이다.
이 두가지 경우는 독자가 당연히 받아들이며, 함수이름을 지을때는 두 경우를 분명히 구분한다. 또한 언제나 일관적인 방식으로 두 형식을 이용한다.
그외 유용한 단항 함수 형식이 있는데 이것은 이벤트이다. 이벤트 함수는 입력인수만 있고, 출력인수는 없다. 프로그램은 함수 호출을 이벤트로 해석해 입력인수로 시스템 상태를 바꾼다. 하지만 이벤트 함수는 조심해서 사용해야 한다. 이벤트라는 사실이 코드에 명확히 드러나야 하며, 이름과 문맥을 주의해서 선택해야한다.
그리고, 지금까지 설명한 경우가 아니면 단항함수는 가급적 피해야한다. 변환 함수에서 출력인수를 사용하면 혼란을 줄 수 있기 때문이다. 입력인수를 그대로 돌려주는 함수일 지라도 변환함수 형식을 따르는 편이 좋다. 적어도 변환형태는 상태를 유지하기 때문이다.
플래그 인수는 추하다. 함수로 부울 값을 넘기는 관례는 정말로 끔찍하다. 함수가 한꺼번에 여러가지를 처리한다고 대놓고 공표하는 셈인것이다. 플래그가 '참'이면 이걸 하고 '거짓'이면 저걸 한다는 소리인 것이다.
인수가 2개인 함수는 인수가 1인 함수보다 이해하기 더 어렵다. 둘다 의미는 명백하지만 전자가 더 쉽게 읽히고 더 빨리 이해하기 쉽다. 둘다 의미는 명백하지만, 전자가 더 쉽게 읽히고 더 빨리 이해된다. 이항 함수는 잠시 주춤하며 첫 인수를 무시해야 한다는 사실을 깨닫는 시간이 필요하다. 어떤코드는 무시해선 안되며, 그 안에 오류가 숨어있을 수도 있기 때문이다.
이항함수가 적절한 상황도 있는데 다음과 같다.
Point p = new Point(0,0)이 좋은 예다. 직교 좌표계 점은 일반적으로 인수 2개를 취한다. 여기서 인수 2개는 한 값을 표현하는 '두 요소'이다.
하지만 이항함수에도 문제가 있는데, 요소에 자연적인 순서가 없을경우 그 순서를 인위적으로 기억해야한다.
이항함수가 무조건 나쁜것은 아니며 프로그램을 짜다보면 불가피한 경우도 생긴다. 하지만 그만큼 위험이 따른다는 사실을 이해하고 가능하면 단항함수로 바꾸도록 권장한다.
예를 들면, 메서드를 어느 클래스의 구성원으로 만들어 호출하는 방법과 한 메소드를 클래스의 메소드 구성원 변수로 만들어 인수로 넘기지 않는것이다. 혹은 새 클래스를 만들어 구성자에 메서드를 받고 궁극적으로 원하는 메서드를 구현한다.
인수가 3개인 함수는 인수개 2개인 함수보다 훨씬 더 이해하기 어렵다. 순서, 주춤, 무시로 야기되는 문제가 두배이상 늘어난다. 삼항함수를 만들때는 신중히 고려하라 권고한다.
인수가 만약 2~3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성을 짚어본다. 예를 들어 , 다음 두 함수를 살펴본다.
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
객체를 생성해 인수를 줄이는 방법이 눈속임이라 여겨질지 모르지만, 그렇지 않다. 위 예제에서 x와 y를 묶었듯이 변수를 묶어 넘기려면 이름을 붙여야 하므로 결국은 개념을 표현하게 된다.
때로는 인수 개수가 가변적인 함수도 필요하다.
String.format("%s worked %.2f" hours.", name, hours);
가변 인수 전부를 동등하게 String으로 취급해 버리면 List형 인수 하나로 취급할 수 있다. 이런 논리로 따지만 String.format
은 사실상 이항 함수이다.
가변 인수를 취하는 모든 함수에 같은 원리가 적용되며, 가변인수를 취하는 함수는 단항, 이항,삼항 함수로 취급할 수 있다. 하지만 이를 넘어서는 인수를 사용할 경우엔 문제가 있다.
void monad(Integer... args);
void dyad(String name, Integer... args);
void triad(String name, int count, Integer... args);
함수 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다. 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다.
write(name)
은 이름이 무엇이든 쓴다는 뜻이다. 거기에writeField(name)
은 이름이 '필드'라는 사실이 명확히 드러난다.
예제는 함수 이름에 키워드를 추가하는 형식이다. 즉 함수 이름에 인수이름을 넣는다.
부수효과는 거짓말 쟁이다. 함수에서 한가지를 하겠다고 약속하고선 남몰래 다른것도 하니까. 때로는 예상치 못하게 클래스 변수를 수정해버린다. 어느쪽이든 교활하고 해로운 거짓말이다.
많은 경우 '시간적 결합'이나 '순서 종속성'을 초래한다. 그 뜻은 특정 상황에서만 호출이 가능하다. 다시말해, 세션을 초기화해도 괜찮은 경우에만 호출이 가능하다. 자칫 잘못 호출하면 의도하지 않게 세션 정보가 날아간다. 시간적인 결합은 혼란을 일으킨다. 특히 부수효과로 숨겨진 경우에는 더더욱 혼란이 커진다. 만약 결합이 필요하다면 함수 이름에 분명히 명시해야 한다. 물론, 한가지 규칙을 위반하겠지만.. (예문 참고 , page 55 )
일반적으로 인수는 '함수 입력'으로 해석한다. 인수를 출력으로 사용하는 함수에 어색함을 느낄것이다. 객체지향 프로그래밍이 나오기 전에는 출력 인수가 불가피한 경우도 있었다. 하지만 객체 지향언어 에서는 출력인수를 사용할 필요가 거의 없다. 출력인수로 사용하라고 설계한 변수가 바로 this이기 때문이다. 일반적으로 출력인수는 피해야 맞으며, 함수에서 상태를 변경해야 한다면 함수가 속한 객체상태를 변경하는 방식을 택해야 한다.
if{set("username","unclebob")}..
//username 속성이 unclebob로 설정되어있는 코드인가?
//아니면 설정되어있는지 확인하는 코드인가?
함수를 구현한 개발자는 동사 ' set'을 의도했는데 , if문에 넣고보면 형용사로 느껴진다. set이라는 단어가 동사인지 형용사인지 모호하기 때문이다.
set이라는 함수이름을 좀 더 다르게 설정하는 방법도 있지만, 진짜 해결책은 명령과 조회를 분라하라
오류코드보다 예외를 사용하라는것은 무슨 뜻일까 ? 말 그대로 안될 오류코드를 사용하지말고 예외를 넣으라는 것일까 ? 코드를 읽어보니 if
로 elseif
나 else
를 많이 넣어두기보단, try/catch
로 될 경우와 안될 경우를 구분해놓은것 같다. 작동이 될 경우와 작동이 되지않을 경우를 나눠서 깔끔하게..
이 문단에서는 형용사/동사로 혼란을 일으키지않는 대신, 여러 단계로 중첩되는 코드를 피하라는 이야기와 같은 맥락인것 같다.
하지만, 이대로 놔두면 try/catch
를 사용해도 되겠네? 라고 생각 하기쉽겠다만 사실 이 블록은 원래 추하다. 정상동작과 오류처리를 뒤섞는다. 따라서,"try/catch
를 별도의 함수로 뽑아 내는 편이 좋다." 고 설명한다.
'오류처리'도 한가지 작업만 해야한다. 오류처리도 '한 가지'작업에 속한다. 그러므로 오류를 처리하는 함수는 오류만 처리해야 마땅하다. 오류코드를 반환한다는 이야기는 클래스든 열거형 변수든 어디선가 오류 코드를 정의한다는 뜻이다.
public enum Error{
OK,
INVALID,
NO_SUCH,
LOCKED,
OUT_OF_RESOURCES,
WATING_FOR_EVENT;
}
위와 같은 클래스는 의존성 자석이다. 클래스에서 Error enum을 import해사용해야 하므로, 즉, Error enum이 변환된다면 Error enum하는 클래스 전부를 컴파일 하고 다시 배치해야한다. 그래서 Error클래스 변경이 어려워진다. 재컴파일/재배치가 매우 번거롭기에 새 오류코드를 정의하고 싶지 않다. 그래서 새 오류코드를 추가하는 대신 기존 오류코드를 재 사용한다.
중복은 소프트웨어에서 모든 악의 근원이다. 중복을 없애도 모듈의 가독성이 크게 높아졌다. 객체지향 프로그래밍은 코드를 부모클래스로 몰아 중복을 없앴다. 구조적 프로그래밍, OOP, AOP 모두 어떤 면에서는 중복 제거 전략이다 .
하위 루틴을 발명한 이래 소프트웨어 개발에서 지금까지 일어난 혁신은 소스코드에서 중복을 제거하려는 지속적인 노력으로 보인다.
구조적 프로그래밍은 '에츠허르 데이크스트라'의 구조적 프로그래밍 원칙을 따른다. 데이크스트라는 모든 함수와 함수내 모든 블록에 입구와 출구 하나만 존재해야한다고 말했다. 즉 함수는 Return
문 하나여야 한다는 소리이다.
또 루프안에break
이나 continue
를 사용해서는 안되며 goto
는 절대로 안된다. 구조적 프래그래밍의 목표의 규율은 공감하지만 함수가 작다면 위 규칙은 별 이익을 제공하지 못한다. 그러므로 함수를 작게 만든다면 return, break,continue
를 여러차례 사용해도 좋다.
모든 시스템은 특정 응용 분야 시스템을 기술할 목적으로 프로그래머가 설계한 도메인 특화 언어로 만들어진다. 함수는 그 언어에서 동사 이며, 클래스는 명사이다. 이것은 훨씬더 오래된 진실이다. 프로그래밍기술은 언제나 언어 설계의 기술이다 . 예전도 그렇고 지금도 마찬가지다.
프로그래밍의 대가는 시스템을 구현할 프로그램이 아니라, 풀어갈 이야기로 여긴다.
소프트웨어를 짜는 행위는 여느 글짓기와 비슷하다. 논문이나 기사를 작성할때 먼저 생각을 기록한 후 읽기를 다듬는다. 함수도 마찬가지다.
프로그래밍이란 , 하나의 예술이라는 것을 새삼 느끼게 되었다. 그리고 우리는 컴퓨터에 명령을 내리지만, 코드를 읽는것은 '사람'이다. 가독성좋고 읽기 쉬운 코드는 '예술' 이라 할만하다.
그런 바탕을 위해 위의 인용구와 같이 개발에 임해야 할것 같다. 그리고 그 코드가 제대로 동작되는지 매번 작은 코드들 역시 "빠짐없는 테스트"단위 테스트 케이스를 만들어 테스트를 한다. 그 다음 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거하고, 메서드를 줄이고 순서를 바꾸는 등 정말 글쓰기의 '퇴고'와 일맥상통한 부분인것 같다. 앞으로 컴퓨터 개발을 '글쓰는 일'이라고 생각해도 좋으려나..?
명령과 조회, 오류코드, 반복하지말라 이 내용은 따로 팁으로 묶어 분류해 놓았다. 일단 내가 지금 이해할수있는부분과 이해할수 없는 부분이존재하기에 그렇게 설정하였다.