7-3-라.설계 원칙

함수를 작성하는 문법과 호출하는 방법, 인수를 받아들이고 리턴하는 방법을 익히는 것은 그다지 어렵지 않다. 그러나 함수를 정말로 함수답게 잘 나누고 디자인하는 것은 무척 어렵고 단기간에 체득되지 않는다. 함수는 프로그램을 구성하는 단위로서 잘 나누어 놓으면 프로그램의 구조가 탄탄해지고 확장하기도 쉽고 재사용성도 좋아진다. 잘 짜여진 프로그램을 분석해 보면 함수의 분할 구조가 감탄스러울 정도로 잘 되어 있음을 볼 수 있고 그런 함수를 만드는 능력이 부러워지기까지 한다.

그러나 함수를 잘못 디자인하면 코드는 더 커지고 프로그램은 더 느려지며 조금이라도 수정하려면 어디를 건드려야 할지 판단하기 힘든 나쁜 구조가 만들어진다. 함수에 의해 코드는 꼬이기만 하고 엉망이 된 코드 사이로 버그가 창궐할 수 있는 환경만 만들어지니 아예 함수를 만들지 않느니만도 못한 상태가 되기도 한다.

프로그래밍에 처음 입문한 사람들에게 함수 디자인이라는 주제는 아주 어렵고 힘든 고비이다. 책을 읽어서 비법을 얻는 것은 불가능하고 잘 하는 사람에게 개인 지도를 받아도 어렵고 혼자서 연습해 보기는 더욱 더 어렵다. 함수 디자인은 오로지 많은 분석과 실습만으로 얻어지는 경험이다. 그래서 꾸준한 연습만이 해결책이다. 다음은 함수를 잘 만드는 기본적인 지침들이다.

 

 함수의 이름을 최대한 설명적으로 작성하여 이름만으로 무엇을 하는 함수인지, 이왕이면 어떻게 쓰는 것인지도 알 수 있도록 한다. 마치 함수의 이름이 주석인 것처럼 해야 한다. 함수는 이름으로 호출되므로 좋은 이름을 붙여 두면 함수를 관리하는 사람과 쓰는 사람 모두가 편해진다. 아주 간단한 규칙인 것 같지만 함수를 설계하는 첫 번째 원칙이 될 정도로 좋은 이름을 붙이는 것은 중요하다. 특히 팀 작업을 하거나 오랫동안 관리해야 할 코드라면 더욱 더 정성스럽게 이름을 붙여야 한다.

함수명은 보통 동사와 목적어 그리고 약간의 수식어로 구성된다. GetScore, DrawScreen, TestGameEnd 등은 이름만으로 어떤 동작을 하는지 쉽게 알 수 있으므로 좋은 함수명이다. 동작이 좀 더 구체적이라면 GetHighestScore, GetAverageScore 등의 수식어를 붙이는 것도 좋다. 이런 이름은 무엇을 어떻게 하는지를 분명히 표현한다.

반면 Score, Draw, Test 따위는 점수나 그리기와 상관이 있는 동작을 하는 것 같아 보이기는 하지만 구체적으로 무엇을 어떻게 하는지를 표현하지 못하므로 좋지 않다. 함수를 만든 사람은 당장은 이 함수들을 이해할 수 있다 하더라도 조금만 시간이 지나면 함수의 본체를 다 읽어 봐야 무엇을 하는 함수인지 알 수 있으므로 코드를 유지 및 확장하기 어려워진다. 함수에 좋은 이름을 붙이는 것은 어려운 기술이 아니라 조금의 관심만 기울이면 누구나 할 수 있는 기술이며 이름을 구성하는 적절한 영어 단어만 잘 선정하여 조립하면 된다.

 두 번 이상 중복된 코드는 반드시 함수로 분리한다. 똑같은 코드를 중복된 채로 내버려 두면 프로그램의 크기가 쓸데없이 커진다. 10줄짜리 코드를 10번 반복한다면 나머지 90줄은 불필요하게 용량만 차지하는 것이다. 용량의 문제보다 더 심각한 것은 코드를 유지, 확장하기가 아주 곤란해진다는 점이다. 요구가 바뀌어 해당 동작을 수정해야 한다고 해 보자. 이 동작을 함수로 분리해 두었으면 함수만 고치면 되지만 중복되어 있다면 일일이 찾아가서 고쳐야 한다. 실수로 한 곳을 고치지 않으면 이것이 바로 버그의 원흉이 된다.

중복되는 회수에 상관없이 앞에서 이미 만들었던 코드와 비슷한 코드를 또 작성해야 한다면 일단 그 부분을 함수로 만들고 기존 코드를 함수 호출로 수정해야 한다. 즉 다음과 같이 구조를 만든다.

아니! 고작 두 번 중복되었을 뿐인데 이런 것들도 함수로 분리해야 한단 말인가 하는 생각이 들지도 모르겠다. 그 대답은 당연히 그렇다이다. 한 번 중복된 코드는 조만간 다시 필요해질 가능성이 아주 높다. 뿐만 아니라 십중팔구 그 코드는 잠시 후 확장되어야 한다. 그래서 중복이 발견되는 즉시, 그것이 단 두 군데 뿐이더라도 무조건 함수로 분리하는 습관을 가져야 한다. 네 번, 다섯 번 중복될 때 분리하겠다고 생각한다면 이미 프로그램은 엉망이 되어 가고 있는 것이다.

 반복되지 않더라도 한 단위로 볼 수 있는 작업은 함수로 만든다. 설사 이 함수를 딱 한 번만 호출하고 다른 곳에서 호출할 확률이 아주 희박하더라도 이렇게 하는 것이 좋다. 예를 들어 어떤 구조체나 배열을 초기화하는 코드 덩어리는 Init~ 라는 이름으로 분리하고 화면을 출력하는 코드 덩어리는 Draw~ 따위의 이름을 주어 분리한다.

함 함수의 소스가 아주 길어져서 수백줄이 되면 그 많은 코드들의 어떤 부분이 어떤 작업을 하는지 얼른 파악되지 않는다. 게다가 다른 일을 하는 코드들이 한 곳에 섞여 있으면 필시 꼬이게 마련이며 이런 복잡한 코드는 대체로 메인 코드인 경우가 많다. 이 코드들의 그룹을 나누어 함수로 분리해 두면 메인 코드는 이 함수들을 조립하는 수준으로 간단해진다.

이렇게 분리되면 메인 코드를 읽기 쉬워지고 이미 완성된 코드들은 더 이상 신경쓰지 않아도 되는 이점이 있다. 또한 이 함수들이 현재 프로젝트에서는 반복되지 않더라도 다른 프로젝트에서는 재사용되기 쉽다.

 함수는 한 번에 하나의 작업만 해야 한다. 함수는 프로그램을 구성하는 부품이며 부품이란 전체를 구성하는 원자적인 단위이다. 물론 한 함수가 두 가지 일을 동시에 수행할 수도 있고 그렇게 하는 것이 더 효율적일 때도 있다. 그러나 이 함수를 나누어 더 작은 부품을 만들어 놓으면 두 일을 각각 따로 실행해야 할 필요가 있을 때 작은 부품을 활용할 수 있다. 만약 꼭 여러 가지 일을 한꺼번에 해야 하는 함수가 필요하다면 각각의 함수를 만든 후 이 함수들을 호출하는 함수를 하나 더 만들면 된다.

이렇게 되면 이 함수를 호출하는 기존의 코드는 영향을 받지 않으면서 새로운 작은 단위의 작업을 호출할 수도 있게 된다.

 입력과 출력이 직관적이고 명확해야 한다. 인수는 함수에게 주어지는 작업거리인데 함수가 하는 일에 꼭 필요한 정보만 최소한의 인수로 받아들여야 한다. 나머지 인수로부터 알 수 있는 값이나 연산에 사용하지 않는 불필요한 정보는 전달될 필요가 없다. 예를 들어 화면의 특정 위치에 메시지를 출력하는 OutMessage 함수를 작성한다고 해 보자.

 

void OutMessage(int x, int y, char *str, int len)

 

이 함수에서 메시지의 길이 len은 불필요한 인수이다. 세 번째 인수 str이 널 종료 문자열이라면 str로부터 길이를 계산할 수 있다. 물론 메시지의 일부만을 출력하는 기능이 있다면 이럴 때는 len이 필요할 것이다. 함수의 작업 결과는 가급적이면 리턴값으로 보고해야 하는데 설사 그 값을 호출원에서 사용하지 않는다 하더라도 일단 보고할 내용이 있다면 보고하는 것이 좋다.

 함수는 자체적으로 에러 처리를 해야 한다. 함수는 독립된 작업을 하며 재사용 가능한 부품이므로 그 자체로서 완벽하게 동작할 수 있어야 한다. 현재 작성중인 프로젝트에서 잘 실행된다고 해서 이 함수가 안전하고 완벽하게 동작한다는 것을 보장할 수는 없다. 특히 입력된 인수의 유효성을 잘 점검해야 한다. 포인터의 경우 NULL이 전달될 수도 있고 정수가 터무니없이 크다거나 음수가 될 수 없는 값에 대해 음수가 전달되는 경우에도 에러 처리를 해야 한다. 그래야 어떤 프로젝트로 가져 가든 별도의 수정없이 재사용 가능한 부품이 된다.

 

여기서 논한 함수 설계에 대한 지침은 어디까지나 일반적인 참고사항일 뿐이다. 특수한 실무 환경에서는 이 지침과는 다르게 함수를 만들어야 하는 불가피한 경우도 존재한다. 이 지침이 함수 설계를 잘 하고 싶은 사람들에게 아주 중요한 내용인 것은 사실이지만 이 지침들을 몽땅 외운다고 해서 함수 설계 경험이 금방 늘어나는 것은 아니다. 마치 "여성의 마음을 사로잡는 100가지 비법"을 달달 외운다고 해서 사교계의 달인이 될 수 없는 것과 같다. 일단 이 권고안을 머리속에 새겨 두고 끊임없이 분석, 연습해 봐야 한다.