•
구현에 대한 설계 작업은 단순히 상세 부분을 작성하기 전 의사코드로 클래스 인터페이스를 작성하는 것일수도 있고 클래스간 관계의 다이어그램을 그려보는 것일 수 있다.
•
설계는 매우 광범위한 주제라서 이 장에서는 그 일부만 다룬다. 훌륭한 클래스와 루틴 설계의 상당 부분까지 다루기에 너무 커서 떨어져 나갔다. 이들은 다음, 다다음장에 알아본다.
1. 설계의 어려움
•
개요
◦
소프트웨어 설계는 컴퓨터 소프트웨어에 대한 명세를 동작 가능한 소프트웨어로 변환하기 위한 계획에 대한 구상이나 창작, 도구를 의미한다.
◦
설계는 요구사항을 코드 작성과 디버깅에 연결하는 작업이다. 훌륭한 상위 수준 설계는 여러 개의 하위 수준 설계를 무리 없이 담을 수 있는 구조를 제공한다.
◦
훌륭한 설게는 규모가 큰 프로젝트에선 꼭 필요한 작업이며 작은 프로젝트에선 유용한 작업이다.
◦
이 절에서는 설계가 가지고 있는 어려움을 통해 설계가 가지는 특징들을 알아보자.
•
설계는 불명확한 문제다.
◦
호스트 리텔과 멜빈 웨버는 불명확한 문제의 정의를 전체 혹은 일부를 해결해야만 정의할 수 있는 문제라고 했다.
▪
본질적으로 역설적인 구석이 있는 이 문장은 문제를 명확하게 정의하려면 문제의 답을 한 번 내려보아야 하며 작동하는 솔루션을 만들기 위해 다시 문제를 해결해야 한다는 의미를 가지고 있다.
▪
이러한 프로세스는 소프트웨어 개발에서 수십 년 동안 전해져온 오랜 관습이다.
◦
불명확한 문제의 가장 극단적인 예로 무너진 타코마 다리 설계 사례를 예로 들 수 있다.
▪
다리를 지을 당시 가장 중요하게 고려했던 고려 사항이 무게를 견디기 위한 견고함을 유지하는 것이었으나 바람이 예상보다 많이 불어 요동치다 결국 무너지게 되었다.
◦
이는 불명확한 문제 규명의 대표적인 예라고 할 수 있다. 왜냐면 다리가 붕괴될 때까지 엔지니어는 공기 역학을 그 범위까지 적용해야하는지 알지 못했기 때문이다.
▪
결국 다리를 한 번 지어보고서야 공기역학도 추가로 고려해야한다는 사실을 배웠다.
◦
코딩테스트를 풀어내는 것과 개발자로써 프로그램을 만드는 것의 주요한 차이점은 코딩테스트는 문제점이 명확하다는 점이다.
•
설계는 엉성한 프로세스다(결과는 정돈되었을지라도)
◦
완성된 소프트웨어 설계는 정돈되어 있고 깔끔해 보여야 하지만 설계 과정은 최종 결과물만큼 깔끔하지 않다.
◦
설계하면서 잘못된 길로 들어서는 경우도 많고 실수도 많이 하기 때문에 설계는 뭔가 엉성하다.
▪
사실 실수를 하는 것이 설계의 핵심이다. 설계 단계에서 실수하고 고치는 것이 똑같은 실수를 코드 작성 후에 발견하여 코드를 수정하는 것보다 비용이 적다.
◦
또한 설계는 충분하다라는 그 기준선을 알기 어렵기 때문에 엉성해보이기도 한다.
▪
설계는 어디까지 구체적이어야 할까? 설계를 공식 설계 표기법을 얼만큼 적용해 작성하고 얼마 정도를 코드 작성의 몫으로 남겨두어야 할까?
◦
설계를 언제 끝낼 것인가? 설계는 완성되는 시점이 분명하지 않으므로 이 질문의 가장 흔하면서도 적절한 대답은 최대한 정보를 얻고 더이상 선택을 미룰 수 없을 때다.
•
설계는 절충과 우선순위의 문제다.
◦
이상적인 세계라면 모든 시스템이 즉각적으로 실행되어 요청을 처리하고 저장 공간따윈 필요없으며 비용같은 건 생각하지 않아도 될 것이다.
◦
현실 세계는 냉혹하다. 설계자의 주요 업무는 서로 상충하는 설계 특징을 비교하여 그 특성들 사이의 균형을 맞추는 일이다.
▪
개발 일정이 좀 더디더라도 응답 속도가 중요하다면 설계자는 그에 적합한 설계를 선택할 것이다.
▪
개발 일정을 빠르게 처리하는 것이 더 중요하다면 좋은 디자이너는 그 목적에 맞게 설계를 변경할 것이다.
•
설계에는 제약이 따른다.
◦
설계의 핵심은 어느 정도는 가능성을 만들고 어느 정도는 가능성을 제한하는 것에 있다.
▪
물리적으로 구조물을 짓는데 필요한 시간과 자원, 공간이 무한정 존재한다면 꿈만 같을 것이다.
◦
결국 자원들은 제한적이기 때문에 좀 더 간결하고 공학적인 해결방안을 선택할 수 밖에 없고 그 결과 비용측면에서는 더 나은 해결책이 나온다.
◦
소프트웨어의 설계의 목표도 마찬가지다.
•
설계는 비결정적이다.
◦
세 사람에게 같은 문제의 해결책을 설계해보도록 시켜보면 서로 완전히 다른 설계를 내놓을 것이다. 그렇지만 모두 쓸만할 것이다.
◦
어떤 목적을 달성하는 방법은 한 가지 이상일 수 있지만 컴퓨터 프로그램을 설계하는 방법은 보통 수십 가지에 이른다.
•
설계는 발견적 학습 과정이다.
◦
설계는 비결정적이기 때문에 설계 기법은 예상된 결과를 만들어내는 반복적인 처리 과정이라기보다는 발견적 학습이라고 할 수 있다.
◦
설계에는 시행착오가 따른다. 불명확한 문제기 때문에, 어떤 작업에서 효과가 있던 설계 툴이나 기법이 다음 프로젝트에서는 무용지물일 수도 있다. 은탄환은 없다.
•
설계는 창발적이다.
◦
설계의 이런 특성들은 설계가 창발적이라는 말로 깔끔하게 요약할 수 있다.
◦
설계는 누군가의 머릿속에서 완전한 형태를 갖추고 짠하고 등장하지 않는다. 설계는 설계 검토와 격식없거나 있는 토론, 코드 작성 경험, 코드 수정 경험을 통해 진화하고 발전한다.
◦
실질적으로 모든 시스템은 초기 개발 과정에서 어느 정도의 설계 변경을 의무적으로 거치게 되고 그 후 보통 애초에 예상했던 것보다 훨씬 많은 내용이 변경된다.
◦
이 변경사항을 어느정도까지 수용할 것인가는 만드는 소프트웨어의 종류에 따라 달라진다.
2. 핵심 설계 개념
•
개요
◦
핵심 개념을 잘 이해하고 있어야 훌륭한 설계가 가능하다.
◦
이 절에선 복잡성 관리, 바람직한 설계의 특징, 설계 수준에 대해서 설명한다.
•
소프트웨어의 주요 기술적 의무: 복잡성 관리
◦
본질적 어려움과 비본질적 어려움
▪
No Silver Bullets: Essence and Accidents of Software Engineering 논문을 작성한 브룩스는 소프트웨어 개발이 어려운 이유는 본질적이고 우연에 기인하는 서로 다른 종류의 문제 때문이라고 주장했다.
▪
먼저 브룩스는 아리스토텔레스 시절의 철학에서 두 용어를 가져왔다.
▪
먼저 철학에서 본질적인 속성이란 어떠한 사물이 그러한 사물이 되기 위해서 반드시 가져야하는 속성이다.
•
예를 들어 어떠한 물건을 자동차라고 부를 수 있으려면 엔진, 바퀴, 문이 있어야 한다. 이들 중 하나라도 빠지면 그것은 자동차라고 부를 수 없다.
▪
다음으로 비본질적인 속성은 특정 사물을 지칭할 때, 그것이 무엇이라고 결정하는 데 영향을 끼치지 않은 우연의 산물이다.
•
4기통 터보 엔진 V8 자동차든 다른 엔진이 장착된 자동차든 엔진의 종류와 상관없이 그것은 자동차다.
•
자동차는 문이 2개, 4개일 수 있고 바퀴는 스키니, 매그일 수 있다. 이와 같은 구체적이며 세부적인 사항들은 모두 비본질적인 속성이다.
•
비본질적인 속성은 부수적이고 임의적이며 추가적이고 우연한 것으로 생각할 수도 있다.
브룩스는 소프트웨어에서의 중요한 비본질적인 어려움이 오래 전에 해결되었음을 발견했다.
브룩스는 소프트웨어에 남아 있는 본질적인 어려움은 느리게 해결될 수 밖에 없다고 주장한다.
소프트웨어 개발이 갖는 본질적인 어려움은 복잡하고 무질서한 현실 세계와 상호작용하고 종속 관계와 예외 상황들을 정확하고 완벽하게 규명해야 하며 대충이 아니라 정확한 솔루션을 설계해야하기 때문이다.
▪
이 모든 본질적인 어려움의 근원은 본질적이고 비본질적인 복잡성 때문이다.
◦
복잡성 관리의 중요성
▪
만약 프로젝트가 기술적인 이유로 실패한 경우에는 그 원인을 복잡성 관리 부족에서 찾을 수 있다.
▪
소프트웨어는 무슨 일이 일어나는지 아무도 모를 정도로 그 복잡성이 증가할 수 있다.
•
프로젝트에서 한 영역의 코드를 변경했을 때 다른 부분에 어떤 영향을 미칠 것인지 완벽하게 이해하는 사람이 하나도 없다면 더 이상 개발을 할 수 없는 지경에 이른 것이다.
▪
복잡성 관리는 소프트웨어 개발에서 가장 중요한 기술적 주제다.
▪
다익스트라는 현대 컴퓨터 프로그램을 보관할 수 있을 정도로 큰 두뇌를 가진 사람은 존재하지 않다고 지적했다.
•
이 말은 곧 소프트웨어 개발자가 전체 프로그램을 억지로 머릿속에 밀어 넣으려고 해서는 안 된다는 뜻이다.
•
따라서 한 번에 한 부분을 제대로 집중할 수 있게 프로그램을 구성하도록 노력해야 한다.
•
목표는 한 번에 생각해야 하는 프로그램의 크기를 최소화하는 것이다.
▪
소프트웨어 아키텍처 수준에서는 시스템을 서브시스템으로 나누어 문제의 복잡성을 줄인다.
•
인간은 하나의 복잡한 부분보다 여러 개의 간단한 정보를 더 빠르게 이해한다.
•
모든 소프트웨어 설계 기법의 목표는 복잡한 문제를 간단한 문제 여러개로 만드는 것이다.
⇒ 어디서 본것 같지 않나요
▪
서브 시스템이 독립적일수록 한 번에 복잡한 부분의 일부에 집중하기 더 쉽다. 객체를 신중하게 정의함으로써 한 번에 하나의 기능에만 집중할 수 있도록 작업을 나눈다.
•
루틴을 짧게 유지하면 신경 쓸 부분이 줄어든다. 저수준의 상세 구현보다는 문제 범위면에서 프로그램을 작성하고 추상성을 최대화하여 작업하면 뇌에 부담이 줄어든다.
▪
결론적으로 인간의 선천적인 한계를 보완할 줄 아는 개발자는 자신뿐만 아니라 다른 사람도 이해하기 쉽고 오류가 적은 코드를 작성한다.
◦
복잡성을 해결하는 방법
▪
생산성이 떨어지는 설계는 다음과 같은 상황에서 발생한다.
•
간단한 문제를 복잡하게 해결할 때
•
복잡한 문제를 단순하고 잘못된 방법으로 해결할 때
•
복잡한 문제를 부적절하고 복잡하게 해결할 때
▪
오늘날의 소프트웨어는 태생적으로 복잡할 수 밖에 없고 현실 세계의 문제가 가지고 있는 본질적인 복잡성을 피할 수 없다.
▪
다음은 복잡성을 관리하는 두 가지 방법이다.
•
두뇌가 한 번에 처리해야 하는 본질적인 복잡성의 양을 최소화하라.
•
비본질적인 복합성이 불필요하게 증가하지 않도록 하라.
▪
일단 소프트웨어에서 복잡성을 관리하는 것이 중요함을 알고 있자.
◦
바람직한 설계의 특징
▪
복잡성 최소화
•
설계의 일차적인 목표는 먼저 복잡성을 최소화하는 것이어야 한다.
•
간단하고 이해하기 쉬운 설계를 만들어라. 특정한 부분을 살펴보고 있을 때, 개념적으로 전혀 상관없는 다른 영역도 살펴보아야 한다면 이는 좋지 않은 설계다.
▪
유지보수의 편리함
•
다시보아도 읽기 쉽다는 설계라는 것이다.
•
내 코드를 처음 보는 신입 개발자가 물어볼 만한 질문을 계속 떠올려 본다. 내 시스템이 그에게 어렵지 않게 쉽게 이애할 수 있도록 설계한다.
▪
느슨한 결합
•
느슨한 결합은 프로그램의 각 연결을 최소화하도록 설계하는 것을 의미한다.
•
클래스 사이의 연결을 최소화하기 위해 인터페이스를 이용해 추상화, 캡슐화, 정보 은닉과 같은 방법을 사용한다. 연결을 최소화하면 통합, 테스트, 유지보수 시 비용을 최소화할 수 있다.
▪
확장성
•
확장성이란 내부 구조를 변경하지 않고 시스템의 기능이 확장될 수 있다는 뜻이다.
•
다른 부분에 영향을 미치지 않고 시스템 일부분을 변경할 수 있어야 한다.
▪
재사용성
•
재사용성은 현재 시스템의 일부를 다른 시스템에 사용할 수 있도록 시스템을 설계하는 것을 의미한다.
▪
높은 팬인
•
높은 팬인은 특정 클래스를 사용하는 클래스의 수가 많다는 것을 의미한다.
•
높은 팬인은 시스템이 유틸리티 클래스를 잘 활용하게끔 설계되었다는 것을 의미한다.
▪
낮은 팬아웃
•
낮은 팬아웃은 특정 클래스가 다른 클래스를 적게 사용한다는 것을 의미한다.
•
높은 팬아웃은 한 클래스가 다른 클래스를 여럿 사용한다는 것을 의미하며 따라서 지나치게 복잡한 로직을 담당하고 있는 가능성이 높다.
▪
이식성
•
이식성은 시스템을 다른 환경으로 쉽게 이동시킬 수 있도록 설계하는 것을 의미한다.
▪
간결성
•
간결성은 불필요한 부분이 없게 시스템을 설계하는 것을 의미한다.
▪
계층화
•
계층화는 시스템에 대해 특정한 계층에서 바라보고 일관되게 이해할 수 있도록 분산 계층을 유지하는 것을 의미한다.
•
시스템을 다른 계층을 보지 않고도 특정 계층에서 볼 수 있도록 설계해야 한다.
▪
표준 기법들
•
시스템이 색다른 방법에 의존하면 할수록 복잡도가 커진다. 표준화되고 일반적인 접근 방법을 사용해 전체 시스템이 친숙하다고 느껴지게 만들려고 노력해야 한다.
◦
설계 수준
▪
하나의 시스템에도 여러 상세한 수준이 존재하는데, 각 상세 수준마다 설계가 필요하다.
▪
몇몇 설계 기법은 모든 수준에 적용할 수 있고 몇몇은 한두 가지 경우에만 적용 가능하다.
▪
수준 1: 소프트웨어 시스템
•
첫 번째 수준은 전체 시스템이다.
•
시스템 수준은 서브시스템이나 패키지 같이 상위 수준의 클래스 조합을 충분히 생각하는 데 도움을 준다.
▪
수준 2: 서브시스템이나 패키지로 분할
•
이 수준의 설계에서 얻게되는 주요 산물은 모든 중요한 서브시스템을 식별하는 것이다.
•
서브시스템엔 데이터베이스나 사용자 인터페이스, 비즈니스 로직, 명령 해석기, 보고서 엔진 등이 있다.
•
프로그램을 주요 서브시스템으로 어떻게 나눌 것인지, 각 서브시스템이 다른 서브시스템을 어떻게 의존할 것인지 결정하는 것이 주요 설계 작업이다.
•
서로 다른 서브시스템이 어떻게 소통할 것인지에 대한 규칙을 정하는 것이 중요하다. 모든 서브시스템이 다른 모든 서브시스템과 소통할 수 있다면 그것들을 굳이 분리할 이유가 없다.
◦
커뮤니케이션을 제한하여 각 서브시스템의 분리를 정당하게 하라.
•
서브시스템이 무분별하게 다른 서브시스템과 커뮤니케이션한다면 어떤 문제가 생길까? 다음 예시들을 생각해보자.
◦
개발자가 UI를 그리기 위해서 전체 시스템에서 얼마나 많은 부분을 알고 있어야 할까?
◦
다른 시스템에서 비즈니스 규칙에 접근하여 뭔가를 사용한다면 어떤 일이 벌어질까?
•
당연한 말이지만 서브시스템 간의 커뮤니케이션은 서로 알 필요가 있을 때만 가능하게 만든다. 확신이 서지 않는다면 처음엔 커뮤니케이션을 제한하고 나중에 필요한 경우 완화하는 것이 낫다.
•
상호 연결 관게를 이해하고 유지하기 쉽게 하려면 내부 서브시스템의 관계를 간단하게 만든다.
◦
가장 간단한 관계는 하나의 서브시스템이 다른 서브시스템에 있는 루틴을 호출하는 것이다.
◦
가장 복잡한 관계는 한 서브시스템에 있는 클래스가 다른 서브시스템에 있는 클래스를 상속하는 것이다.
•
범용적으로 적용할만한 규칙은 시스템 수준의 다이어그램이 순환 구조를 갖지 않는 것이다.
•
공통적인 서브 시스템의 종류는 다음이 있다.
◦
비즈니스 규칙
▪
비즈니스 규칙은 컴퓨터가 풀어내는 문제이자 비즈니스에서 차별점을 만들어낼 수 있는 부분이다.
◦
사용자 인터페이스
▪
사용자 인터페이스가 프로그램의 나머지 부분에 지장을 주지 않고 발전할 수 있도록 독립적인 서브시스템을 개발한다.
◦
데이터베이스 접근
▪
데이터베이스 접근에 대한 구현 세부 사항을 감춰 프로그램 대부분이 저수준 구조체를 다루기 위한 복잡한 세부 사항에 의존하지 않게 만들고 비즈니스 수준의 매핑으로 처리될 수 있게 한다.
▪
구현 세부 사항을 감춘 서브시스템은 프로그램의 복잡성을 줄여주는 유용한 추상화 수준을 제공한다.
◦
시스템 의존성
▪
하드웨어의 의존성을 패키지화하는 것과 같은 이유로 운영체제에 대한 의존성을 서브시스템으로 패키지화한다.
▪
수준 3: 클래스로 분할
•
이 수준의 설계에서는 시스템에 필요한 모든 클래스를 구체화하는 작업이 들어간다.
◦
예를 들어 데이터베이스 서브 시스템은 데이터베이스 메타데이터뿐만 아니라 데이터 접근 클래스와 지속성 프레임워크 클래스로 나뉠 수 있다.
•
각 클래스가 시스템의 나머지 부분과 상호작용하는 방법에 대한 세부적인 사항도 클래스에 명시되어야 한다.
◦
특히 클래스의 인터페이스가 정의된다. 전반적으로 이 단계에서 가장 중요한 작업은 개별적인 클래스로 구현할 수 있을만큼 저수준으로 서브시스템을 분해하는 것이다.
•
프로젝트의 규모가 크다면 서브시스템을 클래스로 나누는 작업이 필요하다.
•
클래스와 객체
◦
객체지향 설계에서 가장 핵심적인 개념은 객체와 클래스의 차이를 이해하는 것에 있다.
▪
객체는 실행 중인 프로그램에 존재하는 구체적인 엔티티다.
▪
클래스는 프로그램 코드로, 정적이지만 객체는 프로그램을 실행할 때 생성되는 특정한 값과 속성을 가지는 동적인 것이다.
▪
데이터베이스 용어에 익숙하다면 스키마와 인스턴스의 차이를 떠올리면 된다.
▪
수준 4: 메소드로 분할
•
이 단계에서는 클래스를 메소드로 즉 동작으로 나눈다.
•
수준 3에서 정의한 인터페이스가 몇 가지 메소드를 정의한다. 여기선 클래스의 비공개 메소드를 상세히 설계한다.
•
클래스 내부에 있는 메소드의 구현 부분을 살펴보면 간단한 메소드도 많지만 계획적으로 구성되어 있어 추상적이므로 좀 더 구체적인 설계가 필요한 메소드가 있는 것도 알 수 있다.
•
클래스의 메소드를 완전히 정의하는 작업을 통해서 인터페이스에 대해 잘 이해하게 되면 인터페이스의 변경도 필요한 경우도 생긴다.
▪
수준 5: 내부 메소드 설계
•
이 단계의 설계에서는 각 메소드의 상세한 기능을 구현한다.
•
내부 메소드 설게는 일반적으로 개별적인 메소드를 개발하는 각 개발자의 몫이다. 설계는 의사코드를 작성하고 책이나 인터넷을 참고해 코드 단락을 어떻게 구성할 것인지 결정하고 코드를 작성하는 활동으로 구성된다.
3. 설계 빌딩 블록: 휴리스틱
•
소프트웨어 설계에는 정답이 없으므로 좋은 소프트웨어 설계를 위해서는 발견적 학습을 효과적으로 적용해야 한다.
◦
휴리스틱이었다.. 발견적 학습이..
◦
여기서 의미하는 휴리스틱은 소프트웨어의 패턴을 경험적, 직관적 학습을 통해 체계화하는 것이라고 생각한다.
•
현실 세계의 객체를 찾아라.
◦
설계의 대안을 규명하는 접근 방법으로 객체지향적 접근 방법이 가장 널리 알려져있으며 이는 현실 세계의 객체와 가상의 객체를 찾는 것에 초점을 맞춘다.
◦
객체를 설계하는 단계는 다음과 같다.
▪
객체와 객체의 속성(메소드와 필드)를 식별한다.
▪
각 객체가 무엇을 할 수 있는지 결정한다.
▪
각 객체가 다른 객체에 무엇을 할 수 있는지 결정한다.
▪
각 객체에서 다른 객체에 노출할 부분을 결정한다.
▪
각 객체의 공개 인터페이스를 정의한다.
◦
객체와 객체의 속성을 결정한다.
▪
프로그램은 현실의 문제를 풀어낸다. 따라서 대개 현실 세계의 엔티티에 기반을 둔다. 따라서 먼저 객체를 식별한다.
▪
예를 들어 시간별 청구 시스템은 현실 세계의 직원, 고객, 청구서에 기반을 둔다.
▪
객체를 식별하고 나면 객체의 속성을 찾는 것은 크게 어렵지 않다. 각 객체는 프로그래밍 언어에서 정의한 타입의 속성을 갖는다.
◦
각 객체가 무엇을 할 수 있는지 결정한다.
▪
각 객체에서 어떤 작업을 수행할 수 있는지 확인해보자.
▪
청구 시스템에서 직원 객체는 청구율이나 청구를 수행하며 고객 객체는 청구서 배달 주소를 변경할 수 있을 것이다.
◦
각 객체가 다른 객체에 무엇을 할 수 있는지 결정한다.
▪
통상 객체가 서로에게 할 수 있는 두 가지는 포함과 상속이다.
•
어떤 객체가 어떤 객체를 가지고 있는가?(속성으로 가질 수도 있고 어떠한 동작의 매개변수로써 가지고 있을수도 있지 않을까~)
•
어떤 객체가 어떤 객체로부터 상속받을 수 있는가?
▪
청구서 객체는 고객 객체에게 전달되어 고객 객체는 해당 청구서에 대한 요금을 지불할 수 있다.
◦
각 객체에서 다른 객체에 노출할 부분을 결정한다.
▪
설계 시 객체의 어느 부분을 공개하고 어느 부분을 비공개로 유지해야 하는지를 파악하는 것은 가장 중요한 결정 중 하나다.
▪
속성, 메소드 모두에 이러한 사항을 결정해야 한다.
◦
각 객체의 인터페이스를 결정한다.
▪
각 객체에 대해 형식적이고 이해하기 쉬운 프로그래밍 언어 수준의 인터페이스를 정의한다.
•
해당 객체가 다른 모든 객체에 노출하는 속성과 메소드를 public이라 한다.
•
객체가 상속을 통해서 결합하고 있는 객체에 노출하는 부분을 객체의 protected 인터페이스라고 한다.
•
일관성 있게 추상화하라.
◦
다른 수준에 있는 서로 다른 세부 사항을 다루지만 세부 사항을 무시해도 문제가 없는 개념과 맞물리는 능력을 추상화라고 한다.
◦
집합체라는 개념을 도입해 작업할 때 추상화를 통해 작업하고 있다고 말할 수 있다.
▪
어떤 물체를 유리, 나무, 못의 결합이라고 하지 않고 집이라고 부른다면 추상화한 것이다.
▪
집이 모여있는 집합을 집의 집합이라 하지 않고 마을이라 한다면 그 또한 추상화한 것이다.
◦
훌륭한 개발자는 인터페이스 수준, 클래스 수준, 패키지 수준의 단계적인 추상화를 제공함으로써 더 빠르고 안전하게 프로그래밍할 수 있게 해준다.
•
구현 세부 사항을 캡슐화하라.
◦
추상화 다음으로 캡슐화를 진행한다.
◦
추상화는 객체를 상위 수준에서 바라볼 수 있도록 하고 캡슐화는 다른 수준에선 해당 객체를 특정한 수준 이상으로 알지 못하게 하는 것이다.
◦
캡슐화는 집의 외관을 볼 수 있지만 집 내부의 가구는 무엇이 있는지 알 수 있을 정도로 접근할 수는 없다는 개념이다.
◦
캡슐화는 복잡한 부분을 보지 못하게 함으로써 복잡성을 관리하는데 도움을 준다.
•
상속은 설계를 단순화할 수 있을때 사용하라.
◦
상속은 적절히 추상화하였을 때, 해당 추상화에 변경이 발생할 가능성이 없을 때 사용하자.
▪
정규직과 계약직은 직원이라는 기본적인 속성에 계약 기간만 다른 정도다. 따라서 사용해도 적합하다.
◦
상속은 객체지향 프로그래밍에서 가장 강력한 도구로 잘 사용하면 엄청난 이득을 보지만 제대로 사용하지 못하면 생산성이 크게 떨어진다.
•
비밀을 숨겨라(정보 은닉)
◦
정보 은닉은 구조적 설계와 객체지향 설계 모두에 있어서 기본적인 부분이다.
▪
구조적인 설계에서는 블랙박스라는 개념이 정보 은닉에 해당한다.
▪
객체지향적인 설계에서는 정보 은닉을 위해 캡슐화와 모듈성 개념이 생겼고 이는 추상화 개념과 연결된다.
◦
정보 은닉은 비밀이라는 개념을 특징으로 하는데, 이는 소프트웨어 개발자가 설계나 구현에서 나머지 프로그램으로부터 한 부분을 숨기기로 결정하는 것이다.
◦
정보 은닉은 복잡성을 감추는 데 중점을 두고 있기 때문에 소프트웨어의 주요 기술적 의무에 특히 강력한 발견적 기법이다.
◦
비밀과 프라이버시
▪
정보 은닉 관점에서 클래스들은 그것을 다른 클래스로부터 숨기는 설계나 구현으로 특정지어진다.
▪
그 비밀은 변경 가능성이 높은 영역일수도 구현 상세일 수도 있다. 클래스의 역할은 이러한 정보들을 숨기고 프라이버시를 보호하는 것이다.
▪
시스템에서 발생한 사소한 변경사항은 클래스 내부의 메소드에 영향을 미칠 수는 있지만 해당 클래스의 범위를 벗어나는 범위까지 영향을 미쳐서는 안 된다.
▪
클래스는 가능한 내부 작업을 드러내지 않아야 한다. 좋은 클래스는 빙산의 일각과 같아서 클래스 대부분을 노출하지 않는다.
◦
정보 은닉의 예
▪
특정 객체의 속성 중 id가 존재한다고 할 때, 이 객체의 생성 메소드를 감추지 않는다면 무슨 일이 벌어질까?
•
해당 id를 생성하는 로직이 여기저기 퍼져있게 될 것이고 유지보수가 어렵게 될 것이다.
◦
은닉의 두 가지 부류
▪
정보 은닉에서 비밀은 다음과 같은 두 개의 그룹으로 나뉜다.
•
특별하게 관심이 없는 경우에 고민할 필요가 없도록 복잡성을 감추는 것
•
변경이 발생했을 때 그 효과가 일부에만 영향을 미치도록 변경의 원인을 감추는 것
◦
정보 은닉의 장애물
▪
정보의 지나친 배분
•
이는 시스템 전체에 정보를 지나치게 배분하는 것이다.
•
시스템 전체에 리터럴 100을 코드로 사용하는 것보다 MAX_EMPLOYEE와 같은 상수에 이 정보를 숨겨서 한 곳에서만 값이 변경되게 하는 것이 좋다.
▪
순환 의존성
•
순환 의존성도 정보 은닉을 불가능하게 만든다. 이는 일반적으로 A 클래스가 B 클래스의 메소드를 호출하고 클래스 B가 클래스 A의 메소드를 호출하는 것이다.
•
의존성 순환을 피해야 한다. 그러한 구조는 어느 한쪽이 준비되기 전까진 둘 다 테스트할 수 없으므로 테스트하기 어렵게 만든다.
▪
전역 데이터로 오해받는 클래스 데이터
•
객체의 값을 전역에 두지 말자, 전역 변수를 사용하면 지옥으로 가는 급행열차를 타는 것이다.
◦
다른 메소드가 전역 데이터를 다루고 있다는 사실을 알지 못한 채 전역 데이터에 접근하는 경우 문제가 될 것이다.
◦
전역 데이터를 다루고 있다는 사실을 알고는 있지만 정확하게 무엇을 하는지 알지 못하는 경우 문제가 될 것이다.
▪
성능 손해
•
아키텍처 수준과 코드 작성 수준에서 성능 손해를 피하기 위한 시도일 것이다.
•
어느 경우에도 걱정할 필요가 없다. 아키텍처 수준에서는 정보 은닉을 위한 시스템 설계가 문제되지 않는다.
•
코드 작성 시 데이터에 간접적으로 의존하는 과정에서 발생하는 메소드와 객체 생성이 성능 손해가 발생할 것이라는 걱정이다.
◦
하지만 이는 너무 성급한 것이다.
•
시스템의 성능을 측정하여 병목이 발생하는 위치를 정확하게 찾기 전까지 코드 수준에서 성능 개선을 미리 대비하는 방법은 코드를 가능한 모듈화하는 것이다.
◦
정보 은닉의 가치
▪
정보 은닉을 사용한 큰 프로그램이 그렇지 않은 프로그램보다 4배나 수정하기 쉽다.(Korson and Vaishnavi 1986).
▪
게다가 정보 은닉은 구조적인 설계와 객체지향적인 설계 모두에 있어서 기본적인 부분이다.
▪
정보 은닉은 고유한 특성에 기반을 두고 발견적인 방법으로 좋은 설계를 돕는다.
▪
이 클래스에서 무엇을 숨겨야 하는가라고 묻는 것이 인터페이스 설계 문제를 해결하는데 가장 중요하다.
▪
무엇을 숨겨야 하지라고 질문하는 습관을 갖자 설계에 관한 결정하기 어려운 문제의 상당수가 해결될 것이다.
◦
변경될 것 같은 영역을 찾아라
▪
변경의 효과가 한 메소드나 클래스, 패키지에 제한되도록 불안정한 영역을 고립시키는 것이 목표다. 다음은 이를 달성하는데 도움을 주는 단계다.
1.
변경될 것처럼 보이는 항목을 찾는다.
2.
변경될 것 같은 항목을 분류한다.
3.
변경될 것처럼 보이는 항목을 분리시켜 제한한다.
▪
다음은 변경될 가능성이 큰 영역이다.
•
비즈니스 규칙
•
하드웨어 의존성
•
입출력
•
표준을 따르지 않는 언어
•
어려운 설계 및 구현 부분
•
상태 변수
•
데이터 크기 제약
◦
변경의 정도 예측하기
▪
시스템에 대한 잠재적인 변경에 대해 생각할 때 변경의 효과나 범위가 변경이 발생할 가능성에 비례하도록 시스템을 설계한다.
•
변경이 잦을 경우, 그 범위는 제한시키는 것이 좋다. 그렇지 않다면 큰 영역에 영향을 끼쳐도 괜찮다.
▪
변경될 것 같은 영역을 식별하는 좋은 기법은 우선 프로그램에서 사용자에게 쓸모가 있는 최소한의 부분을 파악하는 것이다.
•
해당 부분이 시스템의 비즈니스 로직을 많이 가지고 있을 가능성은 작다.
▪
다음으로 시스템에 대한 최소한의 변경 사항이 무엇인지 정의한다.
▪
핵심적인 부분을 먼저 식별함으로써 어떤 컴포넌트가 추가되는 부분인지 알 수 있고 거기서부터 개선할 내용을 추정하고 숨길 수 있다.
•
결합을 느슨하게 유지하라
◦
결합은 클래스나 메소드가 다른 클래스의 루틴과 얼마나 밀접하게 연관되어 있는지를 기술한다.
◦
결합을 느슨하게 유지하려는 목표는 다른 클래스나 메소드와 작고 직접적이며 눈에 띄고 유연한 관계를 갖는 클래스를 만드는 것으로 이를 느슨한 결합이라고 한다.
◦
이 장에선 모듈을 클래스와 메소드의 집합이라고 표현하고 있다. 하나의 인스턴스화 된 클래스나 클래스 개념 관점에서 보면 될 것 같다.
◦
하나의 모듈이 다른 모듈에 의해 쉽게 사용될 수 있을 정도로 느슨한 상태가 훌륭한 결합 형태다.
▪
즉 분해가 어렵지 않아야 한다. 얼마든지 대체될 수 있으며 클라이언트 코드가 사용하는 형태여야 한다.
◦
결합의 기준
▪
크기
•
크기는 모듈 사이의 연결 횟수를 의미한다. 결합에서는 작은 인터페이스를 갖는 모듈이 다른 모듈에 연결하기가 상대적으로 쉽기 때문에 가능하면 작은 것이 좋다.
▪
가시성
•
가시성은 두 모듈 간의 연결이 얼마나 명시적인지를 읨한다. 프로그래밍은 가능한 연결 부분을 눈에 띄게 만들어야 좋다.
▪
유연성
•
유연성은 얼마나 쉽게 모듈 사이의 연결을 변경할 수 있는지를 의미한다.
•
한마디로 어떤 모듈이 다른 모듈을 호출하는 게 쉬울수록 결합은 더 느슨해지는데, 이렇게하면 유연성은 커지고 유지보수하기도 쉬워진다.
•
시스템 구조를 만들 때는 상호 연결을 최소화하도록 프로그램을 나누는 것이 좋다.
◦
결합의 종류
▪
간단한 데이터 매개변수 결합
•
두 모듈 사이에서 전달되는 모든 데이터가 기본 데이터형이고 모든 데이터가 매개변수로 전달된다면 두 모듈은 단순-데이터-매개변수로 결합된 것이다.
•
이러한 종류의 결합은 자연스럽고 허용할 수 있다.
▪
간단한 객체 결합
•
모듈이 특정 클래스를 인스턴스화한다면 그 모듈은 객체에 단순-객체로 결합된 것이다.
•
이러한 종류의 결합은 좋다.
▪
객체 매개변수 결합
•
객체 1이 객체 2에게 객체 3을 달라고 요구한다면 두 모듈은 서로에 대해서 객체-매개변수로 결합한 것이다.
•
이런 종류의 결합은 객체 2가 객체 3의 정보를 알고 있어야 하기 때문에 결합이 좀 강하다.
▪
의미론적인 결합
•
한 모듈이 다른 모듈의 프로그래밍 요소를 이용하지 않고 다른 모듈의 내부 작동에 대한 논리적인 지식을 사용할 때다.
•
가장 좋지 않은 결합이다.
•
사용된 모듈의 코드를 변경했을 때, 사용하는 모듈에 어떻게 영향을 끼칠지 런타임까지는 알지 못하기 때문에 매우 위험하다.
◦
일반적으로 널리 사용되는 디자인 패턴을 찾아라
▪
패턴은 스스로의 설계로는 얻을 수 없는 여러 이점을 제공한다.
▪
패턴은 이미 만들어진 추상화를 제공함으로써 복잡성을 줄인다.
▪
패턴은 일반적으로 널리 사용되는 해결책의 세부 사항들을 규정함으로써 오류를 줄인다.
▪
패턴은 대안을 제시함으로써 발견적 학습의 가치를 제공한다.
▪
패턴은 설계에 대해 수준 높은 논의를 할 수 있게 해서 의사소통을 원활하게 한다.
▪
전반적으로 디자인 패턴은 복잡성을 관리하기 위한 강력한 도구다.
•
다른 휴리스틱
◦
응집력을 강하게 하라
▪
응집력은 좋은 구조적인 설계의 결과로 얻을 수 있고 대개 결합과 같은 맥락으로 다뤄진다.
▪
응집력은 클래스에 있는 모든 메소드나 메소드에 있는 코드가 얼마나 밀접하게 클래스의 책임을 지원하고 있는지 그 클래스가 얼마나 해당 책임에 집중되어 있는지를 나타낸다.
▪
매우 연관성이 높은 기능들을 포함하고 있는 클래스를 응집력이 강하다고 말하며 생산성을 좋게하는 클래스들은 보통 이런 응집력이 높다.
◦
계층을 만들어라
▪
계층은 단계식 정보 구조로 가장 일반적이거나 추상적인 항목이 최상위에 위치하고 점차 상세하고 구체적인 항목이 낮은 수준에 위치한다.
▪
계층은 소프트웨어의 주요 기술적 책임이자 의무인 복잡성 관리를 달성하는데 요긴한 도구다. 계층을 이용하면 현재 계층에서 다루는 책임에만 집중할 수 있기 때문이다.
◦
클래스 계약을 형식화하라
▪
더 상세한 수준에서는 각 클래스의 형식을 나머지 프로그램과의 계약으로 이해하는 것이 도움이 된다.
•
어떠한 특징을 가진 그들이 뭔가를 제공했을때 이 클래스의 메소드는 이렇게 하기로 했다.와 같이 말이다.
▪
메소드 시그니처를 계약이라고 생각해도 될 것 같다. 적어도 이론상으로 객체는 계약에 없는 행위를 무시해도 되기 때문에 복잡성을 관리하는 데 유용하다.
◦
책임을 할당하라
▪
객체에 어떻게 책임을 할당하는 것인가 생각하는 방법이다.
▪
객체가 무엇을 책임져야 하는지 질문하는 것은 어떤 정보를 감추고 어떤 정보를 드러낼 것인가를 묻는 것과 유사하지만 책임을 묻는 것이 더 추상적인 답을 제공해 특별한 발견적 가치를 부여한다.
◦
테스트가 가능하도록 설계하라
▪
테스트가 용이한 시스템이 무엇인지를 고민한다면 좋은 설계를 만들 수 있다.
▪
테스트는 일반적으로 독립되어 수행되기 때문에 각 영역을 고립시키는 좋은 설계를 만들어낼 수 있다.
◦
실패를 피하라
▪
설계자가 다른 성공적인 설계 사례의 특성을 단순히 따르기만 한다면 이는 실패하게 될 가능성이 있다.
▪
실패에 대해서 고민해야한다. 성공의 특징에 의존하는 것은 내게 주어진 요구사항과 제약사항들을 충분히 고려하지 않은 선택이니까.
◦
결합 시점을 의도적으로 선택하라
▪
결합 시점은 특정 값이 변수에 결합하는 시점을 의미한다. 초기에 결합하는 코드는 쉽지만 유연성이 떨어진다.
▪
다음과 같은 질문을 통해 좋은 설계에 대한 통찰력을 얻어보라.
•
이 변수를 초기에 결합한다면 어떻게 될까?
•
이 값들을 나중에 결합한다면 어떻게 될까?
•
이 테이블을 코드에서 초기화하거나 실행 시 사용자로부터 변수의 값을 얻는다면 어떻게 될까?
◦
제어 지점을 정하라
▪
제어는 클래스와 메소드, 전처리기, 유틸리티 파일에 집중될 수 있다.
▪
어느 것이 흐름을 제어할 지 결정하라. 이는 복잡성을 줄게 해준다. 복잡성이 줄어들면 흐름을 파악하기 위해 찾아야하는 코드가 줄고 더 쉽고 안전하게 변경할 수 있다.
◦
브루트포스 기법의 사용을 고려하라
▪
가장 강력한 휴리스틱 도구 중 하나는 브루트포스다.
▪
우아한 해결책은 해당 해결책이 모든 부분을 커버할 수 있는지 사고해야하는 시간이 필요하므로 보통 오래 걸린다.
◦
다이어그램을 그려라
▪
다이어그램 또한 강력한 휴리스틱 도구 중 하나다.
▪
천 마디 말보다 그림 한 장이 낫다. 그림을 사용하면 높은 추상화 수준에서 문제를 표현할 수 있으므로 이해가 쉬워진다.
◦
모듈화를 유지하라
▪
모듈화의 목표는 각 루틴이나 클래스를 블랙박스처럼 만드는 것이다.
▪
무엇이 들어가고 무엇이 나올지를 알지만 안에서 무슨 일이 일어나는지 알지는 못해야 한다.
▪
모듈화 개념은 정보 은닉과 캡슐화, 다른 설계에서의 휴리스틱과 관련이 있다.
•
설계의 휴리스틱에 대한 요약
◦
다음은 주요한 설계의 휴리스틱에 대한 요약이다.
▪
현실 세계의 객체를 찾아라.
▪
일관성 있는 추상화를 구성하라.
▪
세부 사항을 캡슐화하라.
▪
가능할 때 상속하라.
▪
비밀을 숨겨라(정보 은닉).
▪
변경될 것 같은 영역을 규명하라.
▪
느슨한 결합을 유지하라.
▪
일반적으로 널리 사용되는 디자인 패턴을 찾아라.
◦
다음 휴리스틱 역시 때때로 유용하다.
▪
응집력을 강하게 하라.
▪
계층을 만들어라.
▪
클래스 계약을 형식화하라.
▪
책임을 할당하라.
▪
테스트가 가능하도록 설계하라.
▪
실패를 피하라.
▪
결합 시점을 의식적으로 선택하라.
▪
제어 지점을 정하라.
▪
브루트포스 기법의 사용을 고려하라.
▪
다이어그램을 그려라.
▪
모듈화를 유지하라.
•
휴리스틱을 위한 지침
◦
문제 해결을 위한 휴리스틱에 대한 초창기 책 중 하나로 어떻게 문제를 풀 것인가(G. Polya 저)라는 책이 있다.
◦
폴리아가 일반화한 문제 해결 접근 방법은 수학 문제를 해결하는 데 초점을 맞추었다.
▪
그 내용은 다음과 같다.
1.
문제를 이해한다.
2.
계획의 고안
3.
계획의 실행
4.
검토
◦
해당 내용중 가장 효과적인 지침 중 하나는 한 가지 접근 방법에 매달리지 말라는 것이다.
◦
설계 문제 전체를 한 번에 해결할 필요는 없다. 작업이 막히면 잠시 숨을 고르고 현재 상황을 돌아본다. 그리고 내가 해당 문제를 지금 풀기에는 충분한 정보가 부족하다는 것을 인식하자.
◦
나중에 해결될 수 있는데 왜 아까운 자원을 낭비해야할까. 문제는 그 문제를 해결할 필요가 있을때 해결하는 것이다.
4. 설계 실천법
•
개요
◦
이 절에서는 종종 좋은 결과물을 산출할 수 있는 단계인 경험적 설계 방법을 소개한다.
•
반복
◦
설계는 반복적인 프로세스다. 보통 한 번 작성해둔 코드는 다시 한번 읽게 되어있다.
◦
설계 후보를 여러번 훑어보고 여러 가지 방법을 시도해보면 상위 수준과 하위 수준, 모든 관점에서 바라볼 수 있게 된다.
▪
상위 수준 문제를 다루면서 얻는 큰 그림은 거시적 관점에서 하위 수준의 세부 사항을 작성하는 데 도움이 될 것이다.
▪
하위 수준 문제를 다루면서 얻게 되는 세부 사항은 미시적 관점에서 상위 수준의 문제를 더 현실적으로 설계하는데 도움이 될 것이다.
◦
최상위 수준과 최하위 수준의 고려 사항을 각자 조율하는 것은 좋은 행동이다. 이는 전체를 하향식, 또는 상향식으로만 흐름을 두는 것보다 안정적인 강화 구조를 만든다.
◦
처음 설계가 충분히 좋아보여도 멈추지 말라. 두 번째 시도는 보통 첫 시도보다 좋고 설계를 반복할 때마다 전체 설계를 향상시킬 수 있다는 것을 배우게 된다.
•
분할과 정복
◦
다익스트라가 말했듯 복잡한 프로그램의 모든 세부 사항을 기억할만큼 똑똑한 사람은 없으며 설계도 마찬가지다.
◦
프로그램을 서로 다른 관심 영역으로 나눠두고 각 부분을 개별적으로 처리하도록 한다.
◦
점증적인 개선은 복잡성을 다룰 수 있는 강력한 도구다. 문제를 이해하고 계획을 수립하여 수행하고 어떻게 했는지를 검토하도록 한다.
•
하향식과 상향식 설계 접근 방법
◦
이 접근 방법은 객체지향적인 설계를 작성하는 데 도움이 되는 통찰력을 제공한다.
◦
하향식 설계는 높은 추상화 수준에서 시작한다. 기본 클래스나 구체적이지 않은 다른 설계 요소를 정의한다.
▪
이는 하위 클래스 규명하기와 추상 클래스가 가져야할 작업들, 다른 세부적인 설계 요소 등 상세화 수준을 높일 수 있게 한다.
◦
상향식 설계는 구체적인 것부터 시작해서 일반적인 쪽으로 이어진다. 구체적인 객체를 규명하는 것부터 시작해서 그 내용으로부터 객체와 기본 클래스 집합을 일반화한다.
◦
두 설계는 개념적으로 대립되기에 각 설계가 좋다고 하는 진영이 존재한다. 다음은 양측 주장이다.
▪
하향식 접근 방법을 지지하는 주장
•
인간의 두뇌가 한 번에 집중할 수 있는 영역과 그 양에 한계가 있으므로 추상적인 개념부터 시작하면 두뇌가 한 번에 너무 많은 세부 사항을 처리하지 않아도 된다는 장점이 존재한다.
▪
상향식 접근 방법을 지지하는 주장
•
하향식 접근 방법은 추상적이기 때문에 너무 어렵다. 더 가시적으로 작업해야 한다면 상향식 설계 접근 방법이 좋다고 주장한다.
•
다음은 상향식 구성을 할 때 기억해야할 사항이다.
◦
시스템이 무엇을 해야 하는지 알고 있는지 스스로 묻는다.
◦
그 질문으로부터 구체적인 객체와 책임을 파악한다.
◦
공통적인 객체를 식별하고 서브시스템 구조나 패키지 객체 내에서의 합성, 상속 등 적합한 것을 사용해 그 객체들을 그룹짓는다.
◦
다음 상위 수준에서 작업을 계속하거나 최상위로 돌아가 다시 하위 수준으로 작업을 진행한다.
▪
사실상 논쟁은 불필요하다.
•
하향식과 상향식의 가장 큰 차이는 분해 전략이냐 결합 전략이냐다.
•
각각 장점과 단점이 있고 이들은 서로 다른 상황에서 무엇을 선택해야 하는지 어렵지 않게 해준다.
◦
하향식은 쉽다. 그리고 세부 사항 구현을 미룰 수 있다.
◦
상향식은 필요한 유틸리티 기능을 초기에 파악할 수 있어서 간결하고 잘 구성된 설계가 만들어진다는 것이다.
▪
다만 이 접근 방법만 사용해서는 개발이 쉽지 않다. 특정 영역에만 집중된 클래스 여럿이 하나의 응집도를 가진 시스템을 만들어내는 것은 그 확률이 매우 낮다.
•
둘은 배타적인 전략이 아니고 상호 보완이다. 적절한 상황에 적절한 접근법을 선택하라.
•
실험적인 프로토타이핑
◦
구현 세부 사항을 제대로 이해하기 전까지 설계가 적합한지 알 수 없는 경우가 있다.
▪
성능 목표를 만족하는지 등 말이다.
◦
이들은 최소한의 부분을 해결하지 않고서는 설계 문제를 완전하게 정의할 수 없다.
◦
적은 비용으로 이런 문제를 해결하는 기법이 실험적인 프로토타이핑이다.
•
협력적인 설계
◦
설계에서는 조직 구성이 공식적이든 비공식적이든 둘이 하나보다 낫다. 협력은 다음과 같이 다양한 형태를 취한다.
▪
비공식적으로 동료에게 다가가 아이디어를 생각해 보자고 요청한다.
▪
동료와 함께 회의실에 앉아서 화이트보드에 여러 설계 아이디어를 그려본다.
▪
동료와 함께 키보드 앞에 앉아 사용하는 언어로 세부적인 설계를 수행한다.
▪
하나 이상의 동료와 디자인 아이디어를 검토할 수 있게 회의 일정을 정한다.
▪
공식 검토를 수행한다.
▪
자신의 작업을 검토해줄 수 있는 동료가 없다면 초기 설계 작업을 서랍 속에 넣어 놓고 일주일이 지난 후 다시 살펴본다.
▪
조직 외부에 도움을 요청한다. 커뮤니티에 질문을 올린다.
•
설계를 어디까지 해야 할까?
◦
구현 시작 전 설계를 어디까지 해야 하는지, 설계를 문서화하기 위해서 설계를 얼마나 형식화해야 하는지 결정하는 공식은 없다.
◦
팀의 경험과 시스템의 예상 수명, 신뢰도, 프로젝트와 팀의 크기가 모두 고려되어야 한다.
요소 | 설계 수준 | 문서화 형식 |
팀이 경험이 많은 편 | 하 | 하 |
팀이 경험은 많지만 이번 스택의 경험은 적은 편 | 중 | 중 |
팀이 경험이 적은 편 | 중상 | 중하 |
팀의 이직률이 높은 편 | 중 | - |
프로그램이 높은 안정성을 요구 | 상 | 상 |
프로그램이 특수 업무용 | 중 | 중상 |
프로젝트가 큰 편 | 하 | 하 |
프로젝트가 작은 편 | 중 | 중 |
소프트웨어의 예상 수명이 짧은 편(몇 주, 몇 달) | 하 | 하 |
소프트웨어의 예상 수명이 긴 편(몇 달, 몇 년) | 중 | 중 |
•
설계 작업 기록하기
◦
설계 작업을 기록하는 전형적인 방법은 형식적인 설계 문서를 작성하는 것이다.
◦
하지만 규모가 작거나 비형식적인 프로젝트와 같인 설계를 가볍게 기록해도 되는 프로젝트라면 여러 방법으로 설계를 기록할 수 있다.
▪
설계 문서를 코드 자체에 넣는다.
•
핵심적인 설계 결정 사항들을 코드 주석으로 문서화하는 방법이다.
▪
설계에 대한 논의와 결정을 위키에 기록하라.
•
프로젝트 구성원이라면 누구나 쉽게 편집, 접근할 수 있는 웹 페이지 모음이다.
▪
이메일로 요약하라.
•
설계 논의가 종료된 후, 해당 내용에 대한 요약을 누군가에게 보내라.
▪
디지털카메라를 사용하라.
•
설계를 위해 사용했던 메모지나 화이트보드를 찍어서 보관하라라는 의미다.
▪
설계 플립 차트를 보관하라.
•
큰 플립 차트를 그리고 이를 보관하는 방법이다.
▪
클래스, 책임, 협력자 카드(CRC)를 사용하라.
•
각각의 카드 위에 설계자가 클래스의 이름과 책임, 협력자를 기록한다.
▪
적절한 상세 수준에서 UML 다이어그램을 작성하라.
•
UML은 설계 엔티티와 관계에 대한 형식적인 표현 방법을 풍부하게 제공한다.
•
영속성 수준에서도 비즈니스 규칙 수준에서도 사용할 수 있는 적절한 방법같다.
◦
이러한 기법들은 어느 하나만 선택하는 것이 아닌 조합되어 사용ㅎ 가능하므로 혼합하여 사용하도록 하자.
5. 잘 알려진 방법론에 대한 의견
•
요즘은 모든 것을 설계하라에서 아무것도 설계하지 말라쪽으로 분위기가 변했다.
•
여기에 대한 저자의 의견은 선행 설계를 적게하거나 현재 정보로 판단하기에 적절한 수준까지의 설계만 진행하라는 것이다.
◦
YANGNI가 생각났다.
•
어느 정도가 충분한지 어떻게 알 수 있을까? 그건 내가 판단하는 것이다. 얻을 수 있는 정보들로
•
다음 두 설계는 분명히 잘못된 것이다.
◦
모든 세부 사항까지 설계하는 것
◦
전혀 설계하지 않는 것
•
설계 방법을 적용하는 데 있어 하나만 고집한다면 해결할 수 있는 문제는 줄어든다.
◦
첫 설계에 안주하지 말라. 협력하라. 단순함을 추구하라. 필요할 때 프로토타입을 만들라. 반복하고 반복하고 또 반복하라. 그러면 만족스러운 설계가 나올 것이다.
참고 자료
•
다음 자료들은 각 영역에 있어서 추천하는 자료나 서적들이다.
•
일반적인 소프트웨어 설계
◦
Weisfeld, Matt. The Object-Oriented Thought Process, 2d ed
▪
객체지향을 소개하는 책이다. 입문자에게 유용하다.
◦
Riel, Arthur J. Object-Oriented Design Heuristics. Reading
▪
클래스 수준에서의 설계에 초점을 뒀다.
◦
Plauger, P. J. Programming on Purpose: Essays on Software Design
▪
이 책에서 플로거는 다양한 설계 접근 방법을 소개한다.
◦
Meyer, Bertrand. Object-Oriented Software Construction, 2d ed
◦
Raymond, Eric S. The Art of UNIX Programming. Boston, MA: Addison-Wesley, 2004
▪
유닉스 관점에서 소프트웨어 설계를 바라보는 책이다.
◦
Larman, Craig. Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design and the Unified Process, 2d ed
•
디자인 패턴
◦
Gamma, Erich, et al. Design Patterns. Reading, MA: Addison-Wesley, 1995
◦
Shalloway, Alan, and James R. Trott. Design Patterns Explained
•
일반적인 설계
◦
Adams, James L. Conceptual Blockbusting: A Guide to Better Ideas, 4th ed.
▪
효과적인 설계를 위한 사고의 종류의 예제나 창조적인 사고 프로세스에 관해서 설명하는 책이다.
◦
Polya, G. How to Solve It: A New Aspect of Mathematical Method, 2d ed
▪
수학적인 문제를 해결할 때 휴리스틱을 사용하는 방법에 대해서 기술되어 있다.
◦
Michalewicz, Zbigniew, and David B. Fogel. How to Solve It: Modern Heuristics.
▪
비수학적 예제를 포함해 폴리아의 책을 향상된 방법으로 다루고 있다.
◦
Simon, Herbert. The Sciences of the Artificial, 3d ed. Cambridge, MA: MIT Press, 1996
▪
생물학, 지질학 등 자연을 다루는 과학과 경제학, 컴퓨터 과학 등 인공저긴 세상을 다루는 과학을 구별한다.
▪
설계 과학에 중점을 두면서 인공 과학의 특징에 대해서 설명한다.
◦
Glass, Robert L. Software Creativity. Englewood Cliffs, NJ: Prentice Hall PTR, 1995.
▪
설계를 중심으로 소프트웨어의 본질에 대해서 논의한다.
◦
Petroski, Henry. Design Paradigms: Case Histories of Error and Judgment in Engineering
▪
성공 못지 않게 실패를 통해서도 성공적인 설계를 배운다는 주장을 설명하기 위해 토목 공학 분야로부터 결론을 끌어낸다.
•
표준
◦
IEEE Std 1016-1998, Recommended Practice for Software Design Descriptions
▪
이 문서는 소프트웨어 설계 기술에 대한 IEEE-ANSI 표준을 포함한다.
▪
소프트웨어 설계 문서에 무엇이 포함되어야 하는지를 기술한다.
◦
IEEE Std 1471-2000. Recommended Practice for Architectural Description of Software Intensive Systems
▪
이 문서는 소프트웨어 아키텍처 명세를 생성하기 위한 IEEE-ANSI 지침이다.
요점 정리
•
소프트웨어의 주요 기술적 의무는 복잡성을 관리하는 것이다. 이것은 단순함에 초점을 맞춘 설계로 달성할 수 있다.
•
단순함은 다음 두 가지 일반적인 방법으로 달성할 수 있다.
◦
한 번에 뇌에서 처리해야하는 본질적인 복잡성의 양을 최소화하는 것
◦
부수적인 복잡성이 불필요하게 증가하지 않도록 하는 것
•
설계는 휴리스틱이다. 한 가지 방법론만 고집하면 독창성과 프로그램에 해가 된다.
•
좋은 설계는 반복적이다. 여러 번 시도할수록 점차 개선된다.
•
정보 은닉은 매우 유용한 개념이다. 무엇을 숨겨야 하지라는 질문은 해결하기 어려운 설계상의 문제를 해결해준다.