영어로 제목을 쓰려던 것은 아니었으나, “Catch me if you can”을 의식하다보니, 이런 제목이 되었습니다. 사실 영어는 종종 의도를 드러내는데 유리하기도 하구요. 🙂
프로그래밍 언어에서 return문은 아주 중요한 역할을 합니다. 프로그래밍을 수학적 모델로 표현하는데 있어서, 함수라는 개념은 핵심적인 역할을 하고 있으며, 함수의 출력을 받는다는 면에서 매우 중요하지요. 하지만, return문은 독입니다. 왜냐구요? 중요한데 왜 독이냐구요? 이야기를 한번 풀어보도록 하겠습니다.
소프트웨어는 개발 기간이 길어지면, 복잡해지기 마련입니다. 처음 개발할때의 요구사항이 그대로 유지되는 경우는 없다고 보아도 무방하며, 늘어난 요구사항은 처음에 짜둔 아름다운 흐름을 망가뜨리기 시작합니다. 소프트웨어의 구조가 무너집니다. 코드는 점점 스파게티가 되어갈 것이고, 문제라도 생겨서 디버깅이라도 하려고 하면 지옥이 따로 없죠.
이런 코드들을 고통을 감내하며 Sequence Diagram을 그려보면, 왜 복잡한지 드러나는 경우가 많습니다. 클래스별로 호출이 들어갔다가 나오고, 그 결과에 따라서 새로운 분기가 발생하는 사태가 벌어지죠. 그리고, 이 분기의 수가 많아지면, Sequence Diagram은 읽기 힘든 선들의 난잡한 리좀이 되어버립니다. 들뢰즈가 극찬했던 리좀이지만, 각 클래스별의 자유로운 결합과 소통이 아닌 서로를 점점 더 구속하는 꼬인 실이 되어버리죠. 그냥 이정도면 모르겠는데, 반환값이 여러개여야 하는 경우에는 return문을 쓰지 못하고, reference나 pointer를 이용한 억지스러운 반환을 하는 경우까지 발생합니다. 점점 더 복잡해져만 갑니다.
갑작스런 이야기 전환이지만, 전투기에 탑승해서 미사일을 쏜다고 생각해봅시다. 미사일은 날아가고 있고, 그 미사일이 격추 결과를 반환하고, 그 결과에 따라 제어를 해야한다면.. 네, 생각만해도 복잡합니다. 반면에, 그냥 미사일을 쏘고 뭐가 어찌되든 알아서 해결된다고 하면, 편안하죠. fire-and-forget입니다. call-site에선 신경쓰지 않는겁니다. callee쪽에서 알아서 하겠죠 뭐.
함수를 호출하는데 있어서 그 결과값에 신경써야 한다는 것은 상당한 위험성을 내포하고 있습니다. 단순한 수학 연산이나, 문자열 검색같은 피할수 없는 반환값이라면 모르겠지만, 소프트웨어가 구현하는 Business Logic이라면, 그 결과값에 따른 논리구조의 분기는 피할 수 없습니다. 결과값이 존재한다는건, 그 결과값에 따라 무언가 다르게 하려고 했기 때문일테니까요. 그리고, 그 분기는 요구사항이 늘어나면 늘어날수록 두고두고 당신을 괴롭히겠죠.
Sequence Diagram을 생각해봅시다. return문이 없다면, 함수호출을 의미하는 화살표만 있으면 됩니다. 반환값에 따른 분기도 없을테고, 분기가 일어나는 시점은 바로 그 분기조건을 확인하는 그 순간입니다. 더욱 간단해지죠. 그래봐야 Sequence Diagram이 복잡해지는건 매한가지 아니냐? 라고 생각할 수 있겠지만, 원래 복잡한 논리구조를 표현하는데 복잡한건 당연하겠죠. 다만 그 복잡함을 따라가는데 얼마나 더 쉬운가를 이야기하는 겁니다.
이렇게 짜다 보면, 어떤 객체의 상태변화에 대해서 반응하는 코드를 구현하기가 어려워집니다. 이때 사용하는 기법이 Callback이고, Callback을 사랑하는 이유죠. Callback은 대부분의 경우, 직관적이지 못하다는 이유로 배척당하곤 하는데, 간단한 소프트웨어의 경우엔 그렇습니다. 하지만, 복잡해지면 복잡해질수록 그 진가를 발휘하곤 하죠. 특히, 복잡해지면서 유사한 상태변화가 늘어날 경우에 더더욱 심해집니다. Callback기법을 적절히 사용하고 있다면, 해당 상태변화가 감지되었을때, 그 감지에 대한 분기를 추가하는 것이 아니라, 미리 지정된 Callback을 호출하기만 하면 되니까요. 이렇게 하면, 소프트웨어의 실행흐름을 Input Layer -> Business Layer -> Reaction Layer로 나누게 됩니다. 그리고, Reaction Layer를 만들때 Callback을 활용하게 되죠.
이는 객체지향 패러다임의 원칙중 하나인 Encapsulation과도 관계가 있습니다. Encapsulation이 필요한 데이터만 노출해서 소프트웨어를 간단하게 만든다면, 이번에 제시하는 이 원칙은 대부분의 분기를 감추는 것으로 소프트웨어를 간단하게 만든다고 볼 수 있지요. 또한, Callstack을 보는 것 만으로 분기가 어떻게 일어났는지 확인할 수 있다는 장점도 있습니다. 재현가능한 버그라면 모르겠지만, 소프트웨어가 비정상종료 되면서 남긴 제한적인 Callstack정보로 디버깅을 하는 경우에는 더더욱 유리해집니다.
이게 뭔소리냐. 하실 수 있겠지만, 제목에 if you can이 붙은건 가급적이면 피하라는 이야기입니다. 반환값을 가진 return문은 결국 호출측에서의 분기를 유발하게 되고, 그 분기는 소프트웨어를 복잡하게 만들 가능성을 내포하고 있으니까요.