소프트웨어는 soft 해야 제 맛이다

Posted at 2007. 1. 27. 01:17 // in S/W개발 // by 김윤수


S/W 개발하면서 hard coding 하면 안된다는 말 많이 들어 보셨죠? hard coding 하면 안된다고 하니 뭔가 나쁜 것 같긴 한데 도대체 어떤 게 hard coding 인지 구분하기도 어렵고, 어느 정도까지를 hard coding으로 봐야하는 건지 애매 모호하기 짝이 없습니다.

정말, 도대체, 어떤 것이 hard coding 인 걸까요 ?

우선 그 의미를 알아보기 전에 hard 라는 말 뜻부터 되새겨 볼 필요가 있을 것 같습니다. 여기서 hard 라는 말은 어렵다라는 것보다는 딱딱하다라는 뜻일 거구요. 소위 S/W의 품질 요소 중 하나인 flexibility 의 반대말이라고 생각해도 무방할 것 같습니다.

Flexibility 라는 S/W 품질 요소에 대해 IEEE는 다음과 같이 정의하고 있다고 하네요.

좀 황당한 정의네요. 원래 Spec 과 다른 환경에서도 쓸 수 있도록 쉽게 고칠 수 있어야 한다는 뜻이잖습니까 ? 근데 도대체 이런 S/W를 어떻게 개발할 수 있다는 겁니까? 도대체 아무런 Spec 도 정의되어 있지 않다면 S/W가 무슨 의미가 있겠습니까? S/W를 개발하려면 뭔가는 정해져야 하는 것 아니겠습니까?

자자~ 그렇다고 너무 흥분하지 마시구요. 아마 위에 정의는 S/W 개발을 제대로 안해 본 분이 정의했을 겁니다. 어찌 됐든 S/W는 뭔가 정해져 있어야-꼭 S/W뿐만은 아니죠-개발할 수 있는 게 사실입니다. 그런데 또 그 정해져 있던 게 시간이 지나면서 계속해서 바뀌어 나간다는 게 문제이죠. 예를 들면, 화면 크기를 640x480 으로 가정하고 UI 를 구성했는데, 얼마 안 있어서 800x600 으로 늘어 난다던지, 제일 처음에는 CD만 읽을 줄 알면 됐었는데, DVD도 읽을 줄 알아야 된다던지, 한국에서만 팔릴 줄 알았는데, 유럽 여러 나라에도 수출을 한다던지... 도대체 그 Spec 이라는 것 자체가 계속해서 변해 간다는 것이지요.

야속한 사장님은 계속해서 뭔가가 바뀔 때마다 S/W를 계속 바꾸라고 하니, 바꿀 때마다 곤역입니다. 매번 여기 저기를 뜯어 고쳐야 하니까요. 이게 우리의 현실이죠. 그러니까 flexibility 라는 건 결국 뭔가가 바뀔 때마다 바뀐 Spec 에 따라 S/W를 바꾸기 쉬워야 한다는 것입니다.

그렇지만, 이 변화무쌍한 시대에 10년이면 강산이 변하는 게 아니라 1,2년만 지나도 확확 달라지는 이 세상에서 도대체 뭐가 어떻게 바뀔지 알고, 바뀔 것을 예상해 가면서 S/W를 flexible 하게 개발한다는 것입니까 ? 예~ 그렇죠. 인간이 신이 아닌 이상 나중에 바뀔 모든 사항을 예상해 가면서 *모든* 변경 사항에 대해 쉽게 수정 가능하도록 개발한다는 것은 당연히 불가능할 것입니다.

그렇지만, 뒤집어서 생각해 보면, 어느 정도 경험이 쌓이게 됐을 때-음... 속된 말로 이 바닥에서 몇 년 굴러 먹다 보면-는 앞으로 뻔하게 보이는 변경 사항들이 눈에 보이기 시작합니다. 눈에 뻔히 보이는 변경 사항들을 미리 대비해 쉽게 수정 또는 추가/삭제가 가능하도록 S/W를 개발하는 것은 가능합니다. 나중에 예상했던 변경 사항이 맞아 떨어지면 Bingo! 관리자에게 구조를 뜯어 고쳐야 한다며 오래 걸린다고 말해 놓구선, 금방 고치고, 남는 시간에 그동안 맘에 안들었던 코드를 깔끔하게 고치는 거죠. 내 맘에 들때까지.

flexible한 S/W를 개발한다는 게 그리 대단한 일일까요 ? 절대 그렇지 않습니다. 여러분이 매일 매일 하는 일이 flexible한 S/W를 개발하는 일일 수 있습니다. 예를 들어 다음 코드를 한 번 보실까요 ?

256, 255 라는 magic number 가 계속해서 사용되는 게 보이시나요 ? 처음 S/W를 작성할 때는 읽어야할 string 데이터가 256자로 충분했기 때문에 위와 같이 프로그래밍을 했는데, 나중에 갈수록 늘어가더니 300 자 정도가 됐습니다. 여러분은 어떻게 하시겠습니까? 당연히 256을 300 보다 큰 수로 늘려야 할 것입니다. 위의 예처럼 아주 단순하다면 256을 512 로 바꾸고, 255를 511로 바꾸는 것이 쉬을 것입니다. 그렇지만, 256이라는 magic number에 dependent 한 코드가 많이 있다면 어떻게 될까요 ? 다행히 제대로 다 수정한다면 좋겠지만, 사람이 하다보면 실수가 생길 수도 있을 것입니다. 만약에 이 코드를 작성한 사람과 이 코드를 유지보수하는 사람이 같고, 처음 작성한 후 얼마 안 되어 어떤 이유에 의해 위와 같은 코드를 수정하게 됐다면 그나마 다행이겠지요. 그렇지 않은 경우라면 십중팔구는 빼뜨리고 고치지 않는 부분이 생기게 마련입니다. 그렇다면 문제가 발생할 건 안 봐도 비디오죠. 위와 같은 코딩을 소위 hard coding 이라고 합니다.

그러니, 이런 문제를 미리 예방을 하시려면 256, 255와 같은 magic number를 쓰지 마시고, #define 문을 이용하여 정의하는 것이 정신 건강에 도움이 될 것입니다. 다음과 같이 수정하면 좋겠네요.

처음 코드를 작성할 때부터 위와 같이 해두면 나중에 고칠 때는 a.h의 DATA_BUF_SZ 값만 고치면 만사형통일 겁니다.

macro 치환 방식이 너무 맘에 들지 않으신다면 enum 방식이나 비교적 최신 C++에서 지원하는 const int 방식을 쓰셔도 될 겁니다.

요즘은 preprocessor 에 의한 #define 방식보다는 compiler 자체에 의해 치환이 되는 enum 방식과 const int 방식을 추천하더군요.

위와 같은 경우가 어떻게 보면 가장 기초적인 hard coding의 예라고 할 수 있을 겁니다. 이런 hard coding에 대해서는 누구라도 인정하는 바구요. 그렇다면 다음의 예는 어떤가요 ?


왠지 꺼림직하시죠 ? "아니 왜 저런 걸 코드에 박아 놓고 그래 ???" 라는 생각이 절로 드실 겁니다. 그렇지만 이런 저런 코드를 보다보면 저런 코드가 수 없이 많습니다. 생각보다 많이 hard coding을 하게 되더군요. 이런 코드를 보고 후배에게 "야~ 이걸 이렇게 코드에 hard coding 하면 어떻게 해.

data file 이름이 바뀔 수도 있잖아~" 이렇게 말하면 십중 팔구는 "#define 으로 선언해 놓았으니, 헤더 파일만 고쳐서 다시 컴파일하면 되요" 순간 열이 확 받히는데, 맞는 말인 것 같아서 그냥 넘어 갑니다. 나중에 가서 곰곰히 생각해 보니 "우리가 고객에게 소스코드를 주나 binary를 주나 ? 우리가 소스코드를 준다해도 우리 고객에게 data file 이름이 바뀌면, 헤더 파일을 고쳐서 재컴파일 해야한다고 얘기해야 하나 ?"라는 생각이 들면서 후배에게 달려가고 싶어집니다.

그렇죠. 그 후배의 말은 맞는 말인 것 같지만 상황에 따라 틀린 말일 수도 있습니다. 전혀 바뀔 가능성이 없다면 맞는 말이지만, 바뀔 가능성이 있다면 틀린 말입니다. 여기서 경험의 문제가 대두되는 거죠. 선배는 그 동안 이 바닥에서 몇 년을 구르면서 경험적으로 대부분 파일명 같은 건 코드에 박아 놓으면 안되더라라는 것을 터득한 것이고, 후배는 아직 이 바닥을 더 굴러 봐야 하는 거죠. 고객의 환경이 바뀔 수도 있다라는 것을 아직 잘 모르는 것이겠죠.

위 코드는 어떻게 고치는 게 좋을까요 ? data file 명을 고객이 어딘가에 직접 명시하도록 하면 되지 않을까요 ? 고객이 직접 명시하도록 하는 방법에는 여러가지가 있을 것입니다. 첫째로 환경변수, 둘째로 명령행 인자, 세째로 환경설정 파일(또는 window registry, flash 에 저장된 환경 설정 데이터 구조 등...)을 생각할 수 있을 것입니다.

우선 쉽게 환경변수를 쓰는 방법으로 고쳐보죠.

환경 변수를 쓰는 걸로 고쳐보긴 했는데... 여전히 좀 문제가 있네요. 환경 변수를 정의하지 않았다고, 에러 메시지를 출력하고 그냥 종료해 버리다니... 사용자가 자신의 환경에 맞게 configuration할 수 있도록 허용하는 것까지는 좋았는데, 항상 정의하도록 요구하는 것은 자칫 사용성을 떨어뜨릴 가능성도 높아집니다. 사용자가 설정해야 하는 환경 변수가 많아지면 더 문제가 커지죠. 최초로 실행을 성공시키기 전까지 수많은 시행착오를 거쳐야 할테니까요. 환경 설정값이 정말 프로그램이 실행하는데 필수적인 것이 아니라면 합리적인 default 값을 코드에 가지고 있는 게 차라리 낫습니다. 예를 들자면,

환경 변수니 명령행 인자니 하는 것들은 내장 시스템(embedded system)에서는 잘 써 먹을 수 없는 방법이니 내장 시스템에서는 환경 설정 파일(또는 flash의 환경 설정 데이터 구조)을 읽어 들여 소스트웨어 실행 환경을 구성하는 방법을 추천해 드리고 싶네요. 내장 시스템이 아닌 경우에도 환경 변수나 명령행 인자에는 환경 설정 파일의 위치를 명시하도록 해두고 나머지 환경 설정 내용은 환경 설정 파일에서 읽어들이도록 하는 게 좋습니다. 환경 변수나  명령행 인자에는 너무 많은 설정 내용을 명시하도록 하는 것은 사용성을 떨어뜨리니까요.

여기까지도 기초적인 수준의 hard coding 이네요. 아마 대부분의 분들은 이걸 hard coding 이라고 하는데 이의를 다시는 분이 없을 거구요.

자~ 이번에는 여러분이 Media Player를 개발한다고 가정해 보시죠. Media Player를 생각했을 때, 될 수 있으면 다양한 Media Type을 지원하는 것이 중요한 사항이 될 거라는 건 대충 짐작할 수 있습니다. 그래서 각 Media Type을 처리하는 decoder가 있다고 치면, Media Type 에 맞는 decoder를 호출하는 게 Media Player의 핵심 처리 로직이 될 것 같습니다. 그래서 대강 다음과 같은 코드를 짰다고 하죠.(좀 시대에 뒤떨어지긴 했지만 C 로 구현했다고 치겠습니다)

제일 처음에는 시대 상황이 상황인지라 JPEG, PNG, MPEG1, MP3 만 처리할 수 있으면 됐습니다. 그런데 시간이 갈수록 하루가 멀다하고 새로운 포맷들이 계속 나와서 그때마다 새로운 포맷을 처리하기 위해서는 항상 invokeDecoder()를 고쳐야 할 겁니다. case 문을 계속 추가해 가는 거죠. 그리고, decoderdefs.h 도 계속 수정해야 하구요. 이건 처리할 수 있는 포맷을 hard coding한 결과입니다. 이걸 hard coding의 예로 봐야하느냐 논란이 있을 수도 있겠지만, 어찌됐든 위 코드는 컨텐트 포맷에 대해서는 flexible하지 않은 것은 부인할 수 없는 사실입니다. 앞으로도 계속해서 컨텐트 포맷이 추가될텐데 이걸 좀 해결할 방법이 없을까요 ?

이렇게 해 보면 어떨까요 ? 테이블을 이용한 방법을 써 보는 거죠.

위와 같이 정의해 놓는다면, invokeDecoder()는 어떻게 고치면 될까요 ?

어떤가요 ? 이제는 invokeDecoder() 가 굉장히 단순해 진데다가 새로운 포맷을 지원해야 할 때마다 수정할 필요가 없어졌습니다. decoderdefs.h 도 변경할 필요가 없어졌구요. 새로운 포맷을 지원해야 할 때 수정이 필요한 파일은 decodertable.c 로만 한정됩니다. 여기저기 돌아다니면서 뜯어 고쳐야할 일이 없어지는 거죠. 고객에게 소스코드 형태로 제품을 넘길 때도 새로운 포맷을 지원하려면 typedef int (*Decoder)(char* pszContentFile, Channel* pChannel) 의 형식에 맞는 decode function을 작성하고, 해당 decode function이 처리할 수 있는 mime type을 decoderMap 테이블에 등록만 하면 된다고 문서에 명시하면 될 것입니다. 그리고, decodertable.c 라는 파일은 제품의 핵심 소스코드 디렉토리에 두는 것이 아니라, 사용자가 customize할 수 있는 소스를 모아두는 디렉토리에 따로 두는 거죠. 소스 코드로 제공하지 않고, library 형태로 제공한다면, invokeDecoder() 가 정의된 소스는 library에 포함되도록 하고, decodetable.c는 고객에게 소스코드 형태로 제공하면 될 것입니다. 비교적 간단한 수정이지만 flexibility는 상당히 향상되었다는 걸 느끼실 수 있을 것입니다. 새로운 환경-여기서는 새로운 media type-에 우리의 Media Player를 적응시키는 게 쉬워졌다는 거죠.

여기서 또 다른 중요한 사항을 눈치채지 않으셨나요 ? 제가 "눈에 뻔히 보이는 변경 사항들을 미리 대비해 쉽게 수정 또는 추가/삭제가 가능하도록 S/W를 개발하는 것은 가능합니다"라고 말했던 것이 기억 나시나요 ? 위와 같이 invokeDecoder()를 작성한 사람은 컨텐트 포맷이 앞으로 바뀔 수 있다는 것을 예상하고 포맷에 대해서 쉽게 수정 가능하도록-즉, 포맷에 대해 flexible 하도록-코드를 작성한 것이지요.

*모든* 것에 대해 flexible한 S/W는 있을 수가 없습니다. 예상되는 변경 사항에 대해 flexible한 S/W가 있을 뿐이죠. 따라서 여러분이 S/W 개발을 발주하는 고객이라면 요구명세서에 어떤 사항들에 대해서 flexible 해야 하는지 명시해야 할 것이고, 여러분이 개발하는 사람이라면 고객의 암묵적인 flexibility 요구사항을 파악하여 여러분 S/W 개발에 적용해야 할 것입니다. 여러분이 technical leader 이면서 프로젝트 초기에 flexibility를 확보해야 할 부분을 제시해주지 않고서는 나중에 왜 flexible하게 하지 않고, 이렇게 hard coding 했느냐 하는 식으로 말한다면 여러분의 책임을 follower 들에게 전가하는 것이 될 것입니다. 또는 flexible 해야 할 부분이 무엇일지 그리고 그 부분에 대해 flexible하게 하려면 어떻게 architecture를 가져가야 할지에 대해 충분히 고민할 시간을 주지 않고서는 무조건 빨리 빨리 개발하라고 하고서는 과제 마무리할 즈음에 가서 왜 flexible하게 하지 않았느냐 하고 화를 낸다면 그건 자기 자신을 욕하는 것과 같습니다.

저는 S/W 발전의 역사는 hard coding을 피하고 정말 flexible하고 soft 한 S/W를 만들기 위한 역사라고 생각합니다. 프로그래밍 언어의 역사가 그러하고, 소프트웨어 엔지니어링의 역사가 그러하고 설계 기법, 개발 방법론 등의 역사가 그러하다고 생각합니다. 그만큼 flexibility는 S/W 에서 항상 큰 화두였다는 것이지요.

이번 글은 제목을 한 번 반복하는 것으로 마칠까 합니다.

"역시 소프트웨어는 soft 해야 제 맛입니다"

Flexible한 S/W 작성하기 글에서 Flexibility에 대해 더 알아보도록 하겠습니다.

소프트웨어 관련된 저의 다른 글들도 참고로 읽어 보세요.

소프트웨어는 soft 해야 제 맛이다
Flexible한 S/W 작성하기
소스코드 복사의 위험성
C++ 이야기 첫번째: auto_ptr 템플릿 클래스 소개
C++ 이야기 두번째: auto_ptr의 두 얼굴
C++ 이야기 세번째: new와 delete
C++ 이야기 네번째: boost::shared_ptr 소개
C++ 이야기 다섯번째: 내 객체 복사하지마!
C++ 이야기 여섯번째: 기본기 다지기(bool타입에 관하여)