메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

리팩토링(2) - 복잡성에 대한 두려움

한빛미디어

|

2005-07-01

|

by HANBIT

12,496

저자: 임백준
출처: 임백준의 소프트웨어 산책(2005, 한빛미디어) 중 제3장 리팩토링



* 리팩토링(1) - 과거와 대결하는 프로그래머의 무기

전 직장인 루슨트 테크놀로지스에서 5년 간 함께 일했던 영국의 프로그래머들은, 돌이켜 생각해보건대, ‘객체’라는 중장비로 온 몸에 철갑을 두른 용맹한 기사(Knight)들이었다. ‘객체’를 바라보는 그들의 시선은 나무랄 데 없이 깊고 중후했다. 잠시도 공부를 게을리 하지 않는 그들은 디자인 패턴, 리팩토링, UML과 같은 ‘기본적인’ 초식에 정통한 것은 물론이고, ‘멀티쓰레딩’이나 시스템 성능처럼 자칫 발을 헛디디면 내상을 입기 쉬운 심오한 분야에서도 주저함이 없었다.

필자가 그들과 함께 개발한 시스템은 웹(web)에 기초한 네트워크 관리 프로그램이었는데, 그들이 담당했던 부분은 주로 웹서버와 데이터베이스 서버 사이에 존재하는 비즈니스 로직(business logic) 계층을 개발하는 일이었다. 그와 같은 계층이 담당해야 하는 역할은 다양하지만 가장 대표적인 것으로는 그래픽 사용자 인터페이스(GUI) 화면과 데이터베이스 사이에서 오고 가는 데이터를 상황에 따라 적절하게 주무르고 변환시키는 것이었다. 시스템의 설계 내용은 루슨트의 지적재산이므로 자세하게 밝히기 곤란하지만, 어쨌거나 그들이 최종적으로 개발해서 들고 온 ‘컴포넌트(component)’에 대한 느낌을 한마디로 간추리자면 그것은 ‘객체의 소나기’였다.

대서양이라는 공간과 6시간이라는 시간적 차이를 사이에 두고 일하는 미국 캠퍼스의 프로그래머들은 그 컴포넌트가 제공하는 API만 이용하면 되기 때문에 컴포넌트의 내부는 알 필요가 없는 ‘블랙박스(black box)’처럼 취급되었다. 하지만 시스템 설계자의 위치에 있던 필자는 중요한 컴포넌트의 내부 구성을 알 필요가 있었기 때문에 공부를 하지 않을 수 없었다. 처음에는 컴포넌트 내부에 정교하고 치밀하게 구성되어 있는 객체의 ‘오케스트라’ 향연에 감탄했다. 어느 한 객체도 허공에 떠 있지 않고 상속 구조에 편입되어 있어서 시스템 내부에 굳건히 뿌리를 내리고 있었다. 객체의 연관성은 치밀하게 의도되어 어느 한 객체의 동작이 수많은 다른 객체의 상태를 변화시키면서 중후하고 풍부한 음향을 뿜어내었다.

그렇지만 여름밤의 소나기처럼 지루하게 쏟아지는 객체의 ‘과잉’에 질리는 느낌을 받게 된 것은 그로부터 얼마 지나지 않아서였다. 예를 들어서, 데이터베이스 안에 영국 런던을 기준으로 하는 GMT(Greenwich Mean Time) 형식의 날짜가 저장되어 있다고 하자. 그 날짜가 사용자 화면에 나타날 때는 사용자가 영국에 있는지, 미국에 있는지, 혹은 중국이나 호주에 있는지 여부에 따라서 몇 시간을 더하거나 뺀 채 나타나야 한다. (예를 들어서 GMT 시간이 0이라고 했을 때, 뉴욕의 시간은 GMT에서 5를 뺀 -5이고, 서울은 GMT에 9를 더한 9가 된다. 따라서 뉴욕과 서울의 시간은 14시간 차이가 난다.) 시스템의 설계에 따라서 차이가 있긴 하지만 이와 같은 변환은 대개 웹서버 계층이나 비즈니스 로직을 담당하는 계층에서 일어나는 것이 보통이다. 이 시스템의 경우에는 그와 같은 변환이 영국 프로그래머들이 개발한 컴포넌트 안에서 일어났다.

아무리 철저하고 부지런한 프로그래머라고 해도 버그가 없는 소프트웨어를 작성하는 것은 불가능하다. 영국 프로그래머들이 작성한 컴포넌트는 대단히 세심하게 작성된 소프트웨어였지만 촘촘한 객체의 그물망을 뚫고 고개를 내미는 버그가 없을 수는 없었다. 소프트웨어가 출시되고 나서 얼마 후에 날짜의 형식이 제대로 변환되지 않은 채 화면에 나타나는 경우가 발생했다. 이와 같은 버그는 원인을 추측하는 것이 어렵지 않기 때문에 비교적 잡기 쉬운 편에 속한다. 하지만 영국 친구들의 컴포넌트 내부에서는 이렇게 단순한 버그조차 수정하기가 어려운 버그로 판명되었다. 버그를 잡기 위해서는 복잡하게 얽히고설킨 객체의 상속(inheritance)과 위임(delegation) 관계의 폭포를 한걸음씩 거슬러 올라가야 했기 때문에 생각보다 많은 시간과 노력이 들 수밖에 없었다.

이렇게 문제의 성격과 원인이 분명하게 드러나는 버그는 그나마 다행이다. 예를 들어서 문제의 원인이 ‘블랙박스’ 내부에서 복잡하게 얽혀있는 멀티쓰레드와 관련된 경우는 최악이다. 특정한 데이터가 갱신(update)되었을 때, 사용자가 특정한 행동을 취했을 때, 컴포넌트 내부에서 실행 중인 특정 쓰레드가 기대하는 것과 다르게 반응하여 버그를 발생시켰을 때는 원인을 간단하게 파악할 수 없기 때문에 버그를 잡는 것이 힘들어진다. 이런 경우에는 디버깅(debugging)하는 과정이 마치 촛불을 들고 밤길을 걷는 것처럼 위태롭고 고통스러운 가시밭길이 된다. 조금만 바람이 불면 촛불이 꺼지고 사방이 깜깜해져서 한 치 앞을 내다볼 수 없게 된다.

여러 객체의 행동이 독립적으로 규정된 것이 아니라 다른 객체들의 상태와 조건에 따라 변하도록 설계되어 있을 때는 문제가 한층 더 복잡해진다. 이런 경우에는 의심스러운 객체의 소스코드를 들여다보는 것만으로는 원인을 파악할 수 없다. 그 객체의 행동에 영향을 미치는 다른 모든 객체들의 현재 상태와 조건을 알아야 하고, 각각의 객체에게 영향을 미치는 또 다른 모든 객체들의 현재 상태와 조건을 알아야 하는 경우가 발생하기 때문이다. 마치 짓궂은 ‘행운의 편지’가 기하급수적으로 새끼를 치면서 불어나는 것처럼, 복잡한 객체의 구조는 이렇게 디버깅을 기하급수적으로 어렵게 만드는 경향을 갖는다.

문제는 디버깅만이 아니다. 모든 소프트웨어 시스템은 시간이 지남에 따라서 성장하도록 되어있다. 사람이 성장하면서 세포가 재생산되고, 나무가 성장하면서 나이테가 생기듯이, 소프트웨어 시스템은 성장하면서 기존의 설계 구조가 변경되고 코드가 재생산된다. 사용자의 요구조건이 추가되거나 변경되면서 현재 설계의 타당성이 재검토되는 것은 이상한 일이 아니다. 이와 같이 기존의 설계가 재검토될 때 애초에 너무나 ‘완벽하게’ 설계된 나머지 스스로 완성되어 확장성을 갖추지 않은 시스템은 프로그래머를 궁지로 몰아넣는다. 시스템의 어느 한 부분도 수정이나 확장을 쉽게 허락하지 않기 때문에 단순한 기능을 더하는 것이 새로운 컴포넌트를 개발하는 정도의 수고를 요구하기 때문이다.

영국 친구들이 개발한 컴포넌트는 말하자면 지나치게 완벽한 시스템의 전형이었다. 시스템이 지나치게 완벽해진 이유 중의 하나는 그 친구들이 주어진 요구조건을 뛰어넘는 보편적인 시스템을 만들려고 ‘과욕’을 부린 것이었다. 그들은 컴포넌트가 네트워크 관리가 아닌 예를 들어서 금융이나 기업관리 같은 다른 시스템에서도 사용될 수 있도록 설계하기를 희망했다. 그와 같은 ‘일반화’에 대한 열정은 객체의 기능과 구조가 주어진 요구조건에 비추어 보았을 때 필요 이상으로 복잡해지도록 강요했다. 소프트웨어를 범용의 목적으로 설계하는 것이 특정한 분야의 목적에 국한시키는 것보다 좋지 않은가, 하고 반문할 사람도 있겠지만, 그건 그렇지가 않다.

소프트웨어의 설계에서 말하는 일반성은 대개 시스템의 확장성을 의미하는 경우가 많다. 다시 말해서 소프트웨어의 설계는 나중에 그것이 좀 더 풍부하고 보편적인 기능을 갖춘 시스템으로 확장될 수 있도록 열려 있어야 한다는 의미이다. 또한 코딩, 즉 구체적인 구현(implementation) 과정에서 말하는 일반성은 한 줄 한 줄의 코드가 당장의 목적을 달성하기 위해서 if-else 구문과 같은 편법을 동원하는 식이 아니라 다른 알고리즘에서도 사용될 수 있는 보편적인 방식으로 구현되어야 함을 뜻한다. 이와 같은 일반성은 프로그래머가 당연히 염두에 두어야 하는 좋은 원칙이다. 하지만 여기에서 이야기 하고 있는 영국 친구들의 일반화는 그러한 일반성을 뜻하는 것이 아니다. 그것은 주어진 요구사항을 자기 마음대로 뛰어넘는 ‘과욕의 일반화’였다.

컴포넌트의 설계 과정에는 필자도 일부 참여했는데, 당시 우리가 소프트웨어를 설계한 과정은 전형적인 ‘폭포(waterfall)’ 방식에 근거하고 있었다. 소프트웨어 공학(software engineering)에서 이야기 하는 가장 고전적인 소프트웨어 개발 방식 모형인 폭포 방식은 맥락에 따라서 더 상세하게 구분되기도 하지만 보통 ‘요구사항 분석’, ‘설계’, ‘코딩’, ‘테스트’라는 네 가지 커다란 단계로 이루어져 있다. 영국 프로그래머들은 초기 설계의 과정에 많은 시간을 투입하고, 설계한 내용을 문서로 남긴 다음, 다른 설계자나 관리자와 함께 문서를 검토하는데 많은 시간을 할애했다.

설계가 거의 완성된 다음에 이루어지는 코딩은 사실상 추상적인 설계에 살을 붙이고 옷을 입히는 일종의 확인 과정에 불과했다. 수많은 객체 사이에 존재하는 상속(inheritance), 다형성(polymorphism), 캡슐화(encapsulation), 그리고 추상화(abstraction)라는 속옷을 모두 갖춰 입은 다음에 메쏘드 내부의 구현이라는 겉옷을 찾아 입는 일은 어려운 일이 아니었다. 잘 알려져 있다시피 이러한 폭포 모형 방식의 장점은 철저한 설계 과정을 통해서 구현 과정의 오류를 최소화 할 수 있다는 점이다. 하지만 그 장점이 단점이 되는 경우도 있다는 데에 폭포 모형 방식의 고민이 있다. 이러한 방식에서는 일단 겉옷을 입기 시작했으면 도중에 내복을 갈아입거나 손질하기가 무척 어렵기 때문이다.

우려했던 상황이 발생한 것은 코딩이 시작되고 얼마 지나지 않아서였다. 컴포넌트의 윤곽이 어느 정도 드러났을 때 GUI, 웹서버, 비즈니스 계층 컴포넌트, DAO(Data Access Object) 계층, CORBA, 그리고 데이터베이스를 통합하는 테스트가 진행되었다. 이와 같은 통합 테스트의 과정에서 영국 친구들이 설계한 컴포넌트에서 심각한 문제가 발견되었다. GUI와 서버 사이의 데이터는 모두 그 컴포넌트에 의해서 특정한 포맷으로 변형된 다음 전달되는데 그 포맷이 (배열의 배열 같은) 리스트의 리스트(list of list) 구조를 지원하지 않는다는 점이 발견된 것이다. 예를 들어서 GUI 화면에 트리(tree) 구조가 있다면 리스트의 리스트로 이루어진 데이터가 필요할 것이다. 하지만 컴포넌트는 깊이(depth)가 2 이상이 되는 트리 구조를 지원하지 않았다.

이것은 심각한 문제였기 때문에 우리는 입던 겉옷을 잠깐 벗고 구멍이 뚫린 내복을 수선하지 않을 수 없었다. 하지만 이미 복잡하게 얽혀 있는 객체의 구조는 단순한 ‘땜질’을 허용하지 않았다. 내복에 뚫린 구멍을 메우기 위해서는 양말이 아니라 팬티까지 전부 벗어 던져야 할 판이었다. 컴포넌트의 커널(kernel)에 해당하는 부분을 다시 설계하지 않는 이상 구멍은 메워질 수 없었다. 이미 코딩이 진행되고 있는 단계에서 커널을 다시 설계한다는 것은 프로젝트의 ‘실패’를 의미했기 때문에 그것은 선택할 수 있는 조건이 아니었다. 이러한 상황의 원인을 제공한 것이 고전적인 폭포 모형이라고 말 할 수는 없다. 하지만 소프트웨어 개발 과정에서 얼마든지 일어날 수 있는 이러한 상황에서 과거로 되돌아갈 출구를 제공하지 않는 방법론의 비현실성은 충분히 비판의 대상이 될 수 있다.

필자는 함께 일했던 영국 프로그래머들의 중후한 내공과 지칠 줄 모르는 근면함을 존경하지만, ‘복잡성’을 두려워할 줄 모르는 그들의 지나친 용맹에 대해서는 질렸다. 안타깝게도 그들은 자신의 ‘명석한’ 두뇌를 신뢰하는 것과 ‘복잡성’을 두려워할 줄 아는 것이 별개의 문제라는 점을 알지 못하는 것처럼 보였다. 프로그래머라면 한 번쯤 반드시 읽어야 하는 고전에 속하는 "The Practice of Programming"을 쓴 브라이언 커니건(Brian W. Kernighan)과 롭 파이크(Rob Pike)가 책 안에서 끊임없이 강조한 것을 하나 기억해 두는 것은 실전 프로그래밍에 도움이 될 것이다. 그것은 바로 단순성(simplicity)이다. 단순성은 프로그래머가 가슴에 품어야 할 덕목 중에서 으뜸이 아닐 수 없다.
TAG :
댓글 입력
자료실

최근 본 상품0