1. 실용주의 철학
1. 고양이가 내 소스코드를 삼켰어요
가장 큰 약점은 약점을 보일 것에 대한 두려움이다. - 보쉬에 1709
실용주의 철학의 초석 중 하나는 경력 향상, 프로젝트, 일상 업무의 면에서 자신과 자신의 행동에 책임을 지는 것이다. 실용주의 프로그래머는 경력에 대해 책임을 지고, 자신의 무지나 실수를 인정하기를 두려워 하지 않는다.
만약 벤더가 끝까지 잘 해내지 못할 위험요소가 있다면 여러분이 그에 대한 대책(contingency plan)을 세워야 한다. 소스코드와 디스크가 다 망가져 버렸는데 백업이 없다면, 그것은 여러분의 잘못이다. <고양이가 내 소스코드를 삼켰어요>라고 상관에게 말하는 것은 별 도움이 안 될 것이다.
Tip 어설픈 변명을 만들지 말고 대안을 제시하라 p32
나쁜 소식을 전하러 가기 전에 뭔가 시도해 볼 만한 다른 것은 없을까? 때로는 사람들이 뭐라고 말할 지 빤히 보이는 경우가 있는데, 그럴 땐 그들의 수고를 덜어 주어라. 변명 대신 대안을 제시하라. 안된 다고 하지 말고 상황을 개선하기 위해 무엇을 할 수 있는지 설명하라.
2. 소프트웨어 엔트로피
엔트로피는 시스템 내의 '무질서'한 정도를 가리키는 물리학 용어이다. 안됐지만, 열역학 법칙에 따르면 우주의 엔트로피는 점점 증가한다. 소프트웨어의 무질서도가 증가할 때 프로그래머는 이를 '소프트웨어의 부패'라고 일컫는다.
Tip 깨진 창문을 내버려두지 말라. p35
'깨진 창문'(나쁜 설계, 잘못된 결정, 혹은 형편없는 코드)을 고치지 않은 채로 내버려 두지 마라. 발견하자마자 바로 고쳐라. 적절히 고칠 시간이 충분치 않다면 판자로 덮는 것만이라도 하라. 불쾌한 코드를 주석처리하거나, 아직 구현되지 않음(Not Implemented)이라는 메시지를 표시하거나, 가짜 데이터로 대치해 놓거나 하라. 더 이상의 손상을 예방하기 위해 어떤 조치든 취하고 현 상황을 잘 관리하고 있다는 것을 보여줘라.
3. 돌멩이 수프와 삶은 개구리
무엇을 해야 하는지, 어떻게 해야 하는지 정확히 아는 상황이 있다. 전체 시스템이 눈 앞에 그냥 드러난다. 여러분은 그 시스템이 옳다는 걸 안다. 하지만 일을 착수하려고 허락을 구하는 때 부터, 뭔가가 지연되거나 사람들이 멍한 눈으로 여러분을 바라본다. 위원회가 생길 테고, 예산 승인이 필요하고, 일들이 복잡해지기 시작한다. 모든 사람이 각자 자신의 자원을 지키려고 할 것이다. 때때로 이걸 '시작 피로(start-up fatigue)'라고 한다.
돌멩이를 내놔야 할 때다. 큰 무리 없이 요구할 수 있을 만한 것을 찾아내라. 그리고 그걸 잘 개발해라. 일단 되면, 사람들에게 보여주고, 그들이 경탄하게 하라.
허락을 얻는 것보다 용서를 구하는 것이 더 쉽다 - 그레이스 호퍼
4. 적당히 괜찮은 소프트웨어
우리는 종종 뭔가 나아지게 하려다가 괜찮은 것마저 망친다. - 리어왕
'적당히 괜찮은' 이라는 문구는 너절하거나 형편없는 코드를 의미하지 않는다. 시스템이 성공하려면 사용자의 요구사항을 충족해야 한다. 단지 우리는 여러분이 생산해 낸 것이 어느 정도면 적당히 괜찮은지를 결정하는 과정에 사용자가 참가할 기회를 가져가야 한다는 걸 말하고 있는 것이다.
단순히 프로그램에 새 기능을 추가하거나 코드를 한 번 더 다듬는다던가 하기 위해서 사용자의 요구사항을 무시하는 것은 전문가답지 못한 것이다. 우리는 허둥대라고 주창하는 것이 아니다. 불가능한 시간 약속을 하거나 데드라인에 맞추기 위해 기본적인 걸 빼버리거나 하는 것 역시 똑같이 전문가답지 못하다. 여러분이 만드는 시스템의 범위와 품질은 해당 시스템 요구사항의 일부로 명기되어야 한다.
Tip 품질을 요구사항으로 만들어라.
오늘의 훌륭한 소프트웨어는 많은 경우, 내일의 완벽한 소프트웨어보다 낫다. 사용자들에게 뭔가 직접 만져볼 수 있는 것을 일찍 준다면, 피드백을 통해 종국에는 더 나은 솔루션에 도달할 수 있을 것이다.
5. 지식 포트폴리오
지식에 대한 투자가 언제나 최고의 이윤을 낸다. - 벤자민 프랭클린
여러분의 지식과 경험이야말로 가장 중요한 전문가적인 자산이다. 불행히도 그것들은 소진하는 자산(expiring assets)이다. 새로운 기술, 언어, 환경이 개발됨에 따라 지식은 옛 것이 된다. 여러분의 지식 가치가 점점 떨어짐에 따라, 회사나 클라이언트에 대한 여러분 자신의 가치 역시 떨어진다. 우리는 이런 일이 일어나는 걸 예방하고 싶다.
우리는 컴퓨터, 어플리케이션 도메인 등에 대해 프로그래머들이 알고 있는 모든 사실과 경험을 그들의 '지식 포트폴리오'로 생각해 보길 좋아한다. 지식 포트폴리오를 관리하는 것은 금융 포트폴리오를 관리하는 것과 매우 유사하다.
- 진지한 투자자들은 주기적으로 투자하는 습관이 있다.
- 장기간 성공의 열과는 다각화다.
- 똑똑한 투자자들은 자신의 포트폴리오를 보수적인 투자, 위험성이 큰 투자, 보상이 높은 투자 사이에서 균형을 잘 맞춘다.
- 최대 수익을 위해 투자자들은 싸게 사서 비싸게 팔려고 한다.
- 포트폴리오는 주기적으로 재검토하고 재조정되어야한다.
여러분이 엄청난 다독가이고, 해당 분야의 최신 발전에 대해 낱낱이 알고 있는데, 누군가 질문을 한다. 여러분은 답이 뭔지 전혀 알지 못하고, 허물없이 그걸 인정한다. 거기에서 멈추지 마라. 답을 찾기 위한 개인적인 도전으로 생각하라. 구루(guru)에게 물어보라. 웹을 검색해 보라. 도서관을 가보라.
스스로 답을 찾지 못하거든, 누가 답을 찾아줄 수 있을지 그 사람을 찾아라. 중단하지 마라. 다른 사람들과 이야기함으로써 개인 네트워크를 구축하는데 도움이 되기도 하고, 답을 찾는 도중에 별로 관련이 없어 보이는 문제들에 대한 해답을 찾아서 놀라는 일도 생길 것이다. 마지막으로 답해주는 사람들 모두에게 고맙다는 말을 꼭 전하라. 그리고 사람들의 질문 중에 여러분이 답할 수 있는 게 있다면 참여해서 자신의 몫을 하라.
6. 소통하라!
최고의 아이디어, 최상의 코드 혹은 가장 실용주의적인 사고 등이 있다고 해도 다른 사람들과 소통할 수 없다면 그것들은 궁극적으로 아무 효용이 없다.
- 말하고 싶은게 무엇인지 알아라.
- 청중을 알아라.
- 때를 골라라.
- 스타일을 골라라.
- 멋져 보이게 하라.
- 청중을 참여시켜라.
- 청자가 되어라.
- 응답하라.
2. 실용주의 접근법
7. 중복의 해악
프로그래머로서 우리는 지식을 수집하고, 조직하고, 유지하며, 통제한다. 우리는 명세서(specification)에 지식을 문서화하고 실행 코드에서 그 지식이 생명을 갖고 살아나도록 한다. 그리고 그 지식에서 테스트 중에 점검할 사항들을 제공받는다.
불행히도 지식은 고정적이지 않다. 그것은 변화한다. 종종 급격하게 말이다.
대부분의 사람들은 유지보수가 버그를 고치고 기능을 개선하는 것을 의미하기 때문에, 어플리케이션이 출시되었을 때 비로소 유지보수가 시작된다고 믿는다. 우리는 이들이 틀렸다고 생각한다. 프로그래머들은 늘 유지보수 모드에 있다. 우리가 이해하고 있는 것들은 날마다 바뀐다. 설계를 하거나 코딩을 하는 중에 새로운 요구사항이 도착한다. 어쩌면 환경이 변할 수도 있다. 이유가 무엇이건 간에, 유지보수는 별개의 활동이 아니며, 전체 개발 과정의 일상적인 부분이다.
유지보수를 하려면, 사물의 표현양식, 즉 어플리케이션에 들어 있는 지식의 캡슐들을 찾아내고, 또 바꿔야 한다. 문제는 명세와 프로세스 그리고 프로그램을 개발하는 중에 지식을 중복해 넣기 쉽다는 것이다. 그렇게 된다면 어플리케이션이 선적되기 한참 전부터 유지보수의 악몽이 시작될 것이다.
소프트웨어를 신뢰성 높게 개발하고, 개발을 이해하고 유지보수하기 쉽게 만드는 유일한 길은 우리가 DRY 원칙이라고 부르는 것을 따르는 것 뿐이라 생각한다. DRY 원칙이란 이것이다.
모든 지식은 시스템 내에서 단일하고, 애매하지 않고, 정말로 믿을만한 표현 양식을 가져야 한다.
DRY - 반복하지 마라 (Don't Repeat Yourself)
중복을 피해서 각각의 지식을 한 곳에 두고 DRY 원칙을 따르는 길은 아래에 정리해 보았다.
- 정보의 다양한 표현양식 : 예를 들어 클라이언트-서버 어플리케이션을 작성하는 데 클라이언트 사이드와 서버 사이드에서 서로 다른 언어를 사용하지만, 공통된 구조를 양쪽에서 표현해야 할 때가 있다. 이럴 때 필터나 코드 생성기 등을 사용하여 소프트웨어가 빌드 될 때마다 간단한 코드 생성기를 사용하여 공동의 메타데이터 표현에 여러 개의 언어에 걸쳐있는 구조를 만들어 낼 수 있다.
- 코드 내의 문서화 : 나쁜 코드는 많은 주석을 필요로 한다. DRY 원칙은 낮은 차원의 지식은 그것이 속하는 코드에 놔두고, 주석은 다른 높은 차원의 설명을 위해 아껴두어야 한다고 말한다. 그렇지 않으면 지식을 중복하게 되며 변경할 때마다 매번 코드와 주석 모두를 바꾸어야 한다. 주석은 필연적으로 낡게 될 것이고, 믿을 수 없는 주석은 주석이 전혀 없는 것보다 더 심각한 문제를 만들어 낸다.
- 문서화와 코드 : 문서와 코드는 모두 동일 지식에 대한 표현이다. 우리는 일정 마감이 다가올 수록 문서의 갱신을 뒤로 미루기 쉽다는 것을 잘 안다. 이 부분은 소프트웨어가 항상 모든 통과가 되게 만들고, 테스트가 명세에 정확하게 반영되기 위해 문서 자체에서 테스트를 자동으로 생성되게 만드는 방법을 생각해 볼 수 있다.
때로는 부주의한 중복이 발생하기도 한다. 아래는 선을 대표하는 클래스이다.
class Line {
public:
Point start;
Point end;
double length;
};
여기에는 중복이 있다. 길이는 언제나 시작과 끝 점으로 계산되는데 둘 중 하나를 바꾸면 길이도 자동으로 변한다. 또한 비용이 많이 드는 연산을 피하기 위해 데이터를 캐싱해야 하는 경우도 발생한다. 이러한 DRY 원칙을 반영하여 수정한 코드는 다음과 같다.
class Line {
private:
bool changed;
double length;
Point start;
Point end;
public:
void setStart(Point p) { start = p; changed = true; }
void setEnd(Point p) { end = p; changed = true; }
Point getStart(void) { return start; }
Point getEnd(void) { return end; }
double getLength() {
if (changed) {
length = start.distanceTo(end);
changed = false;
}
return length;
}
};
개발자들간의 소통도 중복을 피하기 위해 매우 중요하다. 재사용할 수 있는 코드를 많이 만들도록 하자.
8. 직교성
설계, 빌드, 테스트, 그리고 확장하기에 쉬운 시스템을 만드는 데에 있어 직교성(Orthogonality)은 매우 중요한 개념이다. 직교성은 기하학에서 빌려온 용어이다. 그래프의 축과 같이 두 직선이 직각으로 만나는 경우 직교한다고 말한다. 컴퓨팅에서 이 용어는 일종의 독립성이나, 결합도 줄이기를 의미한다. 하나가 바뀌어도 나머지에 어떤 영향도 주지 않으면 서로 직교한다고 할 수 있다.
잘 설계된 시스템에서는 데이터베이스 코드가 사용자 인터페이스에 대해 직교할 것이다. 데이터베이스에 영향을 주지 않으면서 인터페이스를 바꿀 수 있고, 또한 인터페이스를 바꾸지 않으면서 데이터베이스를 교환할 수 있다.
Tip. 관련 없는 것들 간에 서로 영향이 없도록 하라.
우리는 자족적(self-contained)인 컴포넌트를 설계하기 원한다. 독립적이며, 단일하고, 잘 정리된 목적을 가진 컴포넌트. 컴포넌트들이 각기 격리되어 있으면 어느 하나를 바꿀 때 나머지 것들을 걱정하지 않아도 된다. 해당 컴포넌트의 외부 인터페이스를 바꾸지 않는 한, 전체 시스템으로 퍼져나가는 문제를 일으키지는 않으리라고 안심할 수 있다.
직교적인 시스템을 작성하면 두 가지의 큰 장점이 있다. 생산성 향상과 리스크 감소.
- 생산성 향상
- 변화가 국소화(localize)되서 개발 시간과 테스트 시간이 줄어든다. 상대적으로 작고, 자족적인 컴포넌트를 작성하는 것이 하나의 커다란 코드 덩어리를 만드는 것보다 더 쉽다.
- 직교적인 접근법은 또한 재사용을 촉진한다. 컴포넌트들에 명확하고 잘 정의된 책임이 할당되어 있다면 애초에 구현자들이 미처 생각하지 못했던 방식으로 새로운 컴포넌트와 결합할 수 있다. 시스템이 더 느슨하게 결합(coupling)되어 있을수록 재설정(reconfigure)하고 리엔지니어링하기 쉽다.
- 리스크 감소
- 감염된 코드는 격리된다. 어떤 모듈이 병에 걸렸다 해도 시스템의 나머지 부분으로 증상이 전파될 확률이 낮다.
- 시스템이 잘 깨어지지 않는다. 어떤 부분을 골라서 약간 바꾸고 수리해도 거기서 생기는 문제점들은 그 부분에만 한정될 것이다.
팀 내 업무가 겹치는 영역이 많다면 구성원들은 책임 영역에 대해 혼동하게 된다. 뭘 하나 바꾸려고 해도 그들 중 누구라도 영향을 받을 수 있기 때문에 전체 팀원이 모여야 한다. 우리는 어플리케이션에서 인프라를 분리하는 방식을 선호한다. 주된 인프라 컴포넌트(데이터베이스, 커뮤니케이션 인터페이스, 미들웨어 데이터 레이어 등등)마다 서브팀을 할당한다. 어플리케이션 기능의 분할 역시 유사하게 나뉜다. 그러고 나서 현재의 가용 인원을 확인하고 조직을 적절하게 개편한다.
개발자 대다수는 직교적인 시스템을 설계할 필요를 잘 안다. 그런 설계 과정을 설명할 때 모듈라(modular), 컴포넌트 기반, 레이어 같은 다른 용어를 사용하기도 한다. 시스템은 협력하는 모듈들의 집합으로 구성되어야 하고, 각 모듈은 다른 부분과 독립적인 기능을 구현해야 한다. 때로는 이런 컴포넌트들이 레이어로 조직되기도 하는데, 각 레이어는 하나의 추상화 층을 이루게 된다. 이 레이어식 접근은 직교적 시스템을 설계하는 강력한 방법이다. 각 레이어는 자기 밑에 있는 레이어들이 제공하는 추상화만을 사용하기 때문에, 코드에 영향을 끼치지 않으면서 아래에 있는 다른 구현들을 바꾸는 높은 유연성을 얻을 수 있다.
현실 세계의 변화와 설계 사이의 결합도를 얼마나 줄였는지에 대해서도 스스로 물어보아야 한다. 전화번호를 고객 식별자로 사용한다고 했을 때, 전화 회사가 지역 번호를 재할당한다면 어떻게 할 것인가? 자신의 힘으로 제어할 수 없는 속성에 의존하지 마라.
써드파티 툴킷이나 라이브러리를 도입할 때, 시스템의 직교성을 보존할 수 있는지 주의 깊게 살펴보자. 기술을 현명하게 선택해야 한다. 만약 툴킷이나 심지어 같은 팀의 다른 멤버가 작성한 라이브러리를 도입할 때에도 이것이 여러분의 코드에 있어서는 안 될 변화를 강요하고 있지는 않은지 검토해보자. 만약 객체 영속 방식(object persistence scheme)이 투명하다면, 이것은 직교적이다. 하지만 특별한 방식으로 객체를 생성하고 접근해야 한다면, 그렇지 않다. 이러한 세부 사항을 코드로부터 분리한다면 미래에 벤더가 바뀌더라도 쉽게 대처할 수 있다는 이점이 있다.
직교성에 대한 흥미로운 변형은 AOP(Aspect Oriented Programming)이다. AOP는 이를 사용하지 않았더라면 소스코드 이곳저곳에 분산되어 있을 코드를 한 곳에 모아준다. 예를 들면, 로그 메시지는 소스코드 이곳저곳에 흩뿌려져 있는 로그 함수 호출을 통해 기록된다. AOP를 사용하면 로깅되고 있는 코드와 직교적으로 로깅을 구현할 수 있다.
코드를 작성하고 있다면 언제나 어플리케이션의 직교성을 떨어트릴 수 있는 위험에 노출되어 있다. 직교성을 유지하기 위해서 사용할 수 있는 몇 가지 기법은 다음과 같다.
- 코드의 결합도를 줄여라 : 불필요한 어떤 것도 다른 모듈에 보여주지 않으며, 다른 모듈의 구현에 의존하지 않는 코드를 작성하라. 디미터 법칙(Law of Demeter)을 따르려고 노력해보자. 객체의 상태를 바꿀 필요가 있다면, 객체 스스로가 여러분을 위해 그러한 일을 수행하게 만들라.
- 전역 데이터를 피하라 : 코드가 전역 데이터를 참조할 때 마다, 코드는 해당 데이터를 공유하는 다른 컴포넌트와 묶이게 된다. 읽기 전용 목적으로 전역 데이터를 사용한다 하더라도 문제가 발생할 수 있다. 예를 들어 코드를 갑자기 멀티쓰레드로 바꿔야 한다면 어떻게 될까? 싱글턴 패턴은 특정 클래스의 객체가 단 하나의 인스턴스만을 갖도록 보장해준다. 하지만 많은 개발자들이 싱글튼 객체를 전역 데이터의 일종으로 남용한다.
- 유사한 함수를 피하라 : 스트래티지 패턴(strategy pattern)을 사용하여 더 나은 구현을 할 수는 없는지 고려해보자.
9. 가역성
당신이 가진 생각이 딱 하나밖에 없다면, 그것만큼 위험한 것은 없다. - 에밀 사르티에, 종교론, 1938
무언가를 구현하는 방법에는 여러 가지 길이 있고, 보통 하나의 솔루션에는 여러 벤더의 제품이 존재한다. 이것은 이 방법만으로 해결할 수 있어와 같은 근시안적인 생각을 갖고 프로젝트에 참여한다면 아마도 예상치 못했던 경우에 의해 한숨지을 일이 많을 것이다. 많은 프로젝트 팀들이 프로젝트를 진행하면서 어쩔 수 없이 고통스럽게 그들의 근시안을 조금씩 수정하게 된다.
일단 어떤 벤더의 데이터베이스나 아키텍처 패턴, 혹은 특정 배포 모델(예를 들면 클라이언트 서버, 독립형)을 사용하기로 결정했고, 이를 통해 프로젝트를 진행해 왔다면 큰 비용을 치루지 않고는 이러한 결정을 되돌릴 수 없다.
프로젝트 초기에 특정 벤더의 관계형 데이터베이스를 사용하기로 결정했다 가정해보자. 한참 후 성능 테스트를 하면서 다른 객체형 데이터베이스가 더 빠르다는 것을 알게 되었다. 이 때 만약 데이터베이스에 대한 아이디어를 올바르게 추상화하여 어떤 하나의 지점이 영속 서비스를 제공하도록 만들었다면 도중에 말을 갈아 탈 수 있는 유연성을 갖게 될 것이다.
보통 특정 벤더 제품에 대한 의존도 등은 잘 정의하고 추상화한 인터페이스를 통해 감출 수 있다. 사실 우리가 수행해 온 모든 프로젝트를 이런 식으로 수행할 수 있다. 만약 벤더 의존적인 코드를 깨끗하게 분리하지 않으면 어떻게 될까? 벤더 의존적인 수행문들이 코드 전반에 흩어져 있을 것이고, 이는 유지보수성, 유연성을 극도로 떨어뜨리게 된다. 요구사항을 메타데이터에 넣고, 필요한 수행문을 코드에 넣을 때 메커니즘을 자동화시켜라. 그리고 어떤 매커니즘을 이용하든 이를 되돌릴 수 있도록 하라. 무언가 자동으로 추가할 수 있다면, 역시 자동으로 빼낼 수도 있어야 한다.
양자 역학 분야의 유명한 메타포인 슈뢰딩거의 고양이 이야기를 소개하려 한다. 어떤 고양이가 밀폐된 상자에 갇혀 있다. 상자 안에는 1시간에 1/2의 확률로 알파 입자를 분해하는 알파 입자 가속기와 청산가리 통이 있다. 알파 입자가 분해되어 방출되면 청산가리 통의 센서에 감지되는데 이 경우 청산가리 통이 깨져 고양이는 죽게 된다. 1시간 후에 고양이는 살았을까? 죽었을까? 슈뢰딩거에 따르면 둘 다 옳은 답이다.
알파입자의 분해 주기마다 두 가지 가능한 결과가 있고, 이 때 마다 우주는 복사된다. 한 곳에서 분해가 일어나고, 한 곳에서는 그렇지 않다. 그러므로 고양이는 한 우주에서는 살아있고, 다른 우주에서는 죽는다. 상자를 열어보았을 때야 비로소 여러분이 어떤 우주에 속해있는지를 알 수 있게 된다.
하지만 모든 코드의 진화를 슈뢰딩거의 고양이로 가득한 상자로 생각하라. 각각의 결정은 다른 버전의 미래를 야기한다. 여러분의 코드는 몇 가지 가능한 미래를 지원할 수 있는가? 어떤 미래가 일어날 가능성이 높은가? 그 미래가 닥쳤을 때, 이를 지원하는 것이 얼마나 어려울까?
상자를 열 용기가 있는가?
10. 예광탄
어둠 속에서 기관총을 쏘는 방법은 두 가지가 있는데, 하나는 목표물이 어디 있는지 확인 후 모든 변수를 계산하여 쏘는 것이고 다른 하나는 예광탄을 쏘는 것이다. 예광탄은 탄창의 일반 탄환들 사이에 일정한 간격으로 끼어있다. 예광탄이 발사되면 그 안에 든 인 성분이 발화하여 (그것이 무엇이 되었든지 간에) 총알을 맞은 것과 총 사이에 빛의 궤적을 남긴다. 만약 예광탄이 목표물을 맞힌다면 일반 탄환도 마찬가지로 맞힐 것이다.
사람들이 힘든 계산보다 예광탄을 더 좋아한다는 사실은 놀랄 일이 아니다. 예광탄은 반응도 즉각적이고, 실제 탄환과 동일한 환경 조건에서 날아가기 때문에 외부의 영향도 최소화한다. 새로운 프로젝트에서도 이 사실은 마찬가지다. 전에 만들어진 적이 없는 전혀 새로운 것을 만들고 있다면 더욱 그렇다. 기관총 사수와 마찬가지로 어둠 속에서 목표물을 맞추어야 한다. 사용자들이 이전에 그런 시스템을 한 번도 본 적이 없기 때문에 요구사항이 막연할지도 모른다. 익숙하지 않은 알고리즘, 기술, 언어, 라이브러리들을 사용해야 할 지도 모르므로, 여러분은 수 많은 미지의 것과 맞닥뜨리게 된다. 그리고 프로젝트는 완성하는 데 시간이 걸리기 때문에, 일을 마치기 전에 여러분의 작업을 둘러싼 환경이 변화하리라는 것도 거의 확실히 장담할 수 있다.
예광탄이 효과가 있는 까닭은 일반 탄환과 동일한 환경과 제약 조건에서 발사되고 날아가기 때문이다. 탄환이 목표물에 도달하는 시간이 짧기 때문에, 기관총 사수는 즉각적인 반응을 얻을 수 있다. 그리고 실용적인 관점에서 봐도 예광탄은 상대적으로 비용이 적게 드는 방법이다. 코딩에서도 동일한 효과를 얻으려면, 우리를 요구사항으로부터 최종 시스템의 일부 측면에까지 빨리, 눈에 보이게, 반복적으로 도달하게 해줄 무언가를 찾아야 한다.
예광탄 코드 접근 방법에는 여러 장점이 있다.
- 사용자들은 뭔가 작동되는 것을 일찍부터 보게 된다.
- 개발자들은 들어가서 일할 수 있는 구조를 얻는다.
- 통합 작업을 수행할 기반이 생긴다.
- 보여줄 것이 생긴다.
- 진전 상황에 대해 더 정확하게 감을 잡을 수 있다.
예광탄은 지금 맞추고 있는 것이 무엇인지 보여준다. 그러나 그것이 꼭 목표물이라는 보장은 없다. 그럴 경우 목표물이 맞을 때 까지 조준을 옮겨야 한다. 이것이 핵심이다.
예광탄 코드도 마찬가지다. 예광탄 코드 기법은 일이 어떻게 될지 100% 확신할 수 없는 상황에서 사용된다. 그러므로 처음 몇 번 시도 때 목표에 맞지 않더라도 놀랄 필요가 없다. 지금 있는 것을 목표물에 가져가려면 어떻게 바꾸어야 할지 생각해내고, 가벼운 개발 방법론을 선택했다는 사실에 감사하라. 코드의 크기가 작으면 관성 역시 약하므로 빠르고 쉽게 바꿀 수 있다.
예광탄 코드 vs 프로토타이핑
프로토타이핑을 예광탄과 비슷하다고 생각할 수 있겠지만 둘은 엄연히 다르다. 프로토타입은 최종 시스템의 어떤 특정한 측면을 탐사해 보는 것이 목표다. 즉 이걸 바탕으로 다시 새롭게 만들 각오를 하고 짜는 코드인 것이다. 진짜 프로토타이핑 방식을 따른다면, 어떤 개념을 구현해 보려고 시도할 때 대충 끼워 맞춘 것들을 모두 버린 다음, 실험 과정에서 얻은 교훈을 바탕으로 다시 코드를 만들게 된다.
예광탄 코드 접근 방법은 다른 종류의 문제에 대한 대응 방법이다. 어플리케이션이 전체적으로 어떻게 연결되는지를 알고 싶고, 사용자들에게 실제로 어플리케이션의 요소들이 어떻게 상호작용하는지 보이고 싶고, 개발자들에게는 코드를 붙일 아키텍처적 골격을 제시하고 싶다면, 이 경우 대강 구현한 컨테이너 채우는 알고리즘과 단순하지만 동작은 하는 사용자 인터페이스로 구성된 예광탄을 만들 것이다.
둘의 차이점을 다시 한 번 강조하면 프로토타입은 나중에 버릴 수 있는 코드를 만든다. 예광탄 코드는 기능은 별로 없지만, 완결된 코드이며, 최종 시스템 골격의 일부를 이룬다. 프로토타입을 예광탄이 하나라도 발사되기 전에 먼저 일어나는 정찰과 정보 수집 정도로 생각하면 좋다.
11. 프로토타입과 포스트잇
프로토타입은 실제 제품을 만드는데 들어가는 비용보다 적은 비용으로 가설을 검증하고 위험 요소 등을 분석하고 노출할 수 있다는 장점을 가지고 있다. 프로토타입은 반드시 코드로 작성할 필요는 없으며, 포스트잇 등을 통해 작업 흐름과 어플리케이션 로직 등을 동적으로 프로토타이핑 할 수도 있다.
프로토타입을 통해 조사할 대상은 위험을 수반하는 모든 것이다. 또한 이전에 해본 적이 없는 것, 최종 시스템에 매우 중요한 것 등이 프로토타입의 대상이 된다. 실험적이거나, 의심이 가거나, 심적으로 편하지 않은 것은 모두 프로토타입의 대상이 될 수 있다. 예를 들면 다음과 같은 것들을 생각해 볼 수 있다.
- 아키텍처
- 기존 시스템에 추가할 새로운 기능
- 외부 데이터의 구조 혹은 내용
- 성능 문제
- 사용자 인터페이스 설계
아키텍처 프로토타이핑
많은 프로토타입들이 고려 중인 전체 시스템을 모델링하기 위해 만들어진다. 예광탄과는 달리 프로토타입 시스템의 모듈이 꼭 기능을 가져야 하는 것은 아니다. 사실 아키텍처를 프로토타이핑할 때 코드를 작성하지 않고, 화이트보드, 포스트잇, 인덱스카드 등을 사용해도 된다. 프로토타입에서 기대하는 것은 전체적으로 시스템이 어떻게 동작할지에 대한 감을 잡는 것이다. 다시 말하지만 세부사항은 무시한다. 다음은 아키텍처 프로토타입에서 규명할 만한 사항이다.
- 주요 컴포넌트의 책임이 잘 정의되었고 적절한가?
- 주요 컴포넌트 간의 협력관계가 잘 정의되어 있는가?
- 결합도는 최소화되었는가?
- 잠재적 중복을 찾아낼 수 있는가?
- 인터페이스 정의와 제약 사항은 수용할만한가?
- 각 모듈이 실행 중에 필요로 하는 데이터에 접근할 수 있는 경로를 갖고 있는가? 모듈은 데이터를 필요로 할 때 데이터에 접근할 수 있는가?
12. 도메인 언어
언어의 한계가 곧 자기 세계의 한계다 - 루트비히 비트겐슈타인
컴퓨터 언어는 여러분이 문제에 대해 생각하는 방식과 의사소통에 대해서 생각하는 방식에 영향을 미친다. 모든 언어에는 그 언어의 특징들의 목록이 딸려온다. 그 목록들을 보면 정적 타입 대 동적 타입, 이른 바인딩 대 늦은 바인딩, 상속 모델(단일, 다중, 아니면 아예 상속이 없거나)과 같은 유행어들이 들어 있다. 이런 용어는 모두 어떤 해결 방안들을 제시할 수도 있지만 가려버리기도 한다.
우리는 언제나 어플리케이션 도메인의 어휘를 사용해서 코드를 작성하려고 노력한다. 몇몇 경우, 한 차원 더 나아가서 실제로 그 도메인의 어휘, 문법, 의미론 - 즉 언어 - 을 사용해서 프로그래밍하는 일이 가능할 때도 있다. 어떤 어플리케이션 프로그램을 제어하거나 환경설정하기 위한 단순한 언어든, 규칙이나 절차를 명시하기 위한 더 복잡한 언어든, 우리는 여러분의 프로젝트를 문제 도메인에 가까운 곳으로 옮길 방법들을 궁리해봐야 한다고 생각한다. 더 높은 추상화 수준에서 작업함으로써 사소한 구현의 세부사항들을 무시하고 도메인의 문제들을 푸는 데에만 정신을 집중할 수 있다.
구현하는 언어는 서로 다른 두 가지 방법으로 쓰일 수 있다.
- 데이터 언어 : 어플리케이션이 사용할 어떤 형식의 데이터 구조를 만든다. 이런 언어는 환경설정 정보를 표현하기 위해 쓰이는 경우가 많다.
- 명령형 언어 : 실제로 실행되며, 문장, 제어 구조체 등등을 가질 수 있다. 프로그램의 유지보수를 쉽게 하기 위해서 만들어진다.
예를 들어 레거시 어플리케이션에서 출력되는 정보를 새로운 GUI 개발에 통합해 달라는 요청을 받았다고 가정하자. 화면 긁어오기(screen scraping)를 통해 어플리케이션은 메인프레임 어플리케이션에 마치 일반 인간 사용자처럼 연결해서, 키보드 입력을 발생시키고 돌아온 결과를 읽는다. 어플리케이션에서 지금 이 사회보장번호를 입력하라고 결정하면, 인터프리터를 불러 이 스크립트를 실행하게 만들고, 그러면 이 스크립트가 레거시 어플리케이션과의 트랜잭션을 제어한다. 만약 인터프리터가 어플리케이션 내부에 내장되어 있다면, 아예 서로 데이터를 직접 공유하도록 만들 수도 있다.(예를 들면 콜백 메커니즘을 이용)
13. 추정
추정에 대한 지식을 배운 후에 경험을 통해 추정 능력을 계발하고, 어디에 크기에 대한 직관적 느낌을 적용해야 할지를 알게 된다면, 무언가의 가능성을 가늠할 수 있는 마술과 같은 능력을 발휘할 수 있게 될 것이다.
Tip. 추정을 통해 놀람을 피하라
모든 추정치는 문제의 모델에 기반한다. 그에 앞서 가장 기본적인 추정 기술은 이미 그 일을 해본 사람에게 물어보라는 것이다. 모델 작성에 몰입하기 전에 과거에 비슷한 상황에 처했던 사람이 없는지 주변 사람들에게 문의해보고, 그들이 어떻게 문제를 해결했는지 이해하려 노력해보자. 추정치를 내는 팁을 몇 가지 적어본다.
- 무엇을 묻고 있는지를 이해하자 : 어떤 종류의 추정이건 연습의 첫 단계는 상대방이 무엇을 묻고 있는지에 대해 이해하는 것이다. 앞서 이야기한 정확도만이 아니라 도메인의 범위에 대해 감을 잡을 필요가 있다. 추정을 하기 전에 미리 생각하는 습관을 기르는 것이 좋다.
- 시스템의 모델을 만들어보라 : 이는 추정의 재미난 부분이다. 클라이언트가 요청한 것이 무엇인지 이해한 후에는 대략적이고, 꾸밈없는 모델을 만들어 보는 것도 좋다. 만약 응답시간을 추정하고 있다면 여러분의 모델은 서버와 서버에 도달하는 몇 종류의 트래픽을 포함하게 될 것이다. 프로젝트에서 모델은 여러분의 조직이 개발을 하는 동안 사용할 디딤대가 되어 줄 뿐 아니라 시스템을 어떻게 구현해야 할지에 대한 대략적인 그림을 제공해 줄 것이다.
- 모델을 컴포넌트로 나누어라 : 노드에서 트래픽의 도착을 시뮬레이션 하는 것처럼 여러분은 각 컴포넌트가 전체 모델에 어떻게 기여하는지 영향을 미치는 매개변수를 갖고 있다는 것을 알게 될 것이다.
- 각 매개변수에 값을 주어라 : 이 단계에서 적용해 볼 만한 기술은 결과에 큰 영향을 미치는 매개변수가 무엇인지를 규명하고, 이 매개변수의 값들을 최대한 정확히 산출해 내는 것이다. 실제로 추정을 하다 보면 어떤 추정치를 다른 하위 추정치를 통해 계산하는 경우가 있는데, 이때 큰 오류가 슬그머니 기어 들어올 확률이 높다.
추정에 대한 일반 법칙은 변화하는 어플리케이션의 복잡성과 변덕스러움 앞에 와해되기 쉽다. 프로젝트의 일정을 정할 수 있는 유일한 방법은 진행하는 해당 프로젝트를 경험하는 것 뿐이란 사실을 알게 된다. 다음과 같은 단계를 반복하는 점증적인 개발을 연습하면 이 말을 더 온전하게 이해할 수 있다.
- 요구사항 체크하기
- 위험 분석하기
- 설계, 구현, 통합
- 사용자와 함께 검증하기
초기 기능의 구현과 테스트를 마친 후, 이를 첫 번째 반복의 끝으로 삼아라. 이 경험에 기반해 반복의 횟수와 각 반복에서 무엇을 할지에 대한 초기 추측을 다듬을 수 있다. 이런 정제는 각 반복이 끝날 때 마다 나아질 것이고, 일정에 대한 확신도 이와 함께 증가할 것이다.
3. 기본적인 도구
모든 장인들은 기본적인 훌륭한 도구를 몇 개 갖고 자신의 여정을 시작한다. 각 도구에는 저마다의 개성과 까다로운 면이 있을 것이기에 그에 걸맞게 다루어야 한다. 도구 각각은 고유의 방식으로 날카롭게 다듬어야 하고, 또 고유의 방식으로 손에 쥐어야 한다. 도구는 재능을 증폭한다. 도구가 더 훌륭하고, 그걸 어떻게 사용하는지 더 잘 알수록, 여러분은 더 생산적일 수 있다.
많은 신참 프로그래머들은 예컨대 특정 통합 개발 환경(IDE)같은 전동도구 하나만 고집하는 실수를 저지르고, 그 편한 인터페이스에서 떠날 생각을 하지 않는다. 이건 정말 실수다. IDE가 강제하는 편리함의 울타리 바깥에서도 능숙하게 작업할 수 있어야 한다.
14. 일반 텍스트의 힘
일반 텍스트란 사람이 직접 읽고 이해할 수 있는 형태의 인쇄 가능한 문자로 이루어진 테스트를 말한다. XML, SGML, HTML은 잘 정의된 구조를 가진 일반 텍스트의 훌륭한 예다. 일반 텍스트는 보통 구현에서 직접적으로 유도되는 이진 코딩보다 더 고차원적일 때가 많다.
대부분의 이진 포맷이 가진 문제는 데이터를 이해하는 데에 필요한 맥락이 데이터 자신으로부터 분리되어 있다는 점이다. 데이터와 의미를 인위적으로 절연하는 것이다. 데이터는 암호화될 수도 있다. 그러면 해당 데이터는 그걸 해석하는 어플리케이션 로직 없이는 아무런 의미도 없어지게 된다. 하지만 일반 텍스트를 사용하면 그걸 생성하는 어플리케이션에 상관없이 어떤 의미인지 자명한 데이터 흐름(stream)을 얻을 수 있다.
텍스트의 힘
사용자가 더 많은 공간을 차지하게 혹은 더 느리게 같은 기능을 요구하는 경우는 그다지 많지 않은 것 같은데 일반 텍스트로 고생을 하는 이유는 뭘까?
- 구식이 되는 것에 대한 보험 : 사람이 읽을 수 있는 형태의 데이터와 자명한 데이터는 어떤 다른 형태의 데이터와 그걸 생성한 어플리케이션보다 더 오래 살아남을 것이다.
- 호환성 : 소스코드 관리 시스템, 컴파일러 환경, 에디터, 단독으로 사용되는 필터에 이르기까지 컴퓨팅 세계의 거의 모든 도구들은 일반 텍스트를 다룰 수 있다.
- 더 쉬운 테스트 : 만약 시스템 테스트를 구동하게 할 합성 데이터를 만들기 위해 일반 텍스트를 사용한다면, 특별한 도구를 만들어야 할 필요 없이 간단히 테스트 데이터를 추가, 업데이트, 수정할 수 있다.
유닉스 철학
유닉스는 작고 예리한 각각의 도구가 한 가지 일만 잘 하도록 만들자는 철학에 따라 설계된 것으로 유명하다. 이 철학은 라인 중심의 일반 텍스트 파일을 기반 포맷으로 공유하기 때문에 가능하다. 시스템 관리를 위해 사용되는 데이터베이스는 모두 일반 텍스트 파일로 저장된다.
시스템이 고장났을 때, 복구를 위해서는 최소한의 환경밖에 사용할 수 없는 경우가 있다. 이런 상황에서 여러분은 일반 텍스트의 단순함에 고마움을 느끼게 될 것이다.
15. 조개 놀이
텍스트 파일을 다루는 프로그래머에겐 명령어 셸이 작업대가 된다. 셸 프롬프트의 모든 종류의 도구를 불러 쓸 수 있다. 파이프를 이용해 원 개발자가 결코 꿈꾸지 못할 방식으로 도구를 결합시키면서 파일을 결합할 수 있고, 시스템의 상태를 알아볼 수 있으며, 출력을 필터링할 수 있다. 또한 셸을 프로그래밍해서 자주 수행하는 활동에 쓸 복잡할 매크로 명령을 만들 수도 있다.
몇 가지 예시를 보자
- Makefile 보다 더 최근에 수정된 모든 .c 파일을 찾아라
find . -name '*.c' -newer Makefile -print
- 소스의 zip/tar 아카이브를 만들어라
zip archive.zip *.h *.c // 또는 tar cvf archive.tar *.h *.c
- 지난 주 중에 변경되지 않은 자바 파일은 어느 것들인가
find . -name '*.java' -mtime +7 -print
여기에 나온 셸 명령들은 이해하기 어렵거나 너무 불친절해 보일지도 모르겠지만, 이것들은 강력하고 간결하다. 그리고 셸 명령어들을 결합해서 스크립트 파일을 만들 수 있기 때문에, 여러분이 자주 하는 일을 일련의 명령어로 자동화 할 수 있다. 셸에 익숙해지면 생산성이 급상승하는 걸 알게 될 것이다.
16. 파워 에디팅
우리는 하나의 에디터에 대해 매우 잘 알고, 코드, 문서화, 메모, 시스템 관리 등 모든 편집 작업에 그것을 사용하는 것이 더 낫다고 생각한다. 단일한 에디터가 없다면 바벨탑의 혼란을 다시 접하게 될 것이다.
하나의 에디터를 능숙하게 잘 사용하는 것이 매우 중요하다. 에디터 하나를 골라서 완전히 마스터하고, 모든 편집 작업에 그 에디터를 사용하라. 여러분이 만약 텍스트 편집 전반에 하나의 에디터(혹은 하나의 키 바인딩 세트)를 사용한다면 텍스트를 조작할 때 멈추고 생각하고 할 필요가 없을 것이다. 필요한 키 입력은 반사운동 수준이 될 것이다. 에디터는 여러분 손의 연장이 될 것이다. 키가 텍스트와 생각 사이로 미끄러지면서 노래를 부를 것이다. 그것이 우리의 목표다.
많은 에디터는 특정 프로그래밍 언어를 위해 고유한 기능을 제공한다. 이를 최대한 활용하여 수작업을 최소화 하도록 하자.
- 구문 강조
- 자동 완성
- 자동 들여쓰기
- 코드나 문서 상용어구 지원
- 관련 도움말 시스템
- IDE 기능 (컴파일, 디버그 등)
17. 소스코드 관리
진보라는 것은 변화와는 거리가 멀고 오히려 기억에 의존한다. 과거를 기억하지 못하는 사람은 과거를 반복할 운명이다. - 조지 산티아나 (George Santayana), <이성의 삶(Life of Reason)>
소스코드 관리 시스템 혹은 좀 더 넓은 의미의 형상관리(configuration management) 시스템은 소스코드나 문서 관련의 모든 변화를 기억한다. 더 좋은 것들은 컴파일러나 OS 버전까지도 기억할 수 있다. 적절히 설정된 소스코드 관리 시스템을 쓰면 소프트웨어의 이전 버전으로 언제든 되돌아 갈 수 있다.
내용이 20년 전에 작성이 되어 git에 대한 내용이 없어서 자세한 부분은 생략함.
18. 디버깅
참으로 고통스러운 일입니다.
자신이 어려움을 보고는 알게 되죠.
다른 누가 만든 게 아니고 바로 자신이 문제를 만들었다는 걸. - 소포클래스(Sophocles) 아이아스(Ajax)
소프트웨어 결함은 요구사항을 오해하는 것에서 코딩 에러에 이르기까지 여러 모습으로 나타난다. 아무도 완벽한 소프트웨어을 작성하지 못하기 때문에, 하루의 대부분을 디버깅하는데 보낼 것이라는 건 기정사실이다. 디버깅에 관련된 몇 가지 문제를 살펴보고 찾기 힘든 버그를 찾아내는 일반적인 전략을 몇 가지 알아보도록 하자.
디버깅의 심리
디버깅은 단지 문제 해결이라는 사실을 포용하고, 그 방식으로 공략하라. 비난 대신 문제를 해결하라. 버그가 여러분의 잘못인지 다른 사람의 잘못인지는 그리 중요한게 아니다. 어쨌거나 그 버그는 여러분의 문제로 남는다.
가장 속이기 쉬운 사람은 자기 자신이다 - 에드워드 불워 리톤 (Edward Bulwer-Lytton)
디버깅을 시작하기에 앞서 올바른 사고방식을 갖는게 중요하다. 마감일에 가깝거나 신경질적인 상사가 감시하고 있다면 당황하기 쉽지만, 당황하면 안된다. 한 발짝 뒤로 물러나 무엇이 자신으로 하여금 버그가 있을 거라고 생각하게 하는지, 그 증후의 원인이 무엇일지 실제로 생각해 보는 것이 정말 중요하다.
디버깅을 할 때 근시를 조심하라. 표면에 보이는 증상만 고치려는 욕구에 저항하라. 실제 문제는 여러분이 관찰하고 있는 것에서 몇 단계 떨어져 있고, 또 다른 여러 가지와 연관되어 있을 확률이 다분하다. 항상 문제의 근본적인 원인을 발견하려고 노력하고, 그 문제의 특정한 증상만 고치려고 하지 말라.
디버깅 전략
종종 프로그램이 뭘 하는지, 혹은 뭘 할 것인지 알아내는 가장 쉬운 방법은 그 프로그램이 다루는 데이터를 잘 살펴보는 것이다. 데이터와 데이터들 사이에 존재하는 모든 상호관계를 시각화할 수 있는 디버거를 사용하면 자신의 데이터에 대해 훨씬 더 깊은 통찰을 얻을 수 있다. 비록 데이터 시각화를 제한적으로 지원하는 디버거를 쓸 지라도, 여러분 스스로 이것을 할 수 있다. 손으로, 종이와 연필로 혹은 외부 작도 프로그램으로.
디버거는 일반적으로 프로그램의 현재 상태에 주목한다. 그러나 때로는 그 이상이 필요하기도 하다. 시간에 따라 프로그램이나 데이터 구조의 상태가 변하는 것을 볼 필요가 생기기도 한다. 스택 트레이스는 단지 여기에 어떻게 도달했는지를 말해줄 수 있을 뿐이다. 이 호출연쇄 이전에 여러분이 무엇을 하고 있었는지를 말해줄 수는 없다. 특히 이벤트 시스템에서는 더하다.
트레이싱 구문은 화면 혹은 파일에 출력하는 작은 진단용 diagnostic 메시지를 일컫는다. IDE 형태의 디버거에 비하면 원시적인 기법이긴 하지만, 디버거가 진단할 수 없는 몇 가지 종류의 에러를 진단하는 데에는 특별히 효과적이다. 트레이싱은 동시(concurrent) 프로세스, 실시간(real-time) 시스템, 이벤트 기반 어플리케이션 등, 시간 자체가 중요한 요소가 되는 시스템에서라면 값을 매길 수 없을 정도로 귀중하다. 코드 깊숙히 '파고 들어가기' 위해 트레이싱 구문을 사용할 수 있다. 즉, 콜 트리(call tree)에서 내려가기 위해 트레이싱 구문을 추가할 수 있다.
고무 오리 디버깅은 문제의 원인을 찾는데 매우 단순하지만 꽤나 유용한 기법으로, 누군가에게 그걸 설명하는 단순한 방법이 있다. 상대방은 여러분의 어깨 너머로 화면을 바라보면서 자기 머리를 계속 끄적인다. 그 사람은 말 한 마디 할 필요가 없다. 코드가 무엇을 해야 하는지 차근차근 설명해 나가는 단순한 행위 그 자체가 문제를 화면에서 뛰쳐나와 스스로 드러나게 하기도 한다.
19. 텍스트 처리
프로그래밍에서 텍스트 처리 언어는 목공에서 라우터(router)와 같다. 그것들은 모두 시끄럽고, 지저분하며, 어느 정도는 무식하게 완력(brute force)을 쓰는 것이다. 그걸 사용다가 실수라도 하면, 전체 조각들이 망가질 수 있다. 하지만 제대로 사용하기만 한다면 라우터와 텍스트 처리 언어 모두 믿기 힘들 정도로 강력하며, 또 다양한 용도로 사용할 수 있다.
이런 언어들은 중요한 기반 기술(enabling technologies)들이다. 이것들을 사용해서 재빨리 유틸리티를 만들어 낼 수 있고, 아이디어를 프로토타입해 볼 수 있다. 텍스트 처리 언어에서 저자는 펄(Pearl) 언어를 추천한다.(이 책의 초판은 2000년에 추천되었다) 저자는 펄을 사용하여 텍스트를 처리하고, 프로그램과 상호작용하고, 네트워크를 통해 커뮤니케이션 하고, 웹 페이지를 작동시키고, 임의 정확도의 산술 연산을 할 수 있다며 텍스트 처리 언어의 여러 가지 장점을 이야기한다.
예를 들면 자바(Java)에서 외부 클래스가 메서드를 통해서만 객체의 속성을 get/set 할 수 있도록 해서 액세스를 제한하는 것은 좋은 객체지향 프로그래밍 스타일이다. 하지만 속성(property)이 클래스 내에서 단순한 멤버 변수로 되어 있는 일반적인 경우, 각 변수에 대해 get/set 메서드를 일일이 만드는 것은 지겹고 기계적인 일이다. 우리는 소스 파일을 수정해서 적절히 플래그가 달린 모든 변수에 대해 올바른 메서드를 정의하는 펄 스크립트를 만들 수 있다.
20. 코드 생성기
코드 생성기를 만들면 추가 비용 없이 사용이 가능하며 반복적인 작업을 대신 해주기 때문에 실수도 줄어든다. 코드 생성기에는 수동적 코드 생성기와 능동적 코드 생성기 두 가지가 있다.
수동적 코드 생성기
수동적 코드 생성기는 타이핑을 줄여준다. 수동적 코드 생성기는 기본적으로 몇 개의 입력에서 주어진 출력을 생성하는 매개 변수화된 템플릿이다. 결과물이 만들어 지기만 하면, 그 결과물은 프로젝트의 어엿한 소스 파일이 된다. 수동적 코드 생성기에는 다음과 같은 용도가 있다.
- 새 소스 파일 생성
- 프로그래밍 언어간 일회용 변환을 수행하기
- 런타임에 계산하기엔 비용이 많이 드는 참조 테이블과 여타 자원을 생성하기
능동적 코드 생성기
수동적 코드 생성기는 단지 편하려고 사용하는 반면, 능동적 코드 생성기는 DRY 원칙을 따르고 싶다면 필수적으로 써야 하는 것이다. 능동적으로 코드 생성기가 있으면, 어떤 지식을 단 하나의 형태로만 만들어 놓고 어플리케이션이 필요로 하는 온갖 형식으로 변환할 수 있다.
데이터베이스 어플리케이션을 만들고 있는 경우를 생각해보자. 이 경우, 여러분이 다루어야 할 환경은 데이터베이스와 그 데이터베이스에 접근하기 위해 사용하는 프로그래밍 언어 두 개다. 데이터베이스에는 데이터베이스 스키마가 있고, 프로그래밍 언어에서는 어떤 데이터베이스 테이블들의 형식과 똑같은 저수준 구조체들을 만들 필요가 있다. 구조체를 선언하는 코드를 직접 손으로 짜면, 이는 DRY 원칙을 어기는 것으로, 이렇게 하면 스키마에 대한 지식이 두 장소에서 표현되어 버리고 만다.
능동적 코드 생성기를 사용하면 스키마를 가져다가 구조체들의 소스코드를 만드는데 사용한다. 스키마가 변경될 때 마다 그 스키마에 접근하기 위해 사용되는 코드 또한 자동적으로 변경된다. 열이 하나 제거되면, 구조체에서 해당하는 필드도 제거될 것이고, 그 열을 사용하는 모든 고수준 코드가 컴파일 되지 않을 것이다. 가동 시간이 아니라 컴파일 시간에 잘못을 잡게 된다. 물론 이런 방식은 코드 생성을 빌드 과정 자체의 일부로 만들어 두었을 때에만 효과가 있다.
4. 실용주의 편집증
Tip. 완벽한 소프트웨어는 만들 수 없다.
실용주의 프로그래머들은 자기 자신을 믿지 않는다. 어느 누구 심지어는 자기 자신도 완벽한 코드를 작성할 수 없음을 알기 때문에 실용주의 프로그래머는 자신의 실수에 대비해 방어적으로 코드를 짠다. 이에 대한 방법들을 챕터 하나씩 다뤄보려고 한다.
21. 계약에 의한 설계
상식과 정직만큼 사람을 놀라게 하는 것은 없다. - 랄프 왈도 에머슨(Ralph Waldo Emerson) <에세이>
DBC
계약에 의한 설계(Designed By Contract, DBC)는 버트란드 마이어(Eiffel 언어 창시자)에 의해 개발된 개념이다. DBC는 단순하지만 강력한 기법으로, 프로그램의 정확성을 보장하기 위해 소프트웨어 모듈들의 권리와 책임을 문서화(및 이에 대해 동의) 하는 데에 초점을 맞춘다. 정확한 프로그램이란 무엇인가? 스스로 자신이 하는 일이라고 주장하는 것보다 많거나 적지도 않게 딱 그만큼만 하는 프로그램을 말한다.
소프트웨어 시스템의 모든 함수와 메서드는 뭔가를 한다. 그 뭔가를 시작하기 전에 해당 루틴은 세상의 상태에 대해 어떤 기대를 갖고 있을 테고, 루틴이 끝난 후에 세상의 상태가 어떠해야 한다는 진술을 할 수 있을 것이다. 마이어는 이런 기대를 설명하면서 다음과 같이 주장한다.
- 선행조건(precondition) : 루틴이 호출되기 위해 참이어야 하는 것. 즉 루틴의 요구사항. 루틴의 선행조건이 위반된 경우에는 루틴이 호출되어서는 안 된다. 제대로 된 데이터를 전달하는 것은 호출하는 쪽의 책임이다.
- 후행조건(postcondition) : 루틴이 자기가 할 것이라고 보장하는 것. 즉 루틴이 완료되었을 때 세상의 상태. 루틴에 후행조건이 있다는 것은 곧 그것이 종국에는 종료될 것이라는 걸 암시한다. 무한 반복은 암시되지 않는다.
- 클래스 불변식(class invariant) : 호출자의 입장에서 볼 때는 이 조건이 언제나 참이라고 클래스가 보장한다. 루틴의 내부 처리 중에는 불변식이 참이 아닐 수도 있지만, 루틴이 종료하고 호출자로 제어권이 반환되는 때에는 불변식이 참이 되어야 한다. (불변식에 관여하는 어떤 데이터 멤버에게도 클래스가 무제한적인 쓰기 접근권을 줄 수 없다는 것을 기억하라.)
루틴과 그 루틴의 잠재적 호출자 간의 계약은 다음과 같다.
만약 호출자가 루틴의 모든 선행조건을 충족한다면, 해당 루틴은 종료시 모든 후행조건과 불변식이 참이 될 것을 보증해야 한다.
만약 계약 당사자 중 어느 한 쪽이든 이 계약 내용을 지키지 못하면 배상이 이루어진다. 예를 들면, 예외가 발생하거나 프로그램이 종료하거나 하는 것이다. 무슨 일이 벌어지든지 간에 계약에 부응하지 못하는 게 버그가 되어버리는 실수는 저지르지 마라.
상속과 다형성은 객체지향 언어의 기본인데, 이런 계약이 정말 빛을 발할 수 있는 분야이기도 하다. 한 클래스가 다른 클래스의 한 종류(is-a-kind-of)
인 경우, 상속을 이용해서 한 종류
관계를 만든다고 상정해 보자. 여러분은 아마 리스코프 대체 원칙을 지키길 원할 것이다.
서브클래스는 사용자가 차이점을 모르고서도 기반 클래스 인터페이스를 통해 사용할 수 있어야 한다
달리 말하자면, 여러분이 만든 새 서브타입이 정말 베이스타입의 한 종류
여야 한다. 즉, 동일한 메서드를 지원하고, 그 메서드들은 동일한 의미를 가져야 한다. 이걸 계약을 통해 확실히 할 수 있다. 계약을 기반 클래스에 오로지 한 번만 명기하면, 자동으로 미래의 모든 서브클래스에 적용되도록 할 수 있다. 서브클래스는 원한다면 조금 더 넓은 범위의 입력을 받아들이거나, 혹은 더 강한 보증을 할 수 있다. 하지만 그 서브클래스는 자신의 부모 이상으로 받아들이고, 최소한 자신의 부모만큼은 보증해야 한다.
22. 죽은 프로그램은 거짓말을 하지 않는다
우리 중 대다수는 파일이 성공적으로 닫혔는지, 혹은 trace 문이 우리가 예상한 대로 찍히는지 확인하지 않는 코드를 작성한 경험이 있다. 그리고 다른 모든 조건이 동일하다면, 그럴 필요가 없었을지도 모른다. 문제의 코드는 정상 조건 하에서는 실패하지 않았을 것이다. 모든 에러는 정보를 준다. 우리는 에러가 발생할 리 없다고 스스로를 설득하고선 그걸 무시하기로 할 수 있다. 반면 실용주의 프로그래머는 만약 에러가 있다면 정말로 뭔가 나쁜 일이 생긴 것이라고 자신에게 이야기 한다.
망치지 말고 멈추라
가능한 한 빨리 문제를 발견하게 되면, 좀 더 일찍 시스템을 멈출 수 있다는 이득이 있다. 게다가 프로그램을 멈추는 것이 최선일 때가 많다. 자바 언어와 라이브러리는 이 철학을 포용했다. 런타임 시스템에서 뭔가 예상하지 못한 것이 발생하면 RuntimeException을 던진다. 만약 이 예외가 잡히지 않으면 프로그램의 최상위 수준까지 스며 나올 것이고, 결국 스택 트레이스를 출력하며 프로그램을 멈춰버릴 것이다.
23. 단정적 프로그래밍
모든 프로그래머가 자기 경력을 쌓는 초기부터 기억해야 하는 주문이 있는 것 같다. 컴퓨팅의 근본적 고의로 요구사항, 설계, 코드, 주석 등 우리가 하는 거의 모든 것에 적용하도록 배우는 핵심적 믿음이다. 그것은 이렇게 시작한다.
이런 일은 절대 일어날 리 없어
이런 류의 자기기만을 훈련하지 말자. 특히 코딩할 때는,
tip. 단정문을 사용해서 불가능한 상황을 예방하라
"하지만 물론 그건 절대 일어나지 않을 거야"라는 생각이 든다면, 그걸 확인하는 코드를 추가하라. 이걸 하는 가장 간단한 방법은 단정문(assertion)을 사용하는 것이다. C, C++에는 아래와 같이 assert문을 통해서 처리할 수 있다.
void writeString(char *string) {
assert(string != NULL);
...
단정은 알고리즘의 동작을 검사하는데 유용하게 쓰일 수도 있다. 물론, 단정문에 전달된 조건은 부작용이 있으면 안된다. 또한, 컴파일 중에 단정 기능이 꺼져 있을 수도 있다는 걸 기억하라. 실행되어야만 하는 코드는 절대 assert 속에 두지 마라. 진짜 에러처리 대신으로 단정을 사용하지는 마라. 단정은 결코 일어나면 안 되는 것들을 검사한다.
단정과 그 부작용(side effect)
에러를 발견하려고 넣은 코드가 오히려 새로운 에러를 만드는 결과를 낳는다면 상당히 당황스러울 것이다. 단정문을 쓸 때에도 조건을 평가하는 코드가 부작용이 있다면 이런 일이 발생할 수 있다. 예를 들어 자바에서 다음과 같은 코드를 작성하는 것은 그리 좋은 생각이 아니다.
while (iter.hasMoreElements()) {
Test.ASSERT(iter.nextElement() != null);
Object obj = iter.nextElement();
// ...
}
ASSERT 안에 있는 .nextElement() 호출은 이 호출이 돌려주는 원소 다음으로 반복자(iterator)를 이동시키는 부작용이 있다. 그러므로 이 반복문은 컬렉션 원소의 절반만 처리하게 된다. 다음과 같이 작성하는 것이 좋다.
while (iter.hasMoreElements()) {
Object obj = iter.nextElement();
Test.ASSERT(obj != null);
// ...
}
이는 디버깅 행위가 디버깅되는 시스템의 행동을 바꿔버리는, 일종의 '하이젠버그'적인 문제이다.
24. 언제 예외를 사용할까
이전 챕터에서 우리는 모든 가능한 에러를 체크하는 것이 좋다고 했다. 하지만 실제로 이렇게 하다보면 코드가 꽤 지저분해 질 수가 있다. 또한 프로그램의 정상적 로직이 에러 처리에 묻혀서 잘 보이지 않게 될 수도 있다.
너무 당연한 말이지만, 예외 처리는 예외적인 상황에서만 이루어져야 한다. '모든 예외 처리기(exception handler)를 제거해도 이 코드가 여전히 실행될까?' 라고 자문해 보았을 때 '아니오'라는 답이 나온다면 예외 처리기가 비예외적인 상황에서 사용되고 있는 것이다.
예를 들어, 코드가 어떤 파일을 열어 읽으려고 하는데 그 파일이 존재하지 않는다면 예외가 발생되어야 하는가? 이 질문에 대한 답은 '경우에 따라 다르다'이다.
만약 파일이 꼭 있어야 한다면, 예외가 발생되어야 한다. 왜냐하면 예상치 못한 일이 벌어진 것이기 때문이다. 반면, 파일이 반드시 있어야 하는 것인지에 대해 큰 관심이 없다면, 그 파일을 찾을 수 없다는 것이 그리 예외적인 일이 아닐 것이며, 에러를 반환하는 것이 적절하다.
우리는 왜 이런 식으로 예외를 처리해야 하는가? 예외가 있다는 것은 즉 컨트롤의 이동이 즉각적이고 로컬하지 않다는 것을 말한다. 예외를 정상적인 처리 과정의 일부로 사용하는 프로그램은 고전적인 스파게티 코드의 가독성 문제와 관리성 문제를 전부 떠안게 된다. 이런 프로그램은 캡슐화 역시 깨트린다. 예외 처리를 통해 루틴과 그 호출자들 사이의 결합도가 높아져 버린다.
에러 처리기는 에러가 감지되었을 때 호출되는 루틴이다. 특정 부류의 에러를 처리하기 위해 어떤 루틴을 등록하게 된다. 해당하는 에러가 났을 때 그 처리기가 호출될 것이다. 에러 처리기를 예외 대신, 혹은 예외와 함께 사용되길 원하는 경우가 있을 것이다.
자바의 RMI(Remote Method Invocation) 기능을 사용하는 클라이언트 서버 어플리케이션을 구현한다고 생각해 보자. 원격 루틴을 호출할 때마다 RemoteException을 처리할 준비가 되어야 한다. 이를 구현하기 위한 방법으로 원격이 아닌 클래스로 원격 객체를 감싸는 것이다. 그러면 이 클래스는 에러 처리기 인터페이스를 구현하며, 원격 예외가 감지되었을 때 호출될 루틴을 클라이언트 코드가 등록하도록 한다.
25. 리소스 사용의 균형
코딩할 때 우리는 모두 리소스를 관리한다. 메모리, 트랜잭션, 쓰레드, 파일, 타이머 등 사용에 어떤 제한이 있는 모든 종류의 것을. 대개의 경우, 리소스 사용은 예측할 수 있는 패턴을 따른다. 리소스를 할당하고, 사용한 다음, 해제(deallocate)한다.
그렇지만, 많은 개발자들은 리소스 할당과 해제를 다루는 일관된 계획을 갖고 있지 않다. 그래서 우리는 간단한 팁 하나늘 제안한다. 그것은 바로 시작한 것은 끝내라
이다.
간단한 예제를 하나 살펴보도록 하자. 파일을 열고 거기에서 고객 정보를 읽은 다음, 필드 하나를 업데이트하고, 결과를 다시 기록하는 어플리케이션이다.
void readCustomer(const char *fName, Customer *cRec) {
cFile = fopen(fName, "r+");
fread(cRec, sizeof(*cRec), 1, cFile);
}
void writeCustomer(Customer *cRec) {
rewind(cFile);
fwrite(cRec, sizeof(*cRec), 1, cFile);
fclose(cFile);
}
void updateCustomer(const char *fName, double newBalance) {
Customer cRec;
readCustomer(fName, &cRec);
cRec.balance = newBalance;
writeCustomer(&cRec);
}
겉으로 보기에 updateCustomer 루틴은 괜찮아 보인다. 필요로 하는 로직을 모두 구현했다. 레코드를 읽고, 잔액을 업데이트하고, 레코드를 재기록한다. 하지만 이 깔끔함 이면에는 큰 문제를 가지고 있다. readCustomer 루틴과 writeCustomer 루틴은 긴밀히 결합(coupling) 되어있다. 양자는 전역 변수 cFile을 공유한다. readCustomer는 파일을 열고 그 파일 포인터를 cFile에 저장하고, writeCustomer는 저장된 포인터를 사용해서 종료시 해당 파일을 닫는다.
이 코드가 나쁜 이유는? 만약에 명세가 잔액은 새 값이 음수가 아닌 경우에만 업데이트 되어야 하는 것으로 바뀌었다고 가정해보자. 다음과 같이 updateCustomer를 수정할 수 있다.
void updateCustomer(const char *fName, double newBalance) {
Customer cRec;
readCustomer(fName, &cRec);
if (newBalance >= 0.0) {
cRec.balance = newBalance;
writeCustomer(&cRec);
}
}
테스트 중에는 문제가 없을 수 있다. 하지만 코드는 릴리즈 되고 머지않아 너무 많은 파일이 열려있다는 이유로 무너진다. writeCustomer가 몇몇 상황에서 호출되지 않기 때문에 파일이 닫히지 않는다.
시작한 것은 끝내라 팁이 알려주는 것은, 리소스를 할당하는 루틴이 해제 역시 책임져야 한다는 것이다. 코드를 약간 리팩터링 해볼 수 있다.
void readCustomer(FILE *cFile, Customer *cRec) {
fread(cRec, sizeof(*cRec), 1, cFile);
}
void writeCustomer(FILE *cFile, Customer *cRec) {
rewind(cFile);
fwrite(cRec, sizeof(*cRec), 1, cFile);
}
void updateCustomer(const char *fName, double newBalance) {
File *cFile;
Customer cRec;
cFile = fopen(fName, "r+");
readCustomer(fName, &cRec);
if (newBalance >= 0.0) {
cRec.balance = newBalance;
writeCustomer(&cRec);
}
fClose(cFile);
}
이제 해당 파일에 대한 모든 책임은 updateCustomer 루틴에 있다. 루틴은 파일을 열고 (자신이 시작한 것을 끝맺으면서) 종료 전에 닫는다. 루틴은 파일의 사용을 균형잡는다. 열기와 닫기가 동일 장소에 있고, 모든 열기에 대해 상응하는 닫기가 있다는 것도 분명해 보인다.
중첩 할당
리소스 할당의 기본 패턴을 확장해서 한 번에 하나 이상의 리소스를 필요로 하는 루틴에 적용할 수 있다.
- 리소스를 할당한 순서의 반대로 해제하라. 이렇게 해야 한 리소스가 다른 리소스를 참조하는 경우에도 리소스를 고아로 만들지 않는다.
- 코드의 여러 곳에서 동일한 리소스 집합을 할당하는 경우, 할당 순서를 언제나 같게 하라. 교착(deadlock) 가능성이 줄어들 것이다. (프로세스 B가 resourse2를 이미 확보하고서 resource1을 획득하려고 하고 있는데 프로세스 A가 resource1을 가진 상태로 resource2를 막 요청하려고 한다면, 이 두 개의 프로세스는 영원히 기다리게 될 것이다.)
객체와 예외
할당과 해제의 균형은 클래스의 생성자(constructor)와 소멸자(destructor)를 생각나게 한다. 클래스는 하나의 리소스를 대표하며, 생성자는 그 리소스 타입의 특정 객체를 제공하고, 소멸자는 그것을 현 스코프에서 제거한다.
만약 객체지향 언어로 프로그래밍을 한다면, 리소스를 클래스 안에 캡슐화하는 것이 유용하다고 느낄 것이다. 특정 리소스 타입이 필요한 때마다 그 클래스의 객체를 생성하면 된다. 그 객체가 스코프를 벗어나거나 가비지 콜렉터가 객체를 수거해 가면 객체의 소멸자가 클래스로 감싸진 리소스를 해제한다.
균형과 예외
예외를 지원하는 언어는 리소스 해제에 복잡한 문제가 있을 수 있다. 예외가 던져진 경우, 그 예외 이전에 할당된 모든 것이 깨끗이 청소된다고 어떻게 보장할 수 있겠는가? 이는 어떤 언어인지에 따라 달라진다.
C++의 경우 try...catch 예외 메커니즘을 지원한다. 불행하게도 이 말은 예외를 잡은 다음 다시 던지는 루틴에서는 언제나 그 루틴에서 나가는 경로가 최소한 두 개는 존재한다는 이야기이다.
void doSomething(void) {
Node *n = new Node;
try {
// 무엇인가 함.
}
catch (...) {
delete n;
throw;
}
delete n;
}
우리가 생성한 노드가 해제되는 장소가 두 군데이다. 하나는 루틴이 정상적으로 나가는 경로에 있고, 다른 하나는 예외처리 장소에 있다. 이는 DRY 원칙에 위반이며, 언제 터질지 모르는 유지보수 문제이기도 하다.
하지만 우리는 C++의 작동방식을 이용할 수 있다. 지역 객체들은 자기를 둘러싼 블록에서 나갈 때 자동으로 파괴된다. 만약 상황이 허락한다면, 'n'을 포인터에서 스택에 놓이는 실제 Node 개체로 바꾸면 된다.
void doSomething1(void) {
Node n;
try {
// 무엇인가 함.
}
catch (...) {
throw;
}
}
이렇게 되면 예외가 생기든 그렇지 않든 Node 객체의 자동 파괴를 C++에 맡길 수 있다.
자바에서는 C++와 달리 게으른 방식의 자동 객체 삭제를 사용한다. 참조가 없는 객체들은 가비지 콜렉션의 후보가 되며, 만약 가비지 콜렉션이 그 객체들을 지우려고 하기만 한다면, 객체의 finalize 메서드가 호출될 것이다. 더 이상 대부분 메모리 누수 책임을 지지 않게 되어 개발자에게는 아주 편해진 일이지만, C++ 방식대로 자원을 청소하도록 구현하기는 어려워졌다.
자바 언어 설계자들은 이것을 보상하기 위한 기능 하나를 추가해두었다. finally 절이 그것이다. try 블록에 finally 절이 들어있다면, 그 절 안의 코드들은 try 블록 안의 코드가 한 문장이라도 실행되면 반드시 실행되도록 되어 있다. 예외가 던져지더라도 상관없다. finally 절 안의 코드는 반드시 실행된다. 이 말은 다음과 같은 코드로 리소스 사용의 균형을 잡을 수 있다는 뜻이다.
public void doSomething() throws IOException {
File tmpFile = new File(tmpFileName);
FileWrite tmp = new FileWriter(tmpFile);
try {
// 무슨 작업인가 한다.
}
finally {
tmpFile.delete();
}
}
'끄적끄적 > Book Review' 카테고리의 다른 글
[오웬의 서재] #10. 사울 레이터의 모든 것 (2018) (0) | 2022.01.29 |
---|---|
[오웬의 서재] #9. 뉴욕주민의 진짜 미국식 주식투자 (2020) Part 1. (0) | 2021.12.20 |
[오웬의 서재] #7. 생각이 너무 많은 서른살에게 (2021) (0) | 2021.08.19 |
[오웬의 서재] #6. 아직 멀었다는 말 (2020) (0) | 2021.06.11 |
[오웬의 서재] #5. (비울수록 사람을 더 채우는) 말그릇 (2017) (0) | 2021.05.28 |