5장 - 구현 설계
Edited by / 임수형 (sulogc)
5-1. 설계의 어려움
“소프트웨어 설계”는 컴퓨터 소프트웨어에 대한 명세를 동작 가능한 소프트웨어로 변환하기 위한 계획에 대한 구상이나 창작, 도구를 뜻한다.
- 설계는 불명확하고, 엉성하다. “불명확한 문제”란 전체 혹은 일부를 해결해야만 정의할 수 있는 문제이다.(Horst and Melvin 1973) 문제를 정의하려면 앞서 해결 해야하고, 이러한 프로세스는 소프트웨어 개발의 오랜 관습이다. 이 과정에서 실수를 하는 것이 설계의 핵심이고, 완성되는 시점이 분명하지 않다. 고로 엉성하다.
- 설계는 절충과 우선순위의 문제이고, 제약이 따른다.
- 설계는 비결정적이고, 발견적 학습 과정이다. 예상된 결과를 만들어내는 반복적인 처리과정과 달리, 경험에 의해 진행되는 발견적 학습이다.
- 설계는 창발적이다.
5-2. 핵심 설계 개념
핵심 설계 개념으로 다음 세 가지가 있다.
- 복잡성 관리, 바람직한 설계의 특징, 설계 수준
a. 소프트웨어의 주요 기술적 의무: 복잡성 관리
프레드 브룩스의 논문 “No Silver Bullets.’Essence and Accidents of Software Engineering”에서 소프트웨어의 복잡성 관리를 다룬다.
- 어떠한 사물에 대해 본질적 속성과 비본질적 속성으로 나눌 수 있다.
- 비본질적 어려움은 3세대 언어와 통합 프로그래밍 개발 환경 덕분에 줄어들었다.
- 본질적 어려움은 복잡하고 무질서한 요소들이 상호작용하는 현실 세계의 문제를 다루기 때문에 느리게 해결된다.
데이크스트라는 컴퓨터의 심오한 개념적 계층구조에 따라 발생한 새로운 본질적 어려움을 짚었다. 그렇기에 소프트웨어 개발자의 머리를 초과하지 않고, 한 부분을 집중할 수 있게 프로그램을 구성해야한다. 복잡한 시스템을 독립적인 서브시스템으로 나누고, 객체를 신중하게 정의, 루틴을 짧게 유지, 문제 범위 내에서 추상성을 최대화 하는 노력을 통해 인간의 선천적인 한계를 줄일 수 있다.
- 두뇌가 한 번에 처리해야하는 본질적인 복잡성의 양을 최소화한다.
- 비본질적인 복잡성이 불필요하게 증가하지 않도록 한다.
b. 바람직한 설계의 특징
다음은 바람직한 설계를 위한 내부 설계의 특징들이다.
- 복잡성 최소화
- 유지보수의 편리함
- 느슨한 결합 : 인터페이스 추상화, 캡슐화, 정보 은닉
- 확장성
- 재사용성
- 높은 팬 인(fan-in) : 특정 클래스를 사용하는 클래스 수가 많음.
- 낮은 팬 아웃(fan-out) : 특정 클래스가 다른 클래스를 적게 사용.
- 이식성
- 간결성
- 계층화 : 복잡한 시스템을 여러 계층으로 나눌때, 특정 계층만 보고도 일관된 이해를 할 수 있도록 해야한다.
- 표준 기법들
c. 설계 수준
하나의 시스템에 여러 상세 수준이 있고, 각 상세 수준마다 설계가 필요하다.
수준1. 소프트웨어 시스템
수준2. 서브시스템이나 패키지로 분할
어떻게 서브시스템으로 나눌지, 각 서브시스템이 다른 서브시스템을 어떻게 사용하게 할 것인지를 정해야한다. 서브시스템간의 커뮤니케이션을 제한하고 완화해나가며 순환을 방지해야한다.
수준3. 클래스로 분할 시스템에 필요한 모든 클래스를 구체화 하는 작업으로, 개별적인 클래스로 구현할 수 있을 만큼 자세하게 서브시스템을 분해해야한다. 클래스의 인터페이스가 정의되어 클래스가 나머지 부분과 상호작용하는 방법도 명시되어야한다.
수준4. 루틴으로 분할
클래스의 비공개 루틴을 상세히 설계한다. 인터페이스를 수정해야 될 수도 있다.
- 수준5. 내부 루틴 설계
각 루틴의 상세한 기능을 구현한다.
5-3. 설계 빌딩 블록: 발견적 학습
결정적인 프로그램 동작과 달리 소프트웨어 설계는 발견적 학습을 적용해야한다.
a. 현실 세계의 객체를 찾아라
설계의 대안을 규명하는 가장 정석적인 방법은 객체지향적 접근 방법으로, 현실 세계의 객체와 가상의 객체를 찾는 것이다.
- 객체와 객체의 속성을 식별
- 각 객체에 무엇을 할 수 있는지 결정
- 각 객체가 다른 객체에 무엇을 할 수 있는지 결정
- 각 객체에서 다른 객체에 보일 부분을 결정
- 각 객체의 인터페이스를 정의 : 공개 인터페이스와 보호 인터페이스 구분.
b. 일관성 있게 추상화 하라
좋은 클래스 인터페이스는 클래스의 내부 작업에 신경 쓰지 않고 인터페이스에 집중할 수 있게 해주는 추상화다.
- 잘 설계된 루틴에 대한 인터페이스는 저수준 세부 사항과 동일한 해택을 제공하며,
- 잘 설계된 패키지나 서브시스템에 대한 인터페이슨 고수준 세부 사항의 이득을 제공한다.
훌륭한 개발자는 루틴 인터페이스 수준, 클래스 인터페이스 수준, 패키지 인터페이스 수준의 단계적인 추상화를 제공함으로써 더 빠르고 안전하게 프로그래밍 할 수 있게 해준다.
c. 구현 세부 사항을 캡슐화하라
추상화가 객체를 높은 수준에서 볼 수 있도록 하는 걸면, 캡슐화는 해당 객체를 특정 수준 이상으로 볼 수 없도록 하는 것이다. 복잡한 세부 사항을 보지 못하게 하는 목적이 있다.
d. 상속이 설계를 단순화 할 수 있을 때 상속하라
약간 다르고 비슷한 객체를 발견했을 때, 유사한 클래스를 생성하여 상속을 사용해라. 상속은 추상화와 시너지 효과가 있다.
- 추상화는 서로 다른 수준에서 객체를 다룬다.
- 상속은 프로그래밍을 단순화한다.
문의 일반적인 특성에 의존하는 것을 처리하는 일반 루틴을 작성하고 나서 특정 종류의 문에 대한 작업을 처리하는 구체적인 루틴을 작성할 수 있다. Open()이나 Close()는 문에 종류에 상관없이 연산을 지원하는데 이러한 특징은 다형성이라 한다. 객체지향 언어는 상속과 다형성을 지원한다.
e. 비밀을 숨겨라(정보 은닉)
정보 은닉은 구조적 설계와 객체지향 설계 모두에 있어 기본적인 부분이다.
- 구조적 설계에서는 “블랙 박스”라는 개념이, 객체지향적인 설계에서는 캡슐화, 모듈성, 추상화 개념이 정보 은닉에 해당한다.
- 정보 은닉은 복잡성을 감추는 데 중점을 두고 있다.
- 시스템의 사소한 변경사항이 클래스 내부 루틴을 넘어 클래스 인터페이스를 벗어나는 범위까지 영향을 미쳐서는 안된다.
- 기능을 클래스 내부로 알릴지 비밀로 남길지 결정하는 것이 클래스 설계에서 매우 중요하다.
- 전역 연산을 루틴화, 사용자 정의 타입 사용, 상수 대신 이름 상수 사용 등의 예시.
- 정보 은닉의 장애물 : 정보의 지나친 배분, 순환 의존성, 전역 데이터로 오해받는 클래스 데이터, 성능 손해
- 정보 은닉에 대한 발견적 사고는 객체지향적인 사고가 하지 않는 설계 결정을 돕는다.
f. 변경될 것 같은 영역을 찾아라
변경의 효과가 루틴, 클래스, 패키지에 제한되도록 불안정한 영역을 고립시켜야 한다.
- 변경될 것 같은 항목을 찾고, 분류하고 고립시켜라.
- 변경될 가능성이 큰 영역 : 비즈니스 규칙, 하드웨어 의존성, 입력과 출력, 표준을 따르지 않는 언어 기능, 어려운 설계 및 구현 부분, 상태 변수, 데이터 크기 제약
- 변경이 발생할 가능성이 작을 때, 시스템에 큰 영향을 미치도록 설계해야한다.
- 핵심 부분을 먼저 식별하면 변경될 것 같은 영역을 예측하기 좋다.
g. 결합을 느슨하게 유지해라
결합은 클래스나 루틴(이하 모듈)이 다른 모듈에 얼마나 밀접하게 연관되어 있는지를 서술한다.
- 하나의 모듈이 다른 모듈에 의해 쉽게 사용 될 수 있을 정도로 느슨한 상태가 훌륭한 결합이다. 모듈간 의존이 낮은 것을 의미한다.
- 결합의 기준 : 크기, 가시성, 유연성
- 결합의 종류 : 간단한 데이터 매개변수 결합, 간단한 객체 결합, 객체 매개변수 결합, 의미론적 결합
- 느슨한 결합은 모듈이 추상화 수준을 높여주고, 프로그램의 복잡성을 줄여준다.
h. 일반적으로 널리 사용되는 디자인 패턴을 찾아라
- 패턴은 이미 만들어진 추상화를 제공함으로써 복잡성을 줄인다.
- 패턴은 대안을 제안하여 발견적 학습의 가치를 제공한다.
- 패턴은 설계에 대해 수준 높은 논의를 할 수 있게한다.
i. 다른 발견적 학습
- 응집력을 강하게 하라 : 발견적 학습의 목표는 가능한 한 응집력을 강하게 하는 것이다.
- 계층을 만들어라 : 계층은 소프트웨어의 주요 기술적 의무를 달성하는데 유용하다. 현재 다루고 있는 수준에만 집중 할 수 있다.
- 클래스 계약을 형식화하라 : 각 클래스의 인터페이스를 나머지 프로그램과의 계약으로 생각해보자.
- 책임을 할당하라, 테스트가 가능하도록 설계하라, 등등.
5-4. 설계 실천법
a. 반복
- 설계는 반복적인 프로세스다. 한 번 해결하고 나면 다른 접근 방법에 대한 통찰력을 제시하기 때문에 반복은 매우 유용하다.
- 상위 수준과 하위 수준 고려사항 사이의 범위를 결정하기 어렵다. 이러한 관점 전환은 훈련을 통해 개선 가능하다.
b. 분할과 정복
- 프로그램을 나누고 개별적으로 처리한다.
- 점증적인 개선이 가능하다.
c. 하향식과 상향식 설계 접근 방법
- 하향식 설계는 높은 추상화 수준에서 시작하여 설계 요소를 정의하고, 상세화 수준을 높여간다.
- 상향식 설계는 구체적인 객체를 규명하고 기본 클래스 집합을 일반화한다.
하향식 설계는 분해 전략으로 쉽고, 세부 구현 사항을 미룰 수 있다. 상향식 접근 방법은 결합 전략으로 필요한 유틸리티 기능을 초기에 파악할 수 있어서 간결하고 잘 구성된 설계를 만들 수 있다. 저수준 설계의 복잡성을 다루는 시점이 상이하고, 시스템 특성에 따라 장 단점이 나뉜다. 두 접근 방법은 상호보완적이다.
d. 실험적인 프로토타이핑
소프트웨어에서는 최소 부분적으로 문제를 해결하기 전까지 특정 서브시스템 설계가 작동하는지 알 수 없는 경우가 있다. 이러한 경우 프로토타이핑이 필요하며, 다음 원칙이 중요하다.
- 구체적인 설계 질문
- 버리는 코드 작성
- 최소한의 코드 작성
e. 협력적인 설계
조직 구성이 공식적이든 비공식적이든 상관없이 두 명이 한 명보다 낫다.
f. 설계를 얼마나 해야 하는가
g. 설계 작업 기록하기
설계 문서에 설계를 작성하는 것 외에 다양한 설계 기록 방법이 있다.
- 설계 문서를 코드 자체에 넣기.
- 설계에 대한 논의와 결정을 위키에 기록
- 이메일로 요약
- 디지털카메라를 사용
- 설계 플립 차트를 보관
- 적절한 상세 수준에서 UML 다이어그램을 작성
5-5. 잘 알려진 방법론에 대한 의견
지나친 선행 설계(Big Design Up Front; BDUF)에 의해 10년 동안 분위기는 “모든 것을 설계하라” 에서 “아무 것도 설계하지 말라” 쪽으로 이동했다. 하지만 BDUF의 대안은 선행 설계를 적게(Little Degisn Up Front; LDUF) 하거나 충분한 선행 설계를(Enough Design Up Front; EDUF) 하는 것이다.