로버트 L.글래스의 우리가 미처 알지 못한 S/W공학의 사실과 오해

간만에 정신이 맑아지고 현실과 미래가 뚜렷히 보이는 명쾌한 작품을 읽은 느낌이다. 왜 이 책이 그렇게 많은 사람들에게 회자되고 인용되었는지 알 수 있었다. 이 책은 소프트웨어가 세상에 등장한 이후부터 이 책이 쓰여진 시점까지 소프트웨어 분야에서 일어났던 모든 일들을 집약한 책이라고 설명할 수 있겠다. 우리가 소프트웨어를 개발하면서 만났던 수많은 일들, 경험적으로는 알고 있었으나 증명하기 어려웠던 문제들, 소프트웨어 개발 과정에서 겪었던 어려움과 그 이유들, 수많은 문제들 중에서 어떤 문제에 집중하는 것이 옳고 어떤 자세로 임해야 하는지 등에 대한 명료한 답을 제시해 준다.

이 글은 이 책(원 제목은 Facts and Fallacies of Software Engineering)에서 얻은 내용들을 정리한 것이다. 하지만 이 글만 읽어보고 책을 읽지 않게 되는 것은 내가 원하는 것과는 정 반대의 일이다. 내가 이 책을 누군가에게 소개한다면 다음과 같을 것이다. 이 책은 소프트웨어 개발과 관련된 모든 사람들이 읽어야 할 책이다. 소프트웨어를 직접 개발하는 사람이라면 꼭 읽어야 할 책이다. 이 책을 모르고 소프트웨어 개발을 해왔던 시간들이 너무 아깝다.


1장  관리

사람

사실 1. 소프트웨어 직업에서 가장 중요한 요소는 프로그래머가 사용하는 도구나 기술이 아니라, 프로그래머의 자질이다.

-       사람이 (소프트웨어 개발) 성공의 열쇠다

-       엄격한 방법론을 적용한 프로젝트를 한꺼풀 벗기고 프로젝트의 성공 이유를 물으면 그 답은 사람이다.

-       소프트웨어 생산성에 있어 가장 중요한 요소는 소프트웨어 실무자 개인의 역량이다.

사실 2 최고의 프로그래머는 최하의 프로그래머보다 28배 뛰어나다.

-       뛰어난 소프트웨어 실무자가 (동료들보다) 5배에서 28배까지 뛰어나다는 사실을 알 수 있다면, 가장 뛰어난 사람들을 잘 돌보는 것이 소프트웨어 관리자의 가장 중요한 업무라는 것은 자명한 일이다.

-       우리의 연구에서 가장 중요한 실질적 발견은 프로그래머 실무 능력의 현저한 개인차다

-       개인간에 5배 정도의 생산성 차이는 흔한 것이다.”

사실 3 지체된 프로젝트에 사람을 추가 투입하면 프로젝트가 더 늦어진다.

-       사람이 많을수록 커뮤니케이션은 더욱 복잡해진다. 따라서 프로젝트가 지연될 때 인력을 투입하면 프로젝트는 더 늦어지는 경향이 있다.

사실 4 작업환경은 생산성과 품질에 지대한 영향을 미친다.

-       프로젝트에서 상위 25%와 하위 25%에 대해서(이들간에는 2.6배 생산성 차이가 났다) 작업환경을 조사한 결과, 상위 그룹은 1.7배 넓은 공간에서 일했고, 충분히 조용한 환경이라고 대답한 비율이 2배 높았고, 개인 공간이라고 말한 비율은 3배 이상 높았으며, 전화를 돌리거나 꺼놓을 수 있다고 답한 비율은 각각 4배와 5배 많았다.


도구와 기술

사실 5 소프트웨어 업계에는 과대선전(도구와 기술에 대한)이 만연해 있다.

-       소프트웨어 기술 각각으로 얻을 수 있는 생산성 향상은 기껏해야 5~35% 정도이다.

-       재사용(공용화)으로 얻을 수 있는 이득도 10~35% 정도이다.

사실 6 새로운 도구와 기술은 도입 초기에 생산성/품질 저하를 초래한다.

-       도구와 기술을 익히는데는 적어도 6개월 ~ 2년 이상이 걸리고, 이 기간동안의 생산성은 기존보다 저하된다.

사실 7 소프트웨어 개발자는 도구에 대해 많은 말을 하지만, 별로 사용하지 않는다.

-       좋은 개발 도구라는 것 중 대다수는 계속 사용되지 않고 폐기된다.

추정

사실 8 폭주하는 프로젝트의 가장 흔한 원인 두 가지 중 하나는 부정확한 추정이다.

-       아직까지 소프트웨어 프로젝트가 걸리는 시간을 추정할 수 있는 정밀한 방법은 없다.

-       다만 분야의 전문가가 하는 추정만이 좀 더 정확할 뿐이다.

사실 9 소프트웨어 추정은 보통 부적절한 시기에 수행된다.

-       소프트웨어 추정은 보통 요구사항이 구체화 되기 전에 이루어진다.

-       많은 프로젝트가 이미 완료 기한이 정해진 상태로 시작된다.

사실 10 소프트웨어 추정은 보통 부적절한 사람들에 의해 수행된다.

-       많은 추정은 경영진이나 마케팅에 의해 결정된다.

사실 11 프로젝트가 진행되면서 소프트웨어 추정을 수정하는 경우는 거의 없다.

-       NASA에서는 소프트웨어 추정 재평가를 주창하며 라이프 사이클 상의 재평가 시점까지 정의한 연구가 있으나 권고를 따르는 사람을 본 적은 없다.

사실 12 소프트웨어 추정이 부정확한 것은 별로 놀라운 일이 아니다. 그러나 우리는 추정이 죽고 산다!

-       일정에 대한 추정이 부적절하다고 해도 프로젝트는 대부분 정해진 일정에 의해 관리된다.

사실 13 경영진과 프로그래머 사이에는 단절이 있다.

-       (경영진이 정한 추정에 맞지 않아) 실패라고 평가된 프로젝트에 대해 기술자의 다수가 가장 성공적인 프로젝트로 평가했다. 애초에 불가능했던 일정과 예산을 포기하고 도전적인 목표를 성공적으로 완료 했다고 생각 했기 때문이다.

사실 14 타당성 조사에 대한 대답은 항상 타당하다이다

-       프로젝트 시작 전에 이루어지는 기술적 타당성(구현 가능성을 검증하는) 조사는 거의 항상 타당하다는 결론을 낸다.

재사용

사실 15 소규모 재사용은 잘 해결된 문제다.

-       소규모 재사용(보통 라이브러리라고 불리는 함수 단위 재사용) 1950년대부터 있어 왔으며 이미 증명된 문제다.

사실 16 대규모 재사용은 여전히 해결되지 않은 어려운 문제다.

-       대규모 재사용(일명 공용화”)은 일반적으로 다양해지는 요구사항에 의해 제대로 활용되기 어렵다.

-       매우 좁은 도메인(예에서는 항공 역학) 내에서는 70%까지 재사용 모듈로 구축될 수 있었다.

사실 17 대규모 재사용은 서로 관련 있는 시스템 사이에서 가장 잘 적용된다.

-       대규모 재사용은 도메인 종속적이다.

-       여러 프로젝트나 여러 도메인에 걸쳐 적용하려 한다면 성공할 가능성이 거의 없다.

사실 18 재사용 가능 컴포넌트는 만들기가 3배 어렵고, 3곳에 적용해봐야 한다.

-       3이라는 숫자는 모두 경험적인 숫자이다. 다만 재사용 가능한 컴포넌트는 일반적인 것보다 만들기가 훨씬 어렵고, 잘 적용되는지를 여러 번 검증해 봐야 한다.

사실 19 재사용된 코드를 수정하는 것은 특히 오류를 범하기 쉽다.

-       기존의 솔루션을 이해하는 것은 소프트웨어 작업 중에서 가장 어려운 일이기 때문에, 재사용을 위해서 기존의 컴포넌트를 수정하는 것은 새로 만드는 것에 비해 매우 어렵다.

-       20~25% 이상을 수정해야 할 경우 처음부터 다시 만드는 것이 더 효율적/효과적이다.

-       소프트웨어 작업은 인류가 지금까지 해온 것 중 가장 복잡한 작업이다.”

사실 20 디자인 패턴 재사용은 코드 재사용 문제에 대한 해결책이다.

-       이 사실은 디자인 패턴과 같은 잘 알려진 설계에 대한 지식이 매우 중요함을 드러낸다.

-       구현된 컴포넌트는 재사용이 어렵지만, 소프트웨어의 구조 및 설계의 재사용은 가능하다.

복잡성

사실 21 문제의 복잡성이 25% 증가하면 솔루션의 복잡성은 100% 증가한다.

-       왜 그렇게 사람이 중요한가? 복잡성을 극복하는 데는 상당한 사고력과 기술이 필요하기 때문이다.

-       왜 대규모 재사용은 성과가 좋지 않을까? (도메인의 차이로 인한) 복잡성이 증대되기 때문이다.

-       왜 코드 리뷰(inspection)가 오류 제거에 대한 가장 효과적, 효율적인 접근 방법인가? 그 모든 복잡성을 걸러내고 오류의 위치를 찾는 데는 결국 사람의 노력이 필요하기 때문이다.

-       기존의 제품을 이해하는 것이 소프트웨어 유지보수에서 가장 중요하고도 어려운 작업인가? 하나의 문제를 해결하는데 적용할 수 있는 접근방법이 매우 많기 때문이다.

-       왜 소프트웨어에는 그렇게 많은 오류가 있는가? 처음부터 소프트웨어를 올바르게 이해하는 것은 매우 어렵기 때문이다.(여기에는 다른 견해가 있다. 소프트웨어는 불완전성의 원리에 지배 받기 때문에 사람의 두뇌가 아니면 오류를 잡아 낼 수 없다. 그리고 그 사람들은 모두 전문가인 것이 아니다. 물론 전문가라고 해서 오류를 만들어 내지 않는 것은 아니지만.)

사실 22 소프트웨어 작업의 80%가 지적인 작업이다. 그 중 상당 부분이 창조적인 작업이다. 사무적인 일은 거의 없다.

-       소프트웨어 실무자가 하는 일에 대한 관찰 연구 결과 80%는 지적인 작업, 20%는 사무적인 작업으로 분류되었다. 이 중 창조적인 작업은 6%에서 29%에 해당한다.

2장  생명 주기

요구사항

사실 23 폭주하는 프로젝트에서 가장 흔한 원인 두 가지 중 하나는 불안정한 요구사항이다.

-       고객과 사용자는 (그리고 대부분의 관리자들도) 보통 어떤 문제를 해결해야 하는지에 대해 확실하게 알지 못한다.

-       프로토타이핑은 요구사항이 명확하지 않을 때 자주 사용한다.(AOM 패턴을 이용해 볼만한 과정이다.)

사실 24 요구사항의 오류는 생산 단계에서 수정하는데 가장 비용이 많이 든다.

(너무 당연한 일이다)

사실 25 누락된 요구사항은 가장 수정하기 힘든 오류다.

-       누락된 요구사항은 이미 만들어졌거나 만들어지고 있는 코드에 대한 설계 수준의 수정을 요구한다.

설계

사실 26 명시적 요구사항을 설계로 옮겨갈 때 파생 요구사항이 폭발적으로 증가한다.

-       (명시적) 요구사항은 실제 어떻게 구현해야 할지를 결정하는 설계 단계에 오면 암시적 요구사항을 파생 시키고, 이 파생 요구사항은 명시적 요구사항의 50배에 달한다.

사실 27 소프트웨어 문제에서 최적의 솔루션이 하나 존재하는 경우는 거의 없다.

-       소프트웨어적인 문제는 문제에 대한 해결책이 매우 많다. 그 중에서 최상의 해결책은 없거나 알아내기가 매우 어렵다.

사실 28 설계는 복잡하고 반복적인 과정이다. 초기 설계 솔루션은 보통 잘못 되었거나, 최적이 아닌 경우가 많다.

-       (이는 애자일 진영의 XP, TDD, 리팩토링과 같은 기술 및 개발 방법론의 정당성에 힘을 실어 준다.)

-       전문 설계자는 설계상 핵심적인 문제에 대해 직접 구현해 보거나 솔루션을 찾아 놓은 다음에야 전체 설계 문제로 넘어간다.(이는 아키텍쳐는 코딩을 해야 한다는 주장을 뒷받침한다.)

코딩

사실 29 설계자의 기본단위와 프로그래머의 기본단위가 일치하는 경우는 거의 없다.

-       설계자의 코딩 경험, 전문 코딩 분야, 역량 등에 의해 설계 기본 단위의 크기가 달라진다. 이와 함께 코딩 실무자 역시 경험, 전문 분야, 역량에 따라 이에 대응되는 기본 단위가 달라지게 된다. 따라서 이 둘이 매칭되기는 어렵다.

-       (이것은 코딩 실무자의 능력이 설계자 수준과 비슷하게 뛰어나야 하고, 설계 수준이 낮거나 설계자가 없을 경우에는 코딩 실무자의 능력이 (설계를 커버할 수 있을 만큼) 매우 뛰어나야 한다는 것을 의미한다.)

-       나는 이 사실 때문에, 설계와 코딩 작업을 분리하는 것은 좋지 않다고 생각한다.”

-       소프트웨어 개발에서 전통적인 작업 분할 방식(설계자, 구현자, 테스터와 같이 전문 담당 업무를 분할하는 방식)은 적절하지 않다.”

사실 30 COBOL은 별로 훌륭한 언어가 아니지만, (비즈니스 데이터 처리에 대해서는) 다른 언어도 마찬가지다.

오류제거

사실 31 오류 제거는 생명주기에서 가장 많은 시간을 소모하는 단계다.

-       일반적으로 요구사항분석(20%) – 설계(20%) – 코딩(20%) – 오류 제거(40%)의 시간을 소모한다.

테스트

사실 32 프로그래머가 완전하게 테스트 했다고 믿는 소프트웨어도 보통은 로직 경로의 55~60%만 테스트 된 경우가 많다.

사실 33 100% 테스트 커버리지도 결코 충분하지 않다.

-       대략 소프트웨어 결함의 35%는 누락된 로직 경로(구현하지 않은 로직)에서, 40%는 로직 경로의 특정 조합을 실행할 때(로직 실행 후 다시 실행할 때 일부 변수가 초기화 되지 않아서 발생하는 오류가 대표적인 예이다) 나타난다. 따라서 100% 커버리지로도 잡히지 않는다.

사실 34 테스트 도구는 꼭 필요하지만, 많은 경우 거의 사용되지 않는다.

-       이유는 테스트 단계 자체가 관심을 많이 받지 못하기 때문이다. 자동화 테스트 도구는 매우 유용하다.

사실 35 특정 테스트 프로세스는 자동화할 수 있고, 또 자동화해야 한다. 그러나 자동화 할 수 없는 테스트 작업도 많다.

-       요구사항 명세서로부터 코드를 자동 생성할 수 있다는 개념은 이미 사라졌다. 따라서 프로그래머 없는 프로그래밍프로그래밍의 자동화에 대한 아이디어 역시 이미 사라졌다.(따라서 코더 개념 역시 사라져야 할 개념이다.)

-       (소프트웨어 개발 단계에서 가장 단순해 보이는) 테스트 조차도 완전히 자동화 할 수 있는 방법은 없다.(사실 모든 소프트웨어 개발 단계 중에서 자동화 할 수 있는 것은 없다.)

사실 36 프로그래머가 작성한 디버그 코드는 테스트 도구에 대한 중요 보완 수단이다.

-       컴파일러 옵션이나 외부 파일을 이용하여 테스트 코드를 실행할 수 있도록 만들어 놓는 것도 테스트에 도움이 된다.(이 책이 쓰여질 당시에는 Unit Test가 널리 보급된 상태가 아니었음을 상기하기 바란다.)

검토와 검사

사실 37 엄격한 검사는 첫 번째 테스트 케이스를 실행시키기도 전에 소프트웨어 제품에 포함된 오류의 90% 까지 제거할 수 있다.

-       (엄격한 검사란 inspection이라는 것으로 우리가 보통 이야기 하는 코드 리뷰이다)

-       (이는 인간의 두뇌만이 소프트웨어를 관리할 수 있는 유일한 도구라는 내 생각을 뒷받침한다)

-       이렇게 좋은 방법임에도 잘 실행되지 않는 것은 오류 검사 단계에 대한 무관심과 엄격한 검사 과정의 어려움(“이미 작성된 코드에 대한 이해가 가장 어려운 것이라는 점을 상기하기 바란다.) 때문이다.

-       (여기에 덧붙이자면 많은 관리자들이 코드리뷰나 페어 프로그래밍을 지적 유희라고 생각하거나 베테랑이 초보자에게 가르치기 위해 하는 일이라고 생각한다. 우리나라의 많은 관리자들은 프로그래밍을 저급한 업무로 취급하면서 모든 개발자가 1~2년쯤 지나면 다들 베테랑이라고 생각하기 때문에 페어 프로그래밍 하려는 사람들을 야단친다.)

사실 38 엄격한 검사도 테스트를 대체할 수는 없다.

사실 39 출시 후 검토(회고라 부르는 사람들도 있다)는 중요하지만, 거의 실행되지 않는다.

사실 40 검토는 기술적 측면과 사회학적 측면을 모두 가지는데, 어느 쪽도 무시하면 안 된다.

-      동료 검토(peer review)에 의해 이성과 감정에 상처를 받지 않도록 해야 한다.

-      비자아적 프로그래밍(코드에 자신의 자존심을 투영하지 않는 것)을 당부하지만 대부분의 개발자들은 자신의 코드에 대해 자부심을 가지려고 한다. (이는 미묘한 문제이면서도 코드를 통한 커뮤니케이션이나 형상 관리를 방해하는 문제이기도 하다. 자신 있는 코드라면 공개하는 것이 마땅하고, 자신 없는 코드라면 공개하고 조언을 받아야 하는 것이 마땅하다. 어느 편이든 코드는 공개되어야 한다.)

사실 41 유지보수는 보통 소프트웨어 비용의 40~80%를 차지한다. 따라서, 유지보수는 소프트웨어 생명주기 중 가장 중요한 단계일 것이다.

-       (일단 다음 절에서 유지 보수란 단순한 오류 수정이 아니라 기능의 개선 및 추가까지 포함한 작업이라는 것을 기억하기 바란다.)

-       유지 보수 비용이 높은 이유는 이미 만들어진 기능을 이해하기가 어렵기 때문이다.

-       (따라서 가독성 높고 간결한 코드를 만드는 것이 개발자의 자질 중 가장 중요한 것이다.)

사실 42 유지보수 비용의 60%는 개선 작업에 소요되는 비용이다.

-       소프트웨어를 처음 개발할 때 고객과 사용자는 새로운 소프트웨어로 무엇을 할 수 있을지에 대해 단지 비전의 일부만 갖고 있을 뿐이다. 소프트웨어가 출시되어 한동안 사용해 본 후에야 사용자는 그 소프트웨어 시스템을 개선해 무엇을 더 할 수 있을지 깨닫기 시작한다.

-       60/60 법칙 : 소프트웨어 비용의 60%는 유지보수에 사용되며, 유지보수 비용의 60%는 개선에 사용된다. 따라서 기존 소프트웨어를 개선하는 것은 큰 일이다.

-       60%는 개선 17%는 오류 수정 18%는 포팅, 5%는 기타 작업

사실 43 유지보수는 문제가 아니라 해결책이다.

(다른 산업 분야와는 달리) 소프트웨어의 유지보수는 대부분 기능의 개선을 위한 것이기 때문에 문제가 아니라 해결책이다. 따라서 부정적으로 보지 않아야 한다.

사실 44 유지보수에서 가장 어려운 작업은 기존 시스템을 이해하는 것이다.

-       유지보수 작업에서 가장 중요한 요소는 이해력이다 –Ned Chapin, 유지보수 분야 개척자-

-       소프트웨어 업무 중에서 가장 어려운 일이 유지보수 작업이다. 보통은 (내가 만든 게 아닌) 다른 사람이 만든 소프트웨어를 다루어야 하기 때문이다.

사실 45 더 좋은 소프트웨어 공학 기술로 개발하면 더 많은(더 적은 게 아니라) 유지보수가 필요하다.

-       현대적 개발 방법론 및 소프트웨어 기술이 적용된 소프트웨어는 더 수정하기 쉽기 때문에 더 많은 수정이 가해지고 더 오랫동안 개선되면서 사용된다.

3장  품질

품질

사실 46 품질은 속성의 집합이다.

-       이식성, 신뢰성, 효율, 사용편의성, 테스트 용이성, 이해 용이성, 수정 용이성

사실 47 품질은 사용자 만족, 요구사항 충족, 비용과 일정 목표 달성, 또는 신뢰성이 아니다.

신뢰성

사실 48 대부분의 프로그래머가 흔히 범하는 오류가 있다.

-       인간은 하나씩 밀린 인덱스, 정의/참조의 불일치, 중요한 설계 항목 누락, 자주 사용하는 변수에 대한 초기화 실패, 일련의 조건 중 하나의 조건 누락 등 특정 종류의 작업에서 쉽게 실수한다.

사실 49 오류는 뭉치는 경향이 있다.

-       오류의 반이 모듈의 15%에서 발견된다.

-       오류의 80%가 단지 모듈의 20% 이내에서 발견된다.

-       대략 80%의 결함이 모듈의 20%에서 나오고, 모듈의 절반 정도는 오류가 없다.

-       특정 모듈이 특히 더 어렵기 때문에, 여러 개발자가 모듈 별로 개발하기 때문에(개인 능력차 때문에) 그럴 것이다.

사실 50 소프트웨어 오류 제거에 있어서 단 하나의 최상의 방법은 없다.

사실 51 오류는 항상 남아 있다. 심각한 오류를 제거하거나 최소화하는 것이 목표가 돼야 한다.

-       오류 없는 소프트웨어를 개발하는 것은 불가능하다.

-       CMM 레벨 4인 팀과 다른 정형방법을 사용하는 팀 둘이서 충분한 비용과 일정에도 불구하고 98% 신뢰성의 간단한 제품을 만들어 내지 못했다.

효율

사실 52 효율은 훌륭한 코딩보다는 훌륭한 설계에 더 많은 영향을 받는다.

사실 53 고급 언어 코드도 어셈블리어 코드의 90%에 가까운 효율을 낼 수 있다.

-       항공 애플리케이션에서 고급 언어의 비효율성(어셈블리 대비 C언어) 10~20% 정도이다.

>  최적화 컴파일러를 이용하면 10% 성능향상

> 튜닝을 통해 2~5% 더 향상 시킬 수 있다.

사실 54 크기와 속도 사이에는 트레이드오프가 있다.

4장  연구

연구

사실 55 많은 연구자들이 연구보다는 옹호에 치중한다.

 

2부 오해 5+5

5장  관리

관리

오해 1 측정할 수 없는 것은 관리할 수 없다.

-       측정할 수 없는 것은 통제할 수 없다는 말은 사실이지만 관리할 수 없다는 말은 아니다. 소프트웨어 설계라는 것은 측정 불가능하지만 관리할 수 있는 대상이다.

-       몇몇 회사에서는 메트릭을 통한 관리를 중요시한다. 그리고 자주 사용하는 소프트웨어 메트릭이 개발되어 사용되고 있다.

-       (우리나라에서 메트릭을 통해 소프트웨어를 관리하지 않는 이유는 관리자들의 소프트웨어에 대한 이해가 부족하고, 필요한 경우 외주를 통해 해결할 수 있는 수준의 저급한 업무로 폄하되고 있기 때문이다. 야구나 농구에서 데이터를 관리하거나 기업의 재무제표가 관리되는 이유와 정반대 이유다.)

오해 2 소프트웨어 품질은 관리로 해결할 수 있다.

-       소프트웨어 품질에는 기술적 요소가 많아 관리만으로 해결할 수는 없다.(이는 소프트웨어를 외주로 개발하는 행위에 대해 경종을 울린다. 자체 기술 부족으로 인하여 기술이 검증된 업체를 통해 개발하고 그 기술을 내제화 하기 위한 외주가 아니라 단지 시간이 부족하거나 허드레 업무라고 생각해서 외주를 주는 경우 관리만으로 품질을 확보하는 것은 불가능하다. 이로 인한 짐은 고스란히 개발자에게 넘겨진다.)

오해 3 프로그래밍은 비자아적이 될 수 있고, 또 되어야 한다.

-       (많은 개발자들은 자신의 코드가 자신의 지적 능력을 대변한다고 생각한다. 그래서 코드에 대해 자아를 투영하곤 한다. 하지만 이러한 자세는 원활한 시스템 통합, 오류 검사, 형상 관리를 방해한다.)

-       오류 없는 프로그램을 작성하는 것은 불가능하다는 것을 인정하고, 기술적 약점이 잘 발견될 수 있도록 열린 자세를 가져야 한다.

도구와 기술

오해 4 도구와 기술 : 한 가지로 모든 문제를 해결할 수 있다.

-       소프트웨어 문제를 해결할 완전한 한가지 기술이나 도구는 없다.

오해 5 소프트웨어 분야에는 더 많은 방법론이 필요하다.

-       많은 방법론이 교수들이나 대학원생 등 소프트웨어 비 실무자들에 의해 개발된다. 특히 엄격한 방법론(융통성을 거부하고 전체 개발 프로세스를 감시하려는 방법론, 예를 들어 전통 waterfall과 같은 방법론)을 경계해야 한다.

추정

오해 6 비용과 일정을 추정하기 위해서는 먼저 LOC(Lines of Code)를 추정해야 한다.

-       LOC 만으로는 부정확한 메트릭이다.(프로그래밍 언어, 도메인, 주석과 같은 요소를 고려해야 한다.)

-       (특히 이 절은 비용과 일정을 추정하기 위해 LOC추정하는 문제에 대해 언급하고 있음을 기억해야 한다. 추정을 위해서 부정확한 메트릭을 사용하는 것에 대한 주의이다.)

-       (LOC는 몇몇 부수적인 메트릭과 함께 사용되면 훌륭하게 기능할 수 있다. 특히 코드의 조건문 빈도, 중복 코드 비율, 모듈 특성 등과 함께 사용한다면 개발자의 생산성을 판단하는데 매우 유용할 수 있다. 같은 모듈이나 계층을 개발하는 개발자들간에도 LOC 10배 이상 차이 나는 경우가 흔하다. 2배 이하라면 큰 의미는 없다.)

6. 생명주기

테스트

오해 7 랜덤 테스트 입력은 테스트를 최적화 하는 좋은 방법이다.

-       소프트웨어의 복잡성이 늘어나면 늘어날수록 랜덤 테스트를 통해 찾아낼 수 있는 오류는 줄어든다.

검토

오해 8 “보는 눈이 많으면, 모든 버그는 그 깊이가 얕다.”

-       오픈소스에 대한 이야기인데, 많은 오픈소스가 다수의 눈을 거쳐 수정되는 과정을 거치지 않고, 수정되는 버그는 대부분 발견하기 용이한 것들이다. 중요하고 심각한 버그들은 여전히 숨어 있을 가능성이 있다.

유지보수

오해 9 과거의 비용 데이터를 살펴봄으로써 미래의 유지보수 비용을 예측할 수 있고 시스템 교체 결정을 내릴 수 있다.

-       다양한 연구가 있으나 (기본적으로 소프트웨어의 유지 보수가 대부분 새로운 기능을 추가하는 일이고, 이것은 이미 매우 어려운 일로 알려져 있으므로) 유지보수 비용을 예측하는 것은 매우 어려운 일이다. 따라서 이를 기반으로 시스템 교체 결정을 내리는 것은 불가능하다.

7장.  교육

테스트

오해 10 프로그래밍을 가르칠 때 프로그램을 어떻게 작성하는지 보여주며 가르친다.

-       잘 만들어진 코드를 읽어 보는 것은 직접 작성하는 것만큼(혹은 그보다 더) 중요하다. 하지만 잘 만들어진 코드 샘플을 찾아 내는 것은 어렵다.

-       다른 사람의 코드를 읽는 것은 소프트웨어 개발에서 가장 어려운 작업을 훈련할 수 있는 일이다.

-       (페어 프로그래밍이나 코드 리뷰에 힘을 실어 주는 사실이다. 다른 사람의 코드를 보지 않거나 고쳐보지 않은 개발자, 잘 만들어진 오픈소스나 라이브러리 코드를 보지 않는 사람의 개발 능력은 언제나 낮기 마련이다.)

Posted by 이세영2
,

 dynamicJarLoader.zip


라이브러리에 대한 동적 로딩은 사용자의 요구가 다양하게 변화하는 어플리케이션을 위한 기술이다. Java에서는 URLClassLoader라는 라이브러리 클래스를 통해서 이 기능을 지원하고 있다.

이를 이용할 상황을 하나 생각해 보도록 하자.

화면에 도형을 그리는 어플리케이션을 만들어 사용자에게 배포하였다고 가정하자. 이미 도형에 대한 클래스 모델링이 잘 되어 있기 때문에 Shape이라는 인터페이스는 이미 배포되어 있다. 그리고 몇몇 도형들 역시 배포된 상태다. 소프트웨어 내부에서는 이미 배포된 jar 파일을 통해 도형들을 생성해서 사용하고 있다.

이 상황에서 고객이 새로운 도형을 만들어 달라고 요청해 왔다. 필요한 도형은 Circle과 Triangle이다. 이 파일들은 개발자에 의해서 개발되었고 사용자에게 배포되었다. 이 때 이미 동작 중인 어플리케이션은 종료할 수 없다. 이런 경우에 어플리케이션을 중단시키지 않아도 클래스들을 로드할 수 있는 동적 로더가 활용될 수 있다.

아래 클래스 다이어그램을 보자.

위에서 설명했듯이 Shape 인터페이스는 이미 배포된 상태이다. 여기서 사용자가 원하는 Circle과 Triangle을 각각의 이름으로 된 jar 파일을 배포하였다.

여기서 사용자가 어플리케이션을 종료하지 않고 이들 jar 파일들을 로드하려면 동적 Jar 로드 기능이 탑재되어 있어야 한다. 이를 구현한 것이 DynamicJarLoader 클래스이다.


DynamicJarLoader 클래스 설명

이 클래스는 객체 생성자를 통해서 Jar 파일이 들어 있는 폴더 경로(String jarPath)를 입력 받는다.

이후 load(jarFileName : String) 메소드를 통해서 Jar 파일 이름을 입력해주면 라이브러리가 로드된다.

특히 DynamicJarLoader 클래스는 내부에 있는 loaderMap을 통해서 여러 이름의 라이브러리를 동시에 로드할 수 있다. 따라서 이 객체 하나 만으로도 여러 라이브러리 파일을 로드하고, 로드된 라이브러리들을 통해서 객체를 생성할 수 있다. 리턴되는 boolean 값은 라이브러리 로드가 성공했는지 여부를 알려준다.


unload(jarFileName : String) 메소드는 이미 로드된 라이브러리를 메모리에서 내리거나, 새로운 버전의 라이브러리를 열기 위해서 기존에 이미 열려진 라이브러리를 닫아야 할 경우에 호출하는 메소드이다.


이미 라이브러리가 로드되어 있다면 열린 라이브러리를 통해서 객체를 새로 생성할 수 있어야 한다. newInstance() 메소드는 클래스 이름(className)을 통해서 객체를 로드할 수 있도록 구현된 메소드이다. newInstance() 메소드는 두 개가 있는데, 하나는 className만으로 객체를 생성하도록 하고, 나머지 하나는 jar 파일 이름까지 입력해서 보다 정확한 객체를 생성하도록 한다.


첨부파일

맨 위쪽에 첨부된 첨부파일은 shape.jar / circle.jar / triangle.jar 파일과 DynamicJarLoader 클래스, 그리고 이들 라이브러리 동적 로딩을 테스트해 볼 수 있는  Use 클래스를 포함하고 있는 eclipse 프로젝트 파일이다.(JDK 1.7 이상을 사용하기 바란다.)


아래는 DynamicJarLoader 클래스의 소스이다.

public class DynamicJarLoader {

    private String jarPath;

    private Map<String, URLClassLoader> loaderMap = new HashMap<String, URLClassLoader>();

    public DynamicJarLoader(String jarPath){

        this.jarPath = jarPath;

        this.jarPath.replaceAll("\\\\", "/");

        if(this.jarPath.endsWith("/") == false) this.jarPath = this.jarPath + "/";

    }

    public boolean load(String jarFileName){

        if(loaderMap.containsKey(jarFileName) == true) unload(jarFileName);

        String jarFilePath = jarPath + jarFileName;

        File jarFile = new File(jarFilePath);

        try {

            URL classURL = new URL("jar:" + jarFile.toURI().toURL() + "!/");

            URLClassLoader classLoader = new URLClassLoader(new URL [] {classURL});

            loaderMap.put(jarFileName, classLoader);

            return true;

        } catch (MalformedURLException e) {

            return false;

        }

    }

    public boolean unload(String jarFileName){

        URLClassLoader loader = loaderMap.get(jarFileName);

        if(loader == null) return true;

        try {

            loader.close();

            return true;

        } catch (IOException e) {

            return false;

        }

        finally{

            loaderMap.remove(jarFileName);

        }

    }

    public Object newInstance(String jarFileName, String className){

        URLClassLoader loader = loaderMap.get(jarFileName);

        if(loader == null) return true;

        try {

            Class<?> clazz = loader.loadClass(className);

            return clazz.newInstance();

        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {

            return null;

        }

    }

    public Object newInstance(String className){

        for(String each : loaderMap.keySet()){

            Object object = newInstance(each, className);

            if(object != null) return object;

        }

        return null;

    }

}


Posted by 이세영2
,

*이 문제는 Programming Challenges : 알고리즘 트레이닝 북에 출제된 문제입니다.


지뢰 찾기(Minesweeper)

이 문제는 문제 자체가 어렵진 않지만 코드를 깔끔하게 작성하는 연습을 하기에는 좋은 문제이다. 코드를 깔끔하게 작성하기 좋은 팁은 다음과 같다.


1. 지뢰 찾기 맵을 char[][] map; 로 만들고, 지뢰가 아닌 곳은 아스키 문자 '0'을 넣는다.

    이렇게 하면 주위에 지뢰가 있을 때 map[i][j] ++를 호출해주면 자동으로 아스키 문자가 '1', '2', '3'.... 순서로 1씩 증가 된다.


2. 지뢰를 만나서 주변 8개 지뢰에 대한 카운트를 증가 시킬 때 해야 할 일을 함수로 빼낸다.

    "지뢰 주변"을 탐색하면서 맵을 벗어난 곳을 체크하기가 수월해진다.


3. 카운트 증가 조건에 맞으면 업데이트 하지 말고 조건에 맞지 않을 경우 리턴한다.

    리턴문(return)을 잘 활용하면 로직이 복잡해지는 것을 막을 수 있다.


아래는 직접 구현한 지뢰 찾기 소스이다.

class MineSweeper{

    Input input = new Input();

    private char[][]map;

    int n = 0;// row

    int m = 0;// column

    int iteration = 0;

    public void run(){

        while(true){

            readRowColumn();        // row(n)와 column(m) 값을 입력 받는다.

            if(isExit()) return;    // n == m == 0 이면 리턴한다.

            initMap();              // 맵을 생성한다.

            loadMap();              // 맵을 입력 받는다.

            calculateMap();         // 맵을 계산한다.

            printResult();          // 맵을 출력한다.

        }

    }

    private boolean isExit(){

        if(n == 0 && m == 0) return true;

        return false;

    }

    private void readRowColumn(){

        String line = input.readLine();

        StringTokenizer token = new StringTokenizer(line);

        n = Integer.parseInt(token.nextToken());// row

        m = Integer.parseInt(token.nextToken());// column

    }

    private void initMap(){

        map = new char[n][m];

    }

    private void loadMap(){

        for(int i = 0; i < n; i++){

        String line = input.readLine();

        for(int j = 0; j < m; j++){

        map[i][j] = line.charAt(j);

        if(map[i][j] == '.') map[i][j] = '0';

        }

        }

    }

    private void calculateMap(){

        for(int i = 0; i < n; i++){

            for(int j = 0; j < m; j++){

                if(map[i][j] == '*') addCountAround(i, j);

            }

        }

    }

    private void addCountAround(int x, int y){

        for(int i = x - 1; i <= x + 1; i++){

            for(int j = y - 1; j <= y + 1; j++){

                addCount(i, j);

            }

        }

    }

    private void addCount(int x, int y){

        if(x < 0 || x >= n) return;

        if(y < 0 || y >= m) return;

        if(map[x][y] == '*') return;

        map[x][y]++;

    }

    private void printResult(){

        iteration++;

        if(iteration != 1) System.out.println();

        System.out.println("Field #" + iteration + ":");

        for(int i = 0; i < n; i++){

            for(int j = 0; j < m; j++){

                System.out.print(map[i][j]);

            }

            System.out.println();

        }

    }

}


이 구현에서 핵심 부분은 calculateMap() 메소드와 이 메소드에 호출되는 addCountAround() 그리고 addCount() 메소드일 것이다.

calculateMap()은 가로와 세로 방향으로 for문을 돌아서 모든 맵을 방문하는 일을 담당한다. 만약 이 과정에서 지뢰(*)를 만나면 addCountAround(i, j) 메소드를 호출하는 것으로 자기 임무를 마친다.

addCountAround()는 지뢰 주변의 9개 타일(자기 자신 포함) 타일들을 방문한다. 그리고 addCount() 메소드를 호출하는 것으로 자기 임무를 마친다.

addCount() 메소드가 실질적으로 카운트를 증가시킨다. 이 때 중요한 것은 예외 처리이다. 만약 타일이 지뢰이거나 맵의 경계를 넘어갔다면 카운트를 증가시키면 안된다. 따라서 미리 예외의 경우들을 체크하고 적절한 타일이 아니라면 리턴한다. 그리고 정상적인 타일인 경우에면 카운트를 증가시킨다.


또한 run() 메소드에서는 문제에서 주어진 절차를 모두 메소드로 만들고 절차에 맞는 적절한 이름을 할당해 줌으로써 전체 동작이 어떤지를 한눈에 알 수 있도록 만들어 주는 것이 코드를 깔끔하게 만들 수 있는 비결이다.


중간에 Input 클래스를 사용하는데 이에 대해서는 [Programming Challenges]Sample 및 표준 입력 데이터 이중화 글을 참고하기 바란다.


Posted by 이세영2
,

programmingChallenges.zip


당분간 알고리즘 공부에 전념해보고자 전에 사서 킵 해 두었던 Programming Challenges 알고리즘 트레이닝 북을 꺼내 들었다.


이 책은 여러 알고리즘 문제를 난이도별, 특성 별로 나누어 제공하고 있고, 이에 대한 해답도 소스로 첨부되어 있다. 특히 온라인 페이지를 통해서 자기가 작성한 답안을 올리고 채점을 받을 수 있도록 되어 있어서 좋다. 또한 로컬에서 테스트를 해 볼 수 있도록 샘플 입력 데이터들도 제공한다. 아래는 온라인 채점 사이트이다.


http://www.programming-challenges.com/pg.php?page=index


Sample 및 표준 입력 데이터 이중화

위에서 설명한 것처럼 온라인으로도 채점이 가능하고 로컬에다가 샘플 데이터를 받아서 테스트를 해 볼 수 도 있다.

문제는 온라인에서 채점할 경우 내가 제출한 소스로 동작한 결과를 확인해 볼 수 없다는데 있다. 따라서 로컬에서 샘플 데이터를 통한 테스트가 필수이다. 하지만 샘플 데이터는 파일 입력이고, 온라인에서 소스에 데이터를 입력 받을 때는 표준 입출력(일반적으로 키보드 입력이라 부르는)이다. 따라서 온라인에 답안을 올릴 때와 샘플 데이터를 이용할 때의 입력 방식이 달라서 불편한 점이 있다.


자동으로 Sample 데이터와 표준 입력을 전환

위의 첨부 파일을 받아 보면 이미 Programming Challenges 사이트에서 제공하는 샘플 데이터가 Samples 폴더에 들어 있다.

자동으로 Sample 데이터와 표준 입력을 자동으로 전환하는 방법은 단순하다. 로컬에 샘플을 저장한 폴더에서 원하는 샘플 파일을 열어보고 예외가 발생하면 표준 입력을 사용하도록 하는 것이다.

이에 대한 소스는 아래와 같다.

class Input{

    private Scanner scan;

    private boolean isSampleSource = true;

    public Input(){ initInputSource(); }

    private void initInputSource(){

        Problem problem = new Problem();

        File file = new File("samples/" + problem.pcId +".inp");

        try {

            scan = new Scanner(file);

        } catch (FileNotFoundException e) {

            isSampleSource = false;

        }

    }

    public String readLine(){

        if(isSampleSource) return readLineFromSample();

        return Main.ReadLn(255);

    }

    private String readLineFromSample(){

        if(scan.hasNextLine()) return scan.nextLine();

        return null;

    }

}

class Problem{

    public String pcId = "110101";// PC ID

    public String uvaId = "100";// UVa ID

} 

Input 클래스는 생성시 생성자에서 initInputSource() 메소드를 호출한다. 이 메소드에서는 일단 Problem 클래스를 생성한 후 그 안에서 pcId 값을 읽어 파일명으로 변환한 후 samples 폴더에서 파일을 열어본다. 만약 파일이 열리면 파일을 입력 소스로 사용한다. 만약 파일을 여는데 실패하면 FileNotFoundException이 발생하고 isSampleSource 값을 false로 함으로써 표준 입출력을 사용하도록 되어 있다.


사용법

첨부 파일은 Eclipse 프로젝트이다. samples 폴더에는 샘플 데이터들이 들어 있다.

programmingChallenges.submitForm 패키지 아래에 있는 Main.java 파일은 위의 소스를 포함하여 알고리즘을 작성할 부분과 필요한 클래스를 작성해 넣을 수 있는 부분을 비워 둔 소스이다. 따라서 이 소스에 알고리즘을 작성해 넣으면 된다. 이 때 소스 파일 지정을 위해서 Problem 클래스 안에 pcId는 책에 나오는 PC Id를 입력해 주어야 한다.


programmingChallenges.threeNPlusOne 패키지 아래에 있는 Main.java 파일은 책에 나오는 첫번째 문제인 3n + 1 문제를 풀어 채워 넣은 파일이다. 실제로 채점 사이트에 올려 보고 해결되는 것을 확인해 본 소스이다. 물론 로컬에서 실행을 시켜 보면 로컬의 samples 폴더에 있는 테스트 데이터를 입력 받아 동작함을 알 수 있다.


혹시 Programming Challenges 알고리즘 트레이닝 북을 가지고 알고리즘을 공부하는 사람 중에서 Java를 사용할 의향이 있다면 편리하게 사용할 수 있을 것이다.

'7.알고리즘' 카테고리의 다른 글

[Programming Challenges] 지뢰 찾기(Minesweeper)  (0) 2016.10.06
Posted by 이세영2
,

TDA 원칙이라고도 불린다.

우리 말로 번역해 보자면 "물어보지 말고 그냥 시켜라"가 될 수 있다.

이는 객체와 객체가 협력하는 경우, 다른 객체에게 정보를 요구하지 말고 그냥 행위하도록 시키라는 의미이다. 즉 정보 은닉의 중요성을 강조하는 원칙이라고 할 수 있겠다.


정보를 처리하는 소프트웨어 구현의 경제성 관점에서 보면 이렇다.

소프트웨어의 복잡성은 다루어야 할 정보의 양에 영향을 받는다. 다루어야 할 정보가 많아지면 더 많은 정보를 가공해야 하고, 정보의 값에 의한 제어 변경(보통 상태라고 부른다)이 더 자주 발생하게 된다. 

하지만 꼭 정보의 양만 복잡도를 가늠하는 척도는 아니다. 정보를 처리하는 단계가 짧고, 정보를 다루어야 할 객체가 적고, 정보에 대한 처리 과정을 중복되지 않고 간결하게 처리한다면 같은 데이터를 처리하더라도 훨씬 단순한 소프트웨어를 만들 수 있을 것이다. 이 과정에서 TDA 원칙의 중요성이 떠오르게 된다. 정보를 입수했을 때 그 정보를 한정적인 범위 내에서만 다루도록 하고(예를 들어 단일 객체), 혹시 외부에서 그 정보에 기반하여 동작을 수행해야 할 경우에는 정보를 가지고 있는 쪽에 동작을 요청하도록 하면 넓은 범위에서 데이터를 입수하여 처리하는 방식에 비해서 훨씬 복잡도가 줄어들게 될 것이다.


아래 그림을 살펴보자.




이 그림에서 데이터는 최초로 객체1에 전달된다. 그림 상에서는 객체1이 받은 데이터를 객체2와 객체3에 주고, 객체3은 이를 다시 객체 4에 준다. 이렇게 데이터가 전달되는 방식은 크게 두 가지가 있다.

1. getter를 통해 데이터를 요청하는 경우.

2. 다른 객체의 API에 데이터를 인자로 넣게 되어 있는 경우.


Tell, don't ask라는 것은 1번에 해당하는 말이다. 즉, 데이터를 getter로 요청하지 말 것을 의미한다. 하지만 데이터를 전파하는 방법은 2도 해당하므로 이 두가지 경우가 발생하지 않도록 설계해야 한다.

자, 그림 상에서 1번이든 2번이든 어떤 방식으로든 데이터를 전달하도록 설계했다고 하자. 그러면 저 그림의 모든 객체들은 데이터의 값에 영향을 받게 된다. 그것이 조건문으로 나타나든, 변수로만 나타나든 어떻게든 코드 상에 모습을 드러내게 된다. 이것은 다음과 같은 문제들을 발생시킨다.


코드가 복잡해진다

데이터를 가지고 있으면 데이터를 핸들링 해야 한다. 핸들링하는 코드는 단순히 전달하거나 저장하는데만 그치는 경우도 있고, 데이터 값의 범위에 따라서 조건문이나 제어문이 필요한 경우도 있다. 어떤 식으로든 코드가 늘어나면 문제가 생기는 것은 당연하다.


데이터의 변경에 다수의 객체가 영향을 받는다

일단 데이터를 가지고 있으면 더이상 쓸모없는 데이터여서 지우거나, 새로운 데이터가 더해지거나 데이터의 타입이 변경되는 등의 여러가지 변경사항에 영향을 받게 된다. 이는 OCP(Open Close Principle) 원칙을 위배하게 된다.


데이터의 무결성을 지키기 어렵다 

각 객체들이 가지고 있는 데이터 값이 시간에 따라서 달라짐으로써 관리가 어려워지게 된다. 특히 멀티 쓰레드 환경에서 여러 곳에 데이터가 흩어져 있으면 데이터의 무결성을 지키기는 더더욱 어렵고 복잡해진다.


중복 코드가 발생할 가능성이 높다

한가지 데이터는 보통 소프트웨어 전체에서 한가지 용도로 사용된다. 따라서 하나의 데이터를 다루는 코드들은 유사성이 매우 높다. 이 코드들은 애초에 한번만 작성되도록 만들어져야 하는데 데이터가 여러 객체로 전달되고 나면 중복 코드가 발생하는 것은 거의 필연적이다.


그렇다면 어떻게 이 문제를 해결할 수 있는지 한번 살펴보자.


간단히 이야기 하자면, 데이터가 입력된 이후에는 데이터를 핸들링하는 객체를 별도로 생성하여 관리하면 된다. 위 그림에서는 데이터 객체가 이에 해당한다. 객체1은 데이터를 받아서 데이터 객체를 생성하거나 데이터 객체에 전달해 주는 것으로 자기 일을 마친다. 그리고 기존에 데이터를 가지고 다루던 객체들은 모두 데이터 객체에게 일을 시키는( tell() ) 형태로 설계를 변경한다. 그러면 데이터와 관련된 모든 일은 데이터 객체가 수행하게 되면서 다른 객체들이 데이터에 의존하는 것을 막을 수 있다.

이를 단계별로 설명해보면 다음과 같다.


데이터를 수신(생성) 단계

소프트웨어의 어떤 부분이든 데이터를 수신하거나 생성해 낸 곳이 있기 마련이다. 데이터는 발생 시점부터가 중요하다. 데이터를 최초로 수신한 객체는 일단 다른 곳으로 데이터를 전파시킬 수 없어야 한다. getter를 통해서 다른 객체들이 데이터를 가지고 가게 하거나, 다른 객체의 매개변수로 데이터를 전송해서는 안된다.


데이터 객체 생성 단계

데이터를 처리할 객체를 생성한다. 데이터는 생성된 이후에는 오직 이 객체에게만 전달된다. 데이터를 처리하는 방식은 두가지가 있다.

1. 데이터 처리를 전담하는 객체가 있다. 이 경우라면 최초로 데이터를 수신한 객체는 처리 객체에 데이터를 넘겨주기만 하면 된다.

dataProcessor.receiveData(data);

2. 데이터 처리를 전담하는 객체가 없고, 여러 객체들이 데이터에 대한 의존성을 가지고 있는 경우가 있다. 이런 경우에는 데이터 객체를 (필요시) 생성하고, 이 데이터 객체를 다른 객체에 전달하여 준다. 이 객체는 데이터가 변경되었을 때 다른 객체를 어떻게 변경시켜야 하는지를 알고 있다. 이렇게 데이터를 객체화 하여 전달하는 것은 State 패턴이나 Strategy 패턴과 유사한 모양이 된다.

// 데이터 객체가 생성된다

Data dataObject = new Data(data);

// 처리를 위해 다른 객체에 전달된다.

object.receiveData(dataObject);


데이터 처리 단계

데이터 객체 생성 단계에서 방법이 두가지가 있듯이 처리 방식도 두가지이다.

1. 데이터 처리 전담 객체의 경우에는 다른 객체들이 수시로 데이터 갱신이 이루어 졌는지를 데이터 처리 전담 객체에게 물어보는 방식이 있고, 다른 객체들이 데이터 처리 객체에 자신을 이벤트 리시버로 등록하는 경우가 있다. 후자는 Observer 패턴과 유사하다.

2. 데이터 객체가 생성되서 다른 객체로 전송되어 오면 각 객체들은 이 데이터 객체를 이용하여 변경된 데이터에 의한 동작을 수행해야 한다. 이 때 각 객체들은 데이터를 모르기 때문에 직접 자기 자신의 행위를 변경할 수는 없다. 따라서 데이터 객체에게 자신의 상태를 변경해 달라고 요청해야 한다. 따라서 이를 실행하면 아래와 같은 형식의 코드가 된다.

// 데이터 객체를 수신한 쪽 : Object2라고 가정했을 때

dataObject.doubleDispatch(this);


// 데이터 객체 쪽

public void doubleDispatch(Object2 object){

    object.doSomething();

}

이러한 방식을 켄트 벡의 구현 패턴에서는 "더블 디스패치"라고 한다. 데이터를 알고 있는 쪽에서 데이터에 종속적으로 동작하는 객체를 넘겨 받아서 자기가 알고 있는 데이터를 기반으로 넘겨 받은 객체의 행위를 호출하는 것이다. 이렇게 하면 데이터를 넘겨주지 않고도 데이터가 넘어 갔을 때 일어나야 하는 행위를 호출할 수 있게 된다.


"정보"의 전달을 금지하는 원칙

기본적으로 정보 은닉(information hiding)은 단순히 캡슐화만을 의미하는 것이 아니다. 정보 은닉(information hiding)에 대한 올바른 이해에서도 이야기 했듯이 생성된 객체의 구체적인 타입을 숨기는 것이나 구현을 숨기는 것도 정보 은닉에 해당된다. 또한 아무리 캡슐화를 잘 했다고 해도 getter를 통해서 데이터를 전달하거나 매개변수로 데이터를 다른 객체에 넘겨버리면 기껏 정보 은닉을 강조한 보람이 없어진다.

정보 은닉은 데이터의 종류를 막론하고 데이터 처리를 수행하는 전담 객체가 아니면 어떠한 객체도 데이터를 전달해주지 않아야 한다는 원칙으로 해석해야 한다. 이 정보에는 외부에서 받은 데이터도 포함되지만 생성한 객체의 구체적인 타입이나 구현부와 같이 프로그래밍 요소의 정보도 포함이 된다. 그리고 이런 정보들은 생성과 동시에 은닉 됨으로써 정보에 의존하는 코드들의 생성을 막아야 한다. 이것이 Tell, don't ask 원칙과 정보 은닉 원칙이 추구하는 방향이다.

Posted by 이세영2
,

Adaptive Object Model(이하 AOM) 패턴은 간단히 이야기 하자면 동적 객체 생성 패턴이다. 그래서 Dynamic Object Model 패턴이라고도 불린다.

이 패턴은 몇 개의 다른 패턴들이 결합되어 생겨난 패턴이다. 이에 대한 자세한 설명은 넥스트리(Nextree)의 블로그에서 확인할 수 있다.(http://www.nextree.co.kr/p2469/)

그리고 이 패턴을 응용하여 다양한 시도들이 이루어지고 있는데 이에 대한 자료는 Adaptive Object Model 패턴 공식 홈페이지(http://adaptiveobjectmodel.com/)에서 확인할 수 있다.

이 패턴은 실제 제품에도 활용된 사례가 있다. Tridium 사에서 개발한 Niagara Platform이라는 빌딩용 네트워크 시스템에 탑재되는 Niagara 소프트웨어가 이 패턴을 통해 구현되어 있다.(https://www.niagara-community.com/Comm_Home)


이 패턴은 객체의 동적 선언과 동적 생성이라는 두가지 특성을 모두 지원해주는 패턴이다. 그 자체가 매우 다이나믹한 특성을 가지고 있고, 이에 따라 다양한 파생 구조들이 생겨날 수 있다. 여기에서는 가장 기본이 되는 형태인 동적 선언과 동적 생성에 대해서 초점을 맞춰 보고자 한다.


만약 어떤 어플리케이션이 매우 다양한 대상을 취급한다고 가정하자. 특히 공장이나 빌딩, 대규모 상업 단지와 같이 각종 설비들이 설치될 수 있고, 이에 대한 통합적인 관리가 필요하다고 가정하자. 이런 경우에는 특히 기존에 잘 사용하던 장비를 다른 회사의 장비로 교체할 수도 있고, 새로운 장비들을 추가할 수도 있다. 이런 경우 새로운 장치는 기존에 통합 관리 시스템이 만들어질 당시에는 존재하지 않는 것일 수도 있다.

이런 장비들을 소프트웨어로 매핑하기 위해서는 수많은 장비들을 포용할 수 있는 객체를 만들어야 한다. 즉 각 장비들은 그와 대응되는 객체가 정의 되어 있어야만 정확하게 정보를 얻고 저장할 수 있다. 하지만 이미 이야기 했다 시피 장비의 종류는 너무나도 다양하고, 매번 새로운 장비들이 개발되게 되어 있다. 이런 상황에서는 아무리 객체 설계를 잘 했다고 해도 언젠가는 그 객체로는 장비 정보를 제대로 표현할 수 없게 될 것이다.

또한 현장에서의 요구사항은 항상 바뀌게 마련이다. 현장마다 장비의 종류가 각각 다르다. 그런 현장들을 지원하기 위해 매번 소프트웨어를 다시 컴파일 할 수는 없는 노릇이다. 즉, 소프트웨어가 동작하는 동안에도 새로운 장비들의 정보를 보여줄 객체를 생성할 수 있어야 하며, 이 객체는 수 많은 장치들을 모두 커버할 수 있는 객체여야 한다.

즉 문제의 핵심은 어떻게 실시간에 새롭게 정의된 객체를 생성할 수 있느냐 하는 것이다.


실세계의 장치를 지원할 객체를 디자인 한다고 하면 우리는 다음과 같은 방식들을 사용할 수 있다.

- 우선 공통된 정보들을 모아서 상위 클래스로 선언한다.(예를 들어 모든 종류의 장치를 나타내는 Device 클래스를 선언한다.)

- 그리고 상위 클래스의 속성들을 선언한다.

- 개별 디바이스 정보를 담을 하위 클래스를 선언한다. 당연히 상위 클래스의 속성들을 상속 받아야 한다.

- 하위 클래스의 속성을 선언한다.

- 장치들은 모두 하위 장치들을 포함할 수 있다. 따라서 속성 중에는 다른 장치가 포함될 수도 있다. (일종의 부품이나 참조 객체와 같은 성격이다.)


이런 방식은 우리가 객체지향 언어들을 통해서 일반적으로 장치에 대한 클래스를 선언할 때 사용하는 모델링 방식이다. 따라서 동적으로 객체를 생성할 수 있으려면, 위에서 이야기 한 상속 개념, 참조 개념을 모두 구현할 수 있어야만 한다. AOM 패턴은 상속이나 참조 개념들을 포함한 동적 객체를 모사하여 생성해 줄 수 있는 패턴이다.


AOM 패턴의 클래스 다이어그램


AOM 패턴은 TypeSquare라는 사각형 형태의 다이어그램을 가지게 된다.

이 다이어그램에서 왼쪽은 객체화되고 시간에 따라서 가변적인 정보들을 담는 객체이다. 오른쪽 두 클래스는 타입에 관한 정보를 가지고 있게 된다. 오른쪽 클래스들이 가진 정보는 일반적으로 변경되지 않는다.

우선 Klass와 KlassType에 대해 알아보자.

이 둘의 관계는 객체지향 프로그래밍에서 말하는 객체(Klass) - 클래스(KlassType)의 관계와 같다고 생각하면 된다. 우선 KlassType은 클래스명에 해당하는 name을 가지고 있다. 그리고 클래스가 가질 수 있는 상위 클래스는 parent라는 변수를 통해 가지고 있게 된다. 당연히 parent도 KlassType 객체이다. 그리고 실제 클래스는 속성(attribute)을 선언하게 된다. 이 속성에 대한 선언이 AttributeType이고, KlassType은 이를 소유하고 있다.

Klass는 실제 객체로 생성되었을 때 가지는 정보들을 선언한 클래스이다. Klass 클래스를 기반으로 객체가 생성될 때에는 무조건 한 개의 KlassType 객체를 인자로 받아야 한다. 일단 생성 이후에는 객체의 타입이 바뀌길 원하지는 않기 때문이다. 이는 마치 클래스를 기반으로 객체를 생성하는 과정과 같다. Klass 객체는 KlassType에 선언된 AttributeType을 기반으로 Attribute 객체를 생성한다.

Attribute와 AttributeType 간의 관계는 클래스의 속성 선언과 객체가 가진 속성의 관계와 같다. AttributeType이란 우리가 속성을 선언할 때 int data; 와 같이 선언하는 것처럼 우선 속성의 타입을 나타내는 typeClass 변수를 가져야 한다. 이 다이어그램에서는 이 typeClass를 Java의 클래스 객체로 선언하여 가지고 있다. 다음으로 속성의 이름에 해당하는 name 을 가지고 있어야 한다. description은 이 속성에 대한 설명을 담는 변수이다.

Attribute는 AttributeType을 기반으로 생성되는 객체이다. 실제 객체에서 데이터를 담을 수 있는 공간으로 이해할 수 있다. 따라서 자신의 타입 정보인 AttributeType 객체를 하나 가지고 있어야 하고, 데이터를 담을 value 객체를 가지고 있어야 한다. 이 Attribute는 범용적으로 쓰일 수 있어야 하기 때문에 value는 Object 타입이다.


이제 AOM 패턴의 구현을 살펴보자.


KlassType 클래스

class KlassType{

    // 클래스 정보에 해당한다.   

    KlassType parent;

   

    public KlassType(String name, KlassType parent){

        this.name = name;

        this.parent = parent;

        addParentAttributeTypes();

    }

   

    private void addParentAttributeTypes(){

        if(parent == null) return;

        Collection<AttributeType> parentAttributeTypes = parent.getAttributeTypes();

        for(AttributeType each : parentAttributeTypes){

            attributeTypes.put(each.getName(), each);

        }

    }

   

    Map<String, AttributeType> attributeTypes = new HashMap<String, AttributeType>();


        public void addAttributeType(AttributeType attributeType){   

            attributeTypes.put(attributeType.getName(), attributeType); 

        }


        public AttributeType getAttributeType(String typeName){ 

            return attributeTypes.get(typeName); 

        }


        public Collection<AttributeType> getAttributeTypes() {

            return attributeTypes.values();

        }

       

    private String name;

        public String getName(){ return name; }   

}

KlassType은 객체지향 언어에서 클래스를 선언하는 것과 대응되는 클래스이다. 클래스는 클래스 이름과 상속받은 부모 클래스 명, 내부에 선언된 속성들로 구성된다.(행위는 AOM 패턴과 연관된 다른 패턴들에 의해 구현된다. 이 예제에서는 일단 배제되어 있다.)

- parent : 부모 클래스

- name : 클래스명

- attributeTypes : 선언된 속성들


맨처음 KlassType이 객체로 선언되었을 때에는 아무런 속성타입(AttributeType)을 가지고 있지 않으므로 이를 추가해 줄 수 있도록 addAttributeType()이라는 메소드를 제공해야 한다. 또한 생성자에서 부모 클래스를 나타내는 객체를 받았다면 부모가 선언한 AttributeType들도 상속 받아야 한다. 따라서 addPrentAttributeTypes() 메소드를 생성자에서 호출하게 된다.


Klass 클래스

class Klass{

    // 객체에 해당한다.

    public Klass(KlassType type, String name, String id){

        this.type = type;

        this.name = name;

        this.id = id;

        initAttributes();

    }

   

    private void initAttributes(){

        for(AttributeType attributeType : type.getAttributeTypes()){

            attributes.put(attributeType.getName(), new Attribute(attributeType));

        }

    }

   

    private KlassType type;

        public KlassType getType(KlassType type){ return type; }

   

    String name;

        public String getName(){ return name; }

   

    String id;

        public String getId(){ return id; }   

    Map<String, Attribute> attributes = new HashMap<String, Attribute>();

        public Object get(String name){

            Attribute attr = attributes.get(name);

            if(attr != null) return attr.getValue();

            else throwNoSuchAttributeException(name);

            return null;

        }


        public void set(String name, Object value){

            Attribute attr = attributes.get(name);

            if(attr != null) attr.setValue(value);

            else throwNoSuchAttributeException(name);

        }


        public Attribute getAttribute(String name){

            Attribute attr = attributes.get(name);

            if(attr != null) return attr;

            else throwNoSuchAttributeException(name);

            return null;

        }

   

    public String toIndentString(String indent){

        StringBuffer buffer = new StringBuffer();

        buffer.append(indent + "Class " + type.getName() + " " + name);

        if(type.parent != null) buffer.append(" extends " + type.parent.getName());

        buffer.append("{\n");

        for(Attribute each : attributes.values()){

            if(each.getValue() instanceof Klass){

                Klass inner = (Klass)each.getValue();

                buffer.append(indent + "   " + each.getType().getTypeClassName() + " " + each.getType().getName() + " = " + inner.toIndentString(indent + "   ") + ";\n");

            }

            else{

                buffer.append(indent + "   " + each.getType().getTypeClassName() + " " + each.getType().getName() + " = " + each.getValue() + ";\n");

            }

        }

        buffer.append(indent + "}");

        return buffer.toString();

    }

   

    private void throwNoSuchAttributeException(String attributeName){

        try {

            throw new NoSuchAttributeException();

        } catch (NoSuchAttributeException e) {

            System.out.println("Class \"" + name + "\" has no such attribute : \"" + attributeName + "\n");

            e.printStackTrace();

        }

    }

} 

Klass 클래스는 생성 이후에 객체 역할을 수행하는 클래스이다. 객체는 객체로서의 이름과 아이덴티티, 그리고 타입에 대한 정보 및 속성 값들을 가지고 있어야 한다.

- type : 객체의 타입 정보(AttributeType)

- name : 객체의 이름(변수명이라고 이해하면 된다.)

- id : 객체와 객체를 유일하게 구분해주는 구분자 역할. 구현에서는 UUID를 문자열화 해서 사용한다.

- attributes : 속성들을 저장한 Map. 속성명(변수명)을 키로 하여 Attribute를 저장하는 Map이다.


각 Klass 객체는 별도의 속성을 가지고 있어야 한다. 따라서 Klass 객체가 생성되면 우선 KlassType을 인자로 받은 다음, 이 KlassType에 선언되어 있는 속성 타입 정보(attributeTypes)를 얻어온다. 그리고 그 정보를 기반으로 초기화 되지 않은 Attribute 객체를 만들고 이를 attributes 맵에 저장한다.

객체는 외부에서 자신에 접근하는 연산들을 제공해 주어야 한다. 따라서 저장된 속성값을 제공해주는 get() 메소드와 속상 값을 설정할 수 있게 해주는 set() 메소드를 제공해준다.

추후에 테스트를 통해서 의도한 대로 Klass 객체가 잘 만들어졌는지 확인하기 위해서 toIndentString() 메소드도 구현해 주었다. 이 메소드는 마치 우리가 클래스를 코딩했을 때와 유사하게 내부 속성들과 클래스명, 그리고 상속 받은 클래스의 정보들을 표시하도록 되어 있다.


AttributeType 클래스

class AttributeType{

    // 클래스에 속성(변수)을 선언하면 int data; 와 같이 선언한다. 이 정보를 이 클래스가 가지고 있어야 한다.

    Map<String, KlassType> klassTypes = new HashMap<String, KlassType>();


    @SuppressWarnings("rawtypes")

    private Class typeClass;

    @SuppressWarnings("rawtypes")

    public Class getTypeClass(){ return typeClass; }

    public String getTypeClassName(){

        String typeName = typeClass.getName();

        typeName = typeName.substring(typeName.lastIndexOf(".") + 1);

        return typeName;

    }

   

    @SuppressWarnings("rawtypes")

    public AttributeType(Class typeClass, String name, String description){

        this.typeClass = typeClass;

        this.name = name;

    }

   

    private String name;

    public String getName() { return name; }

   

    private String description;

    public String getDescription(){ return description; }

}

AttributeType 클래스는 속성의 선언에 해당하는 클래스이다.

- typeClass : 속성의 타입을 나타낸다. 이 구현에서는 클래스 객체를 이용하고 있다.

- name : 속성의 명칭을 나타내는 정보이다.

- description : 이 속성에 대한 설명을 넣는 변수이다.


이 클래스는 생성자를 통해서 위의 정보들을 받아 저장한다. 이 정보들은 이 후 Attribute 객체를 생성하기 위한 정보로 활용된다.


Attribute 클래스

class Attribute{

    // Attribute = 속성, 필드. attributType value를 가져야 한다.

   

    private AttributeType type;

   

    public Attribute(AttributeType type){

        this.type = type;

    }

   

    public AttributeType getType(){ return type; }

       

    private Object value;

    public Object getValue() { return value; }

    public void setValue(Object value) {

        if(isSettable == false){

            throwOperationNotSupportException();

            return;

        }

        this.value = value;

    }

   

    private void throwOperationNotSupportException(){

        try { throw new OperationNotSupportedException(); }

        catch (OperationNotSupportedException e) {

            System.err.println("Attribute " + type.getName() + " is immutable.");

            e.printStackTrace();

        }

    }

   

    private boolean isSettable = true;

    public boolean isSettable(){ return isSettable; }

    public void setSettable(boolean isSettable){ this.isSettable = isSettable; }

} 

Attribute 클래스는 실제 객체에서의 속성을 나타낸다. 따라서 Attribute는 자신이 저장할 속성 값에 대한 타입 정보를 가지고 있어야 하고, 속성 값을 함께 가지고 있어야 한다.

- type : AttributeType

- value : 속성 값을 저장하는 변수


이 속성은 외부에서 조회가 가능하고, 값이 변경되면 저장이 가능해야 한다. 따라서 getValue() 메소드와 setValue() 메소드를 통해 이런 연산들을 지원하고 있다.


이제 이 클래스들을 통해서 새로운 클래스를 선언하고 객체를 생성하는 과정을 살펴보도록 하자.


실행 함수

public static void main(String[] args) {

    KlassType site = new KlassType("Site", null);

    site.addAttributeType(new AttributeType(String.class, "position", "위치"));

    KlassType house = new KlassType("House", site);

    house.addAttributeType(new AttributeType(String.class, "owner", "소유주"));

    house.addAttributeType(new AttributeType(Integer.class, "area", "면적"));

    house.addAttributeType(new AttributeType(Klass.class, "car", "차량"));

    Klass myHouse = new Klass(house, "우리집", UUID.randomUUID().toString());

    myHouse.set("position", "사당동");

    myHouse.set("owner", "홍길동");

    myHouse.set("area", "30");

    KlassType car = new KlassType("Car", null);

    car.addAttributeType(new AttributeType(String.class, "model", "모델"));

    car.addAttributeType(new AttributeType(Integer.class, "hp", "마력"));

    car.addAttributeType(new AttributeType(String.class, "type", "종류"));

    Klass myCar = new Klass(car, "내차", UUID.randomUUID().toString());

    myCar.set("model", "아우디");

    myCar.set("hp", "500마력");

    myCar.set("type", "세단");

    myHouse.set("car", myCar);

    System.out.println(myHouse.toIndentString(""));

}


이 테스트 코드들이 의도하는 바는 다음과 같다.

우선 Site 클래스를 선언한다. Site 클래스는 각종 위치를 나타내기 위한 상위 클래스 역할이 된다.

House는 Site 클래스를 상속 받아 구현되는 클래스이다.

myHouse는 House의 객체(인스턴스)이다.

Car 클래스는 자동차를 나타내는 클래스이다.

myCar는 Car 클래스의 객체(인스턴스)이다.

myCar는 myHouse 내부에 선언된 속성으로 들어가게 된다. 즉 참조 객체가 된다.


설명이 좀 많긴 하지만 실행해보면 쉽게 알 수 있다. 출력은 다음과 같다.


출력

Class House 우리집 extends Site{

   String owner = 홍길동;

   Integer area = 30평;

   Klass car =    Class Car 내차{

      Integer hp = 500마력;

      String model = 아우디;

      String type = 세단;

   };

   String position = 사당동;

}

출력된 내용은 마치 소스코드와도 같다. 사실 AOM 패턴이 해결하고자 하는 문제가 딱 이런 것이다. 우리는 마치 새로운 클래스를 선언하고 이를 생성해 낸 것과 같은 효과를 얻었다. 이 과정에서 상속이나 참조 객체와 같이 객체지향 언어로 모델링을 할 경우 필요한 특성들까지도 역시 동일하게 확보할 수 있게 되었다.

이러한 특성은 또한 데이터베이스 설계에도 매우 도움이 된다. 실행 함수에서 선언한 정보들을 각각 테이블로 만들어주면 Klass / KlassType / Attribute / AttributeType과 매핑되는 4개의 테이블 만으로도 원하는 기능들을 모두 구현할 수 있다.

'5.디자인패턴' 카테고리의 다른 글

Actor Model 패턴의 구현(Java)  (0) 2016.09.30
Property List 패턴  (0) 2016.09.24
Mediator 패턴  (0) 2016.09.18
Facade 패턴  (0) 2016.09.18
Command 패턴  (0) 2016.09.18
Posted by 이세영2
,