수업/Software Design

[Software Design] Class and Method Design

hw-ani 2022. 12. 12. 20:57

어떤 Layer든 Class와 Method Design은 기본으로 들어가있다. 이런 Class/Method를 Design하는 것에 대해 자세하게 알아보자.

구현전에 Design은 무조건 선행돼야한다.

 

 

Design Criteria

design을 평가하기위한 몇가지 방법?이다.

1. Coupling : class/object/method들간가까운 정도를 판단한다. classes간에는 간단하게 message를 보낼 수 있나, 즉 association관계에 있나로 판단한다. 낮아야 좋다.

2. Cohesion : 한 class의 attributes나 methods가 단일 object를 지원하는지 확인해야한다. general하게 말하자면 Class든 Method든 같은 개념을 말해야 cohesion이 좋은 것이다. 이건 높아야 좋다.

3. Connascence : objects간의 독립성 정도로 판단한다.

 

Q. class에서 public이면 어디서든 호출할 수 있는데(== 어디서든 message를 보낼 수 있는데), couplig을 어떻게 판단?
A. 위에서 말했듯이 둘 사이 association 관계가 있는지로 판단한다. association이 있는 순간 둘은 coupling이 높아진다. 물론 코드상에서 기술적으로 말하자면 public은 어디서든 호출할 수 있는게 맞다.(완벽하게 어디서든은 아니지만 객체만 안다면 제한은 없다. 예를들어 main 함수에서 객체들 다 선언해놓고 이리저리 메시지 주고받을 수 있다.)
하지만 그런 식으로 아무 생각 없이 만드는건 좋은 디자인이 아니다. 우리가 만든 관계에 따라 작동하도록 만들어야한다. 해당 언어상에서 기술적으로 가능하다고해서 막 짜버리는 순간 제어할 수 없는 코드가 된다.

 

 

> Coupling

Coupling이 높으면 한 part에서의 변화가 다른 part에 영향을 준다.(좋지 않다.)

두 가지 종류의 Coupling이 있다.

1. Interaction Coupling : message 주고받는 것을 통해 측정한다. "association이 있는가?"

2. Inheritance Coupling : inheritance 구조를 보고 판단한다.

 

당연히 위 둘을 최소화하는 것이 목표이다.

1번은 messages를 제한함으로써 최소화할 수 있고(Law of Demeter),

2번은 generalization/specialization과 principle of substituability를 지원하는 경우에만, 쉽게말해 꼭! 필요한 경우에만 상속을 사용함으로써 최소화할 수 있다.

 

Interaction coupling을 줄이기 위해 message를 제한하는데, 그런 제한들을 정리해둔 법칙인 Law of Demeter라는게 있다.

※Law of Demeter※

"message는 특정 object에서

(1) 자기 자신으로

(2) 자신 class 혹은 그 super class의 attribute에 포함돼있는 object로

(3) method의 parameter로 넘겨받은 object로

(4) method로 생성된 object로

(5) global variable로 지정된 object로

만 보낼 수 있다."

애초에 C++, Java 등 PL에서 위 경우가 아니면 메시지를 못보내지않나?? 라고 생각할 수 있는데, 아니다.
당장 C++만 봐도 아무 일반 함수 내에 객체 다 선언해놓고 함수 막 호출할 수 있음. 그때 어떤 멤버함수 호출할때 연관관계 없는 object의 attribute 하나 같이 던져주면 그것도 메시지를 보내는 것이다. Java도 그냥 import만 돼있다면 new해서 객체 하나 만들어버리고 메시지 보낼 수 있음.
말고 static 변수같은 것도 아무데서 호출 가능할거임.
message가 헷갈리면 https://hw-ani.tistory.com/4 참고

demeter의 법칙 을 보면 좀 더 자세하게 설명돼있다. 한 오브젝트에서 다른 오브젝트의 멤버함수를 타고타고 가서 호출하는 경우도 위반하는 경우라고 보네.

 

디미터의 법칙을 따르더라도 coupling을 증가시킬 수 있다. 따라서 아래 표를 참고해 줄이도록 노력해야한다.(non PD layer에서 PD layer로 커플링이 있는건 어느 정도 예외라고 함)

아래는 interaction coupling의 type을 정리한 표이다. 위로 갈수록 interaction coupling이 낮아서 좋은 것이다.

함수에 대한 커플링을 판단하는 표이다. class 간의 커플링은 보통 그냥 association 유무 정도로 판단하는듯 하기도 하고, 아래 표 개념을 함수든 라이브러리든 특정 모듈에 적용하면 다 적용되긴 하는듯하다, 다른데선 굳이 method라고 특정짓지않고 module이라고 설명한다.

Data는 전달된 data의 모든 정보가 사용된 경우이다. 즉, 직접적인 coupling이 없는 것이 가장 좋지만, 만약 coupling이 있다면 data를 전달해서 모두 사용하는 것이 그나마 제일 낫다는 말이다. 아래로 갈수록 coupling이 높아서 좋지 않다.

Stamp는 전달된 data의 일부 정보가 사용된 경우를 말한다. Control은 전달된 data에 따라 if-else같은 조건문으로 나눠지도록 하는 경우를 말한다. Common and Global은 말 그대로 해당 object 바깥의 global 데이터를 사용하는 경우를 말한다.

제일 아래 worst case는 C++의 friend 함수 같은게 그 예시이다.

Control type이 나온다면 객체지향 개념을 잘못 적용했을 가능성이 크다. 애초에 조건으로 동작이 나뉜다면, 그 함수는 둘로 나뉘어서 다른 객체에 들어가있어야 했을 것이다.

 

 

 

 

 

> Cohension

Cohesion이 높아야 보기 좋은건 기본이고 데이터를 관리하기도 좋다.

세가지 종류의 Cohesion이 있다.

1. Method Cohesion : method가 하나보다 많은 operation을 수행하면 이해/구현이 어렵다.

2. Class Cohesion : class의 attributes와 methods가 하나의 개념을 나타내야(지원해야)한다.

3. Generalization/Specialization Cohesion : a-kind-of 관계일때 inherit을 해야 cohesion이 좋다. a-kind-of 여야 (association이나 aggregation이 아닌) 상속을 해야한다는 말도 맞지만, 반대로 a-kind-of가 아니라면 상속을 해선 안된다는 말이기도하다.

 

 

아래 표는 순서대로 각각 Method CohesionClass Cohesion의 type을 정리한 표이다.

Communicational 이라면 그냥 함수를두개로 분리하는게 맞다. / Temporal 같은건 좋지 않다. 모든 attributes는 각각 getter/setter가 존재해야지, 모든걸 한번에 다 처리하는건 좋지 않다.

 

Ideal은 당연히 cohesion이 아예 없는 경우이고, Mixed-Role은 같은 layer 안에서 개념이 좀 섞인 경우를 말하고, Mixed-Domain은 다른 Layer와 개념이 섞인 경우를 말한다.

Mixed-Domain은 "사용한다"는 개념과 "해당 class에 속한다"는 개념을 헷갈려 디자인하면 생길 수 있다. 혹은 편리하다고 layer끼리 섞어서 표현하기도 한다.→그렇게 하지 말 것.

 

Q. Class cohesion을 판단할때, normalization 때 들어가는 attributes도 고려해야할까?
A. 당연히 그렇지 싶다. 교수님께서도 Coupling이 올라가면 Cohesion이 내려가는 trade off가 있다고 하셨으니...

 

 


Object Design Activities

Object Design Activities는 지난 글에서 봤던 "분석 모델이 디자인 모델로 진화되는 과정"의 연장선이라고 보면 된다.

partitions과 layers/classes의 세부사항을 아래 항목들로 확장한다.(좀 더 구체적으로 된다는 말인듯...)

(쉽게 생각해서 분석할때 그린 Class diagram을 우리가 design 해가고 있는건데 그걸 하는 더 전문적인 방법)

 

1. 현재 model에 Specifications을 추가한다.

2. 재사용(Reuse)의 가능성을 확인한다.

3. design을 Restructuring한다.(구조변경)

4. design을 Optimizing한다.(최적화)

5. Problem Domain의 classes와 Programming Language를 매핑시킨다.

 

하나씩 자세히 보자.

 

 

 

1. Adding Specifications

1) Analysis model들을 review한다.

    : Classes가 모두 필요/충분한지 (불필요한게있는지 그리고 모든 Classes가 이걸로 충분한지)

    : attributes나 methods가 필요/충분한지 (불필요한게있는지 그리고 모든 attributes/methods가 이걸로 충분한지)

2) Classes의 Visibility를 점검한다.

    : Public/Private/Protected 같은 visibility가 옳게 돼있는지 점검한다.

3) Method signatures을 결정한다.

    : method이름/parameter/return type 등을 결정한다.

4) object가 지켜야하는 constraints(제약사항)를 정의한다.

    : preconditions(선수조건), post-conditions(후수조건), invariants(불변조건)

    : 이런 사항을 지키지 않았을때 어떻게 처리할지도 정의해야한다.

 

constraints 예시 (invarants)

 

참고로 design 중에 추가되는 class의 methods는 public이 아닐 수도 있다. analysis 중에 나온 함수들은 거의 100% 확률로 public이지만, design 중에 발견되는 함수들은 아닐 수도 있으니 잘 확인하자.

 

 

 

2. Identify Opportunities for Reuse

재사용성을 높이면 개발자는 편하기도하고 검증된 것들을 사용하고 개발기간도 단축할 수 있고, 여러 장점이 있다. 따라서 Reuse는 할 수 있는한 최대한 활용하는게 좋다.

Reusing시 대표적으로 아래 전략(들)을 적용한다. Layer에 따라 무엇을 선택할지 다르다.

 

1) Design patterns : 공통적으로 자주 발생하는 문제를 해결하기위해 classes를 그룹화해둔 것 (not a code)

2) Framework : application의 기초를 형성하는 구현된 class들의 집합. framework의 public 함수를 이용하면, 그걸 기반으로 (밑단부터 스스로 다 만드는 것 보다는) 비교적 쉽게 내 application을 만들 수 있다. 보통 만들어진 Framework의 classes를 상속하여 사용한다. 상속하기때문에 종속성이 생기므로 프레임워크가 변경되면 다시 컴파일해야할 수도 있다.

3) Class libraries : Framework처럼 이미 구현된 class들의 집합이지만, 특정 목적을 가지는 framework에 비해 general하다.

4) Components : 특정 기능을 제공하기위해 plug-in으로써 사용되는 self-contained classes이다.

 

 

몇가지 기초적인 Design Patterns에 대해 알아보자.

> Singleton pattern

: 특정 class의 object를 하나만 만들고 싶을때(하나만 만들어야할때) 사용할 수 있는 패턴이다.

constructor를 private으로 만들어버려서 함부로 접근할 수 없도록 한다.

getInstance() 함수를 호출하면 class 내부에 있는 instance를 반환받을 수 있다.

 

(구현 방법이야 하기나름이긴한데)

구현시에 C++/Java 둘 다 보통 singleton 객체와 getInstance() 함수를 static으로 만든다.(java는 모르겠는데 C++은 static으로하거나 pointer로 할거아니면 class 내부에 자기자신은 포함못함.)

 

 

> Iterator pattern

aggregate 자료의 elements를 하나씩 접근할때 그것의 representation 같은 세부사항에 노출되지 않고 접근할 수 있도록 해준다.

각 element가 어떤 자료구조인지 representation을 encapsulates함으로써 접근시 일관성을 가진다.

실제 구체적인 iterator나 aggregate 자료형은 interface를 상속받아서 만들어진다. 즉 iterator나 aggregate 자료형은 세부사항이 각기 다른 여러개가 나오지만, iterator 틀은 맞춰주며 구현된다는 말이다.

client는 그럼 그 interface에 맞춰서 iterator을 이용해주면 된다.

예시로 C++의 STL에 구현된 각종 iterator가 있다.(얘네는 generality때문에 상속으로 구현안되긴함)

 

 

> Facade pattern

불어로 대문이라는 뜻이다. 복잡한 subsystem으로의 dependencies를 최소화하여 간단하게 사용할 수 있도록 해준다.

subsystem들의 interface에 관하여 간단한 interface를 구현한다. 간단하게 생각해서 subsystems의 기능이 너무 복잡할때 하나의 큰 system으로 묶어버려서 그 큰 system의 interface로 이용하도록 하는 것이다.

subsystems와 dependency는 최소화돼야하고, Facade class에서 subsystems의 기능 외 추가 작업을 할 수도 있다.

 

Analysis model과 Design decisoin은 다른 것이다.
(이런 패턴같은건 design에 와서 생각해봐도 된단 의미인듯...?)

 

 

 

3. Restructure the Design

1. Factoring

    : method나 class를 분리해내고 재구성하는 과정이다.(이전 글에서 본 factoring이랑 비슷한듯)

      어떤 method를 어떤 class에 넣을지 재구성하기도 한다.

2. Normalization

    : Association Classes를 모두 1-to-m relation으로 변경한다.

      (간선 중간에 점선으로 끊지말고 제대로 연결관계 표시해서 넣으란 소리, ppt p.16 참고)

    : Classes간 association, aggregation 같은 모든 연관관계를 끊고 class의 attribute로 변경해야한다.

3. 상속관계가 generalization/specialization 의미만을 나타내는지 확인한다. (이건 뭐 계속보네)

 

 

위 그림은 normalization을 진행하는 예시이다.

마지막에서 Class들간 연관 관계를 표현했던 선은 사라지고 대응 관계(몇대몇인지)에 따라 (일반 데이터, 배열 등으로) 상대 Class의 attributes로 들어간다.

이렇게 연관관계를 내부 attributes로 변경하는 이유는 무엇일까.
association을 실제로 표현하는 방법이기 때문이다. 코드로 구현할때도 선같은건 없다. (지금은 Design을 보고서 코딩이 가능할 정도로 class D를 변경하고있으니,,,) 그리고 앞에서 본 demeter의 법칙을 따르려면 자기자신 class 내부에 오브젝트가 있어야 호출이 가능하다.(위에서도 말했지만,, 꼭 있어야 가능한 건 아니지만(편법으로 어떻게 할 수도 있겠지만) 법칙을 따르는 것이 좋다.)

(Chapter8 ppt p.18참고) 보통은 언어 자체에서 linked list 같은걸 지원해주기도 하는데 그러면 normalize할때 그냥 변경하면 되지만, 만약 지원을 안해주는데 linked list를 쓰고 싶다면 내가 직접 그런 자료를 관리하는 class를 중간에 만들어야한다.

 

 

 

4. Optimizing the Design

(non-funcitonal requirements에 해당)

efficiency도 SW 설계에선 간과할 수 없는 측면이다. 대게 효율이 좋아지면 readability라던가 다른 측면이 안좋아지는 경향이 있어서 understandability와 efficiency 사이의 밸런스를 잘 맞춰야한다.

 

아래는 최적화하는 몇가지 방법이다.

1. objects간의 접근 경로를 점검한다.

    : 예를들어 A-B-C-D 순으로 class간의 associations이 있다면 A에서는 D의 함수를 호출하기위해 3번을 건너가야한다.

      이런 경우에 A와 D를 바로 연결함으로써 그 사이 불필요한 함수 호출을 줄일 수 있다.

2. 각 Class의 모든 Attributes를 점검한다.

    : 예를들어 어떤 class의 methods가 attributes를 read/update할뿐이고, 그걸 접근/이용하는 외부 class가 하나 뿐이라면,

      굳이 분리하지말고 그 attributes를 이용하는 class와 합쳐버리면 효율이 더 좋아진다.

3. direct/indirect fan-out을 점검한다.

    : 전자는 method에서 보내는 messages 수이고, 후자는 반대로 다른 methods에서 오는 messages 수이다.

    : fan-out이 높은 methods는 최적화될 필요가 있다.(메시지수를 줄여야되겠지?)

4. 자주 사용되는 methods에서 statements의 호출 순서를 잘 고려해본다.(비효율적인게 있는지)

    : 재배치하면 효율이 더 좋아질 수도 있다.

5. derived attributes와 triggers를 만듦으로써 re-computation을 방지한다.

    : 아래 그림처럼 하는 것이다.

      정석대로라면 다른 Classes의 public methods를 호출해서 접근해야겠지만,

      다른 Classes의 attributes를 볼 일이 잦다면 이렇게 아예 넣어버려서 함수호출의 overhead를 줄일 수 있다.

      저렇게 해당 class의 attributes가 아닌 놈을 포함시킬때 이 놈을 derived attribute라고 한다.

      normalization하면 들어오지않나?→그래도 private영역보려면 getter써야됨.

6. one-to-one association인 두 classes를 합치는걸 고려해본다.

    : 두 classes가 간단한 1대1 관계라면 trade off를 고려해서 합치는 것도 나쁘지 않을 수 있다.

 

 

함수 호출은 overhead가 큰 작업이라 줄이는 것이 좋다.

 

 

 

5. Mapping PD to PL

우리가 디자인한 개념을 잘 지원할 수 있는 언어로 골라야한다.

예를들어 Java를 고를 거라면 MI 가 들어가있는 것을 풀어해쳐야하고, inherit이 지원이 안되는 언어라면 inherit을 없애야하고, 하다못해 OO를 제대로 지원 안해주면 OO design도 안될 것이다.

(그렇다고 너무 언어/구현을 생각하며 분석/설계를 해선 안된다. 전에도 말했지만 그러면 재사용성이 떨어진다. design model은 PL과 independent하다.)

 

MI를 단일 Inheritance로 풀어내는 예시는 강의자료 ch8 p.21에 있다. (시험나올라나)

간단하게 base class 둘 중 하나를 그냥 아예 derived class랑 합쳐버림. 그렇게하면 inheritance 하나가 줄어드니까 해결됨.

 

 


Method Specfication

Methods의 세부사항을 문서로 작성하는 것이다. 만약 Design팀과 구현팀이 다르다면 이는 필수로 작성해야한다.

이 문서만 봤을때 그대로 따라쳐도 구현이 가능할 정도가 될만큼 명확해야한다.

정해진 양식은 없지만 아래 정보는 꼭 포함해야한다.

- method name, class name 같은 일반적인 정보

- 이 methods를 trigger하는 특정 이벤트

- parameter와 return 값

- Algorithm specifications

- calculations, procedure call 같은 다른 해당 사항들

 

아래는 Method specification의 예시이다.

빨간색으로 표시된 부분은 Sequence Diagram을 통해 찾아낼 수 있다.

파란색으로 표시된 부분은 method내의 Algorithm Specification으로, 주로 Seudo code나 Activity Diagram을 사용해 나타낸다.