[Software Design] Analysis phase to Design phase
이전 분석 단계에서는 비즈니스적으로 필요한 것들을 결정하는 단계이다.
설계(Design) 단계에서는 드디어 "어떻게" 시스템을 만들지에 중점을 둔다.
(이전 분석 단계에서는 "어떻게"에 중점을 두지 말라고 했다.)
설계 단계에서의 목표는 구현하기에 적합한 설계의 청사진, 즉 설계도를 만드는 것이다.
분석과 설계 단계는 상호적으로 연관성이 매우 높으며
"going back and forth"가 많이 필요할 수 있음.
이것이 OOAD의 특성 중 하나인 "iterative and incremental" 이다.
설계 단계의 과정은
분석 -> 단계 -> 구현 -> 검증
으로 가는 단계에서의
분석 모델을 확인하고 검증한 다음, 분석 모델을 설계 모델로 발전시킨다.
패키지(class grouping)를 만들고 패키지 다이어그램을 사용한다.
그리고 설계 전략을 결정한다.
(분석은 무조건 스스로 해야하지만, 설계는 다른 팀에 맡기기도 한다. 우리가 외주라고 일컫는 그런 것들 말이다.)
먼저 분석 모델을 확인하고 검증하는 단계를 보자.
우리는 분석 단계에서 Functional view, Structural view, Behavioral view 의 분석 모델을 생성한다.
모델 간의 일관성을 유지하기 위해 모델 간의 균형을 조정해야한다.
그리고 각 모델의 정확도를 테스트 해야 한다.
Functional view 에서의 activity diagram, use-case diagram 등은 같은 functional requirements 를 설명해야 한다.
이제 분석 모델을 설계 모델로 발전시켜 보자.
분석 모델은 functional requirement에 중점을 뒀다.
설계 모델은 non-functional requirement을 포함해야 한다.
시스템 성능이나 시스템 환경 문제(분산 처리 vs 중앙 집중식 처리 / 사용자 인터페이스 / 데이터베이스) 등이 non-functional requirement 이다.
시스템은 유지보수가 가능하고 경제적이어야 하며 효율적이고 효과적이어야 한다.
설계 모델로의 발전을 위해선 세 가지 요소가 필요로 한다.
"factoring", "partitions&collaborations", "layers" 이다.
첫 번째 - Factoring
Factoring은 모듈을 독립 모듈로 분리하는 과정이다. 관심 단위 간의 유사성 및 차이를 설명하는 모듈을 생성하는 것이다.
이 새로운 모듈은 새로운 클래스일수 도 있고 새로운 메소드일 수도 있다.
예를 들어, 클래스 집합을 검토할 때 클래스 집합이 유사한 attribue 집합과 메소드를 가지고 있음을 발견할 수 있다.
위를 통한 형성된 새 클래스는 Generalization(a-kind-of) 관계 또는 Aggregation(has-parts) 관계를 지니게 된다.
Factoring은 크게 두 가지로 나뉘는데,
Abstraction(추상화)와 Refinement(세분화)로 나뉜다.
Abstraction은 높은 수준의 클래스를 만드는 것인데
일상 생활에 대입하면 예시는 다음과 같다.
매일 아침 집에서 직접 커피 머신을 통해 커피를 내려서 출근을 한다고 해보자.
커피 머신을 통해 커피를 내리는 것을 이야기하면,
물, 커피콩, 그리고 어떤 종류의 커피를 만들지 고를 필요가 있다.
여기서 중요한 점은 준비물만 있다면 커피 머신이 알아서 커피를 만들어 준다는 점이다.
즉, 물의 온도나 필요한 커피콩의 양은 알 필요가 없다.
그러한 복잡한 과정이 '추상화' 되었기 때문에 누구나 커피를 집에서 만들 수 있게 된다.
반면에, Refinement는 세부적인 클래스를 만드는 것이다.
두 번째 - Partitions and Collaborations
Partition은 긴밀하게 협업하는 클래스의 하위 시스템을 만드는 것이다.
시퀀스 다이어그램을 통해 활동 패턴을 분석함으로서 얻어낸 기본적인 partition이 존재할 수도 있고,
클래스 간의 결합이 더 크면 partition을 알아차릴 수 있다.
예를 들면, 회계 정보 시스템은 회계 지급 시스템, 회계 수령 시스템, 급여 시스템 등 기능적으로 분해가 가능하다.
세 번째 - Layers
이제 시스템 환경 정보를 추가해야한다.
Layer를 사용하여 소프트웨어 아키텍처의 요소를 표현하고 구분한다.
이를 이용하면, 복잡한 시스템을 보다 쉽게 이해할 수 있다.
다섯 가지의 Layer가 존재한다.
Foundataion
예를 들면 컨테이너 클래스, library를 이야기한다.
Problem domain
예를 들면 캡슐화, 상속, 다형성 등이다.
이 부분은 우리가 설계할 영역인데 바뀌지 않는다.
Data management
데이터를 저장하고 검색하는 곳이다.
User interface
데이터를 입력하는 양식이다.
이 부분은 변할 수가 있다.
Physical architecture
특정 컴퓨터나 네트워크를 말한다.
Package는 유사한 구성 요소, 예를 들어 use-case나 클래스 다이어그램 등을 그룹화한다.
Package Diagram은 패키지와 패키지의 관계를 보여준다.
Aggreagation과 Association 관계도 가능하다.
그리고 패키지끼리 서로 종속될 수도 있다.
하나의 패키지가 수정된 경우에 이에 종속된 다른 패키지도 수정이 필요하다.(Dependency)
보통 종속 패키지부터 패키지로 화살표가 그려진다.
"A가 바뀌면 B가 바뀐다" 를 화살표로 나타내면
B -------> A
이렇게 표기될 것이다.
Package Diagram을 만드는데 있어서 필요한 Guideline을 알아보자.
패키지 간 수직 위치 지정은 상속을 나타낸다.
그리고 수평 위치 지정은 aggregation과 association을 나타낸다.
use-case package diagram일 경우 actor 포함한다.
각 패키지에는 단순하지만 딱 봐도 알아볼 수 있는 이름을 사용해야 한다.
그리고 유사한 것들끼리 모아 응집도가 높게 만들어야한다.
Package Diagram을 만드는 순서를 간단히 정리한 것은 다음과 같다.
- 맥락 설정
- 공유 관계를 기반으로 클래스 클러스터링(군집화 -> 유사한 속성을 가진 클래스끼리 모음)
- 클러스터에서 패키지 생성
- 패키지 간의 종속성 관계 확인
- 패키지와 패키지의 종속성만 포함한 도표를 레이아웃하고 그리기
- 패키지 다이어그램 확인 및 검증
(위에서 '클러스터에서 패키지 생성' 이라는 문장은 바로 위 '클러스터링' 이란 단어와 연관 관계가 있는 것 같다.
클러스터링이 유사한 속성을 가진 클래스끼리 모으는 것을 의미하는데, 클러스터는 그 클래스를 모아놓은 집합체 아닐까 싶다.)
Design Criteria란, 설계를 평가하기 위한 측정 단위의 집합을 말한다.
그 단위에는 두 가지가 있는데,
Coupling / Cohesion
으로 나뉜다.
Coupling은 클래스 간 관계의 친밀도를 나타내는데, 결합도라고 불린다.
결합도가 높으면 좋지 않은 디자인이다.
Close coupling은 디자인의 한 부분이 변경되면 다른 부분의 변경이 필요할 수 있음을 의미한다.
(아마 Coupling이 높은 경우를 의미하는 것 같다.)
Coupling의 종류도 두 가지로 나뉜다.
Interaction coupling은 메시지 전달을 통해 측정된다.
메시지를 제한해서 interaction coupling을 최소화하는데,
이를 Demter의 법칙이라고 한다.
generalization/specialization과 substitutability 원칙만 지원하기 때문에
상속을 사용하여 inheritance coupling을 최소화한다.
substitutability 원칙은 객체 지향 5원칙 중 하나이다.
객체 지향 5원칙은 다음과 같다.
1. 단일 책임 원칙 (Single responsibility principle)
모든 클래스는 하나의 책임만 가지며, 클래스는 해당 책임을 완전히 캡슐화해야 함을 말한다.
클래스가 제공하는 모든 기능은 이 책임과 깊게 알맞아야 한다.
(응집성을 높이고 결합도를 낮춘다.)
2. 개방-폐쇄 원칙 (Open/closed principle)
소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야 하고,
수정에 대해서는 닫혀 있어야 한다는 프로그래밍 원칙이다.
(여기서 확장은 모듈의 동작의 확장을 이야기하고,
수정은 소스 코드나 바이너리 코드를 수정하는 것을 이야기한다.
이를 건드리지 않아도 모듈의 기능을 확장하거나 변경할 수 있다.)
3. 리스코프 치환 원칙 (Liskov substitution principle)
substitutability은 객체 지향 프로그래밍 원칙이다.
컴퓨터 프로그램에서 자료형 S가 자료형 T의 하위형이라면
필요한 프로그램의 속성의 변경 없이 자료형 T의 객체를 자료형 S의 객체로 교체할 수 있어야 한다는 원칙이다.
4. 인터페이스 분리 원칙 (Interface segregation principle)
인터페이스 분리 원칙은 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙이다.
인터페이스 분리 원칙은 큰 덩어리의 인터페이스들을 구체적이고 작은 단위들로 분리시킴으로써
클라이언트들이 꼭 필요한 메서드들만 이용할 수 있게 한다.
이와 같은 작은 단위들을 역할 인터페이스라고도 부른다.
인터페이스 분리 원칙을 통해 시스템의 내부 의존성을 약화시켜 리팩토링, 수정, 재배포를 쉽게 할 수 있다.
5. 의존관계 역전 원칙 (Dependency inversion principle)
객체 지향 프로그래밍에서 의존 관계 역전 원칙은 소프트웨어 모듈들을 분리하는 특정 형식을 지칭한다.
이 원칙을 따르면, 상위 계층이 하위 계층에 의존하는 전통적인 의존 관계를 반전시킴으로써
상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있다.
이 원칙은 다음과 같은 내용을 담고 있다.
첫째, 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.
둘째, 추상화는 세부 사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.
이 원칙은 '상위와 하위 객체 모두가 동일한 추상화에 의존해야 한다'는 객체 지향적 설계의 대원칙을 제공한다.
위와 나왔던 Law of Demter에 대해 알아보자.
해당 법칙의핵심은 객체 구조의 경로를 따라 멀리 떨어져 있는 낯선 객체에 메시지를 보내는 설계는 피하라는 것이다.
즉, 객체는 내부적으로 보유하고 있거나 메시지를 통해 확보한 정보만 가지고 의사 결정을 내려야 하고
다른 객체를 탐색해 뭔가를 일어나게 해서는 안 된다.
"Don't Talk to Strangers"
위와 같이 이야기한다.
Law of Demter은 강제되는 것은 아니다.
메시지는 오로지 object에 의해서만 전송되어야 한다.
- to itself = 스스로
- to objects contained in attributes of itself or a superclass = 그 자체 또는 슈퍼클래스의 속성에 포함된 object로
- to an object that is passed as a parameter to the method = 메소드에 매개 변수로 전달된 object로
- to an object that is created by the method = 메소드에 의해 생성된 object로
- to an object that is stored in a global variable = 전역 변수에 저장된 object로
Interacton Coupling의 유형은 다음과 같다.
- No Direct Coupling
이 메소드는 다른 것과 연결되어 있지 않다.
즉, 다른 것들을 부르지 않는다.
- Data
모듈 간의 인터페이스로 전달되는 파라미터를 통해서만 모듈 간의 상호 작용이 일어나는 경우의 결합도이다.
단순히 파라미터를 통해서만 데이터를 주고 받는다.
예시 코드는 다음과 같다.
public void foo() {
int result = makeSquare(5);
}
// foo와 makeSquare 간에 파라미터를 통해 데이터를 주고 받는다
public int makeSquare(int x) {
return x*x;
}
- Stamp
두 모듈이 동일한 자료 구조를 참조하는 형태의 결합도이다.
자료 구조의 형태가 변경되면 그것을 참조하는 모든 모듈에 영향을 주며
변경되는 필드를 실제로 참조하지 않는 모듈에도 영향을 준다.
호출된 메소드는 해당 함수를 수행하기 위해 객체의 일부만 사용한다.
예시 코드는 다음과 같다.
public void foo() {
// 이름과 메일주소를 생성자로 초기화한다.
Person p = new Person("한켤레", "abc@abc.com");
sendEmail(p);
}
// Data Coupling과 다른 점은 단순 데이터 타입이 아닌 오브젝트 형태의 자료 구조를 전달한다는 점이다
// 자바를 예로 들면, 클래스의 필드가 변경되는 경우 이를 참조하는 모듈에도 변경이 필요할 수 있다
public void sendEmail(Person person) {
// 메일 발송 로직
}
- Control
어떤 모듈이 다른 모듈 내부의 논리적인 흐름을 제어하는 요소를 전달하는 경우를 말한다.
예를 들면, 파라미터로 전달되는 값에 따라서 모듈 내부 로직의 처리가 달라지는 Flag 값 등으로 결합되는 형태다.
예시 코드는 다음과 같다.
public void foo() {
printCharge(true);
}
// 만약, 멤버라면 멤버의 가격으로 되는 방식이다
// 멤버인지 아닌지의 여부에 따라 내부의 로직 처리가 달라진다
public void printCharge(boolean isMember) {
if (isMember) {
printMemberCharge();
} else {
printNormalCharge();
}
}
- Common or Global
여러 모듈이 하나의 데이터 영역을 참조하여 사용하는 형태다.
전역 변수(global variable)를 예로 들 수 있다.
전역 변수의 변경이 여러 모듈에 영향을 끼칠 수 있다.
예시 코드는 다음과 같다.
class Example {
// 클래스 변수, 다른 클래스에서 호출 가능
static int a = 5;
// 인스턴스 변수, 같은 클래스에서 호출 가능
int b = 2;
}
public void methodA() {
// a 또는 b 값 참조
}
public void methodB() {
// a 또는 b 값 참조
}
// 전역으로 선언된 변수를 서로 다른 모듈에서 참조하는 경우다
// 자바 언어를 예로 들었을 때, 클래스 변수와 인스턴스 변수를 사용하여 조작하는 경우다
// 위 코드도 methodA, methodB 모두 Example 클래스를 참조한다
- Content or Pathological
어떤 모듈이 사용하려는 다른 모듈의 내부 기능과 데이터를 직접 참조하는 경우다.
다른 모듈의 로컬 데이터에 접근하는 경우처럼 사용하고자 하는 모듈의 내용(코드)을 알고 있어야 한다.
모듈이 변경이 발생하는 경우 이를 참조하는 모듈의 변경이 반드시 필요하게 되므로 가장 좋지 않은 결합이다.
Cohesion은 모듈(클래스, 객체 또는 메소드)이 시스템 내에서 얼마나 하나로 묶여있는지를 의미한다.
많이 쪼개면 쪼갤수록 독립성이 증가한다.
Cohesion의 유형은 두 가지로 나뉘는데 다음과 같다.
Method cohesion은 하나의 메소드가 하나의 기능보다 더 실행하는지를 판단하는 정도이다.
두 개 이상의 작업을 수행하는 것은 이해하고 구현하기가 더 어렵다.
Class cohesion은 attribute와 메소드가 하나의 객체를 나타내는지를 판단하는 정도이다.
클래스는 클래스의 역할과 도메인 또는 객체를 혼합해서는 안된다.
Generalization/specialization cohesion은 계층의 클래스는 연결 association 또는 aggregation이 아닌
"a-kind-of" 관계를 표시해야한다.
Method Cohesion의 유형은 다음과 같다.
- Functional
Functional Cohesion은 모듈 내의 모든 요소들이 하나의 기능을 수행하기 위해 구성된 경우를 말한다.
(예 : 현재 GPA를 계산한다.)
- Sequential
Sequential Cohesion은 첫 번째 것의 출력이 두 번째 것의 입력으로 사용되는 두 가지 함수를 결합한다.
(예 : 현재 GPA를 포맷하고 검증한다.)
예시 코드는 다음과 같다.
public void someMethod() {
String content = readFile();
writeFile(content);
}
// 어떤 모듈이 특정 파일을 읽고 처리하는 기능을 하는 등과 같다
- Communicational
Communicational Cohesion은 모든 요소들이 동일한 입력 또는 출력 데이터를 사용하여 서로 다른 기능을 수행하는 경우다.
앞서 살펴본 순차적 응집도와 다르게 처리 순서가 중요하지 않다.
(예 : 현재 및 누적 GPA 계산)
- Procedural
Procedural Cohesion은 모듈내에서 여러 개의 기능 요소가 순차적으로 수행되지만
다음 기능 요소로 데이터가 아닌 흐름 제어 요소가 전달되는 경우다.
(예 : 학생 학점을 계산하고, 학생 기록을 출력하고, 누적 학점을 계산하고, 누적 학점을 출력할 수 있음)
- Temporal or Classical
Temporal / Classical Cohesion은 연관된 기능이라기보다는
특정 시점에 처리되어야 하는 활동들을 한 모듈에서 처리하는 경우다.
(예 : 파일을 읽을 때 접근 허가를 확인한 후에 파일을 읽는 형태 등)
- Logical
Logical Cohesion은 유사한 성격을 갖거나 특정 형태로 분류되는 처리 요소들로 모듈을 구성하며
논리적으로 비슷한 기능을 수행하지만 서로의 관계는 밀접하지 않은 형태다.
예제 코드는 다음과 같다.
public void someMethod(int val) {
switch (val) {
case 0:
// do something
break;
case 1:
// do something
break;
default:
break;
}
}
- Coincidental
Coincidental Cohesion은 모듈 내부의 각 구성 요소들이 아무런 관련 없이 구성된 형태다.
앞서 살펴본 논리적 응집도와 비슷하지만,
유사한 성격이나 형태가 없다.
Class Cohesion의 유형을 살펴보자.
- Ideal
어떤 혼합적인 응집도가 없다.
- Mixed-Role
클래스의 객체를 동일한 layer(예 : Problem Domain Layer)의 다른 객체와 연관시키는 하나 이상의 attribute를 가지고 있지만,
attribute는 클래스의 기본 의미론과는 관련이 없다.
- Mixed-Domain
클래스의 객체를 다른 layer의 다른 객체와 연결하는 하나의 이상의 attribute가 존재한다.
따라서 클래스가 나타내는 것의 기본 의미론과는 아무런 관련이 없다.
이러한 경우 문제가 되는 attribute는 다른 layer 중 하나에 위치한 다른 클래스에 속한다.
예를 들어서, Problem Domain 클래스에 위치한 포트 attribute는
Problem Domain 클래스와 관련된 시스템 아키텍처 클래스에 있어야 한다.
- Mixed-Instance
클래스는 두 가지 다른 유형의 객체를 나타낸다.
클래스는 두 개의 개별 클래스로 분해되어야한다.
일반적으로 서로 다른 인스턴스는 클래스의 전체 정의의 일부만 사용한다.
아무 상관없는 object가 모인다.
Design 전략에 대해서 알아보자.
분석과 설계를 나눌 때,
설계는 무조건 시스템을 만들려고 하는 자가 직접해야 한다.
하지만 설계는 남에게 맡기는 경우 또는 스스로 하는 경우가 있을 수 있다.
Coustom development는 처음부터 스스로 구축하는 것이다.
스스로 하는 경우라고 볼 수 있다.
Purchase packaged software는
오피스 제품군이나 엔터프라이즈 시스템 등이다.
설계 내에서 일부 시스템만을 구입하는 방식이다.
예를 들어, 어떤 시스템에서 결제 시스템에 대한 설계만 구입해오는 그런 것이다.
Hire an external vendor은
outsorce라고도 하는데,
우리가 말하는 '외부 공급 업체 사용' 즉, 외주를 말한다.
Custom Development는
매우 전문화된 요구 사항들을 충족할 수 있다.
남이 하는 것보다 스스로 하는 편이 요구 사항 충족에 있어 더 앞선다.
직접 하게 되면, 문제 해결에 있어 유연성과 창의성을 제공한다.
직접 하는 이유는 무엇일까?
굳이 돈을 쓸 필요가 없어, 능력이 되거나,
기술적 능력 향상이 필요할 때이다.
그리고 설계 내 구성 요소 변경이 용이하다.
하지만,
설계자들에게 과도한 부담을 줄 수 있고,
상당한 리스크가 있다.
Packaged Software는
이미 작성된 소프트웨어다.
오히려 더 효율적일 수 있다.
왜냐하면 이 소프트웨어는
보기 보다 철저하게 테스트되고 입증된 것이기 때문이다.
구성 요소에서 도구, 엔터프라이즈 시스템까지 다양할 수 있다.
그렇지만,
제공된 기능은 무조건 수용해야하며,
기업이 비즈니스를 수행하는 방식에 적용하려면 변화가 필요할 수도 있다.
상당한 "customization" 또는 "workaronds"가 필요할 수도 있다.
System Integration은
Packaged Software과 연관있는 것이다.
패키지, 레거시 시스템(= 낡은 기술이나 방법론, 컴퓨터 시스템, 소프트웨어 등을 말한다.), 새 소프트웨어를 결합하여
새 시스템을 구축한다.
원래 있던 소프트웨어를 구입하여 기존 시스템에 통합 아웃소싱 하는 경우가 흔하다.
가장 중요한 과제는 데이터를 통합하는 것이다.
새 패키지는 레거시 시스템과 동일한 형식으로 데이터를 작성해야 한다.
Object wrappers라는 것을 개발했다.
레거시 시스템을 API로 감싸 새로운 시스템이 레거시 시스템과 통신할 수 있도록 한다.
레거 시스템에 대한 투자를 보호한다.
Outsourcing은
시스템을 만들기 위해 외부 회사를 고용하는 것이다.
(아까 이야기했듯, 우리가 보통 이야기하는 외주다.)
각각 구매자와 판매자(?) 간의 정보 교환과 신뢰가 필요하다.
단점으로는 통제력을 잃어버리고, 기밀 정보가 유출된다는 점이다.
또한 전문 지식이 이전된다.
그래서 공급업체(vendor)를 신중하게 선택해야한다.
계약서 및 지불 방법을 주의 깊게 준비해야한다.
계약하는 유형에도 세 가지가 있는데 다음과 같다.
- Time-and-arrangement : 모든 시간과 비용을 지불
- Fixed-price : 합의된 가격을 지불
- Value-added : 이익의 비율을 지불
요약해서,
실제 설계를 시작한다고 해보자.
일단 먼저, 사내 개발에 필요한 도구나 기술을 결정할 것이다.
그리고 사용자의 요구를 충족하는 기존 패키지를 확인한다.
그리고 계약에 따라 구축할 수 있는 회사를 찾는다.
그 때 필요한 것은 가능한 각 선택에 따라 나타날 수 있는 장단점을 정리하는 것을 만들어야한다.
기술적, 경제적, 조직적 타당성을 모두 확인하고,
RFP 또는 RFI를 활용하여 잠재적 공급업체로부터 비용 및 시간 추정 수치를 미리 확인한다.