Structured Design

김기한·2022년 3월 6일
0

design

목록 보기
1/1
post-thumbnail

좋은 프로그램이란, 좋은 함수란, 좋은 서브루틴이란 높은 응집도와 낮은 결합도를 가질 수 있도록 작성해야한다.

높은 응집도와 낮은 결합도를 가지는 코드는 유지보수에 용이하다.

예를 들어, Stack을 제어하는 push, pop이라는 메서드가 존재한다고 가정했을 때, push는 A라는 클래스에 존재하고, pop은 B라는 클래스에 존재하게 된다면, 일관성을 느낄 수 없고 메서드 사용 시에 어느 메서드가 어느 클래스에 존재하는 지 외워서 사용할 수 밖에 없다.

또한 높은 결합도를 가지고 있는 코드를 생각해보면, 하나의 메서드를 호출하는데 필요한 객체가 무수히 많아진다. 이렇게 되면 클래스 하나의 수정이 필요할 때, 해당 클래스에 종속되어 있는 클래스들에 문제가 생기지 않는 지 모두 고려하여 수정해야하는 문제가 생기고, 이는 곧 유지보수의 어려움으로 이어진다.

결합도와 응집도를 단계별로 나누어진 모델이 존재하는데, 아래와 같다. 오른쪽에 있을수록 바람직하다.

결합도

Content > Common > External > Control > Stamp > Data

Content(강결합)

class A {
  constructor(v) {
    this.v = v;
  }
};
class B {
  constructor(a) {
    this.v = a.v;
  }
};
const b = new B(new A(3));
  • B 클래스가 A 클래스에 의존하고 있다.
  • A 클래스 유지보수 시, B 클래스가 망가지지 않을까 걱정해야함.

Common(강결합)

class Common {
  constructor(v) {
    this.v = v;
  }
};
class A {
  constructor(c) {
    this.v = c.v;
  }
};
class B {
  constructor(c) {
    this.v = c.v;
  }
};
const a = new A(new Common(3));
const b = new B(new Common(3));
  • Content 강결합과는 다르게 Common이라는 중심 클래스 하나를 A와 B가 의존하고 있다.
  • Common 클래스 하나를 잘 관리함으로써 유지보수를 할 수 있다.
  • 하지만, 여전히 Common 클래스에 대한 의존이 남아있기 때문에 결합도가 높다.

External(강결합)

class A {
  constructor(member) {
    this.v = member.name;
  }
};
class B {
  constructor(member) {
    this.v = member.age;
  }
};
fetch('/member').then(res => res.json()).then(member => {
  const a = new A(member);
  const b = new B(member);
});
  • 앞선 두 결합과 마찬가지로 external 결합도 강결합이다.
  • 하지만, external 강결합은 외부에서 전달받은 데이터이므로 해결할 수 있는 방법이 없다.
  • 위 코드에서는 fetch 요청을 통해 전달받은 member 라는 객체에 의존하고 있다.
  • member라는 객체의 스펙에서 name과 age라는 프로퍼티가 존재하지 않게 될 때, 해당 코드는 망가지게 된다.

Control

class A {
  process(flag, v) {
    switch(flag) {
      case 1: return this.run1(v);
      case 2: return this.run2(v);
      case 3: return this.run3(v);
    }
  }
};

class B {
  constructor(a) {
    this.a = a;
  }
  noop() {
    this.a.process(1);
  }
  echo(data) {
    this.a.process(2, data);
  }
};

const b = new B(new A());
b.noop();
b.echo(3);
  • flag에 따른 로직을 변경할 수 없다.
  • 예를 들어, 1번이 콜라 2번이 사이다 3번이 환타라고 했을 때, 2번을 사이다가 아닌 콜라로 변경이 일어났을 경우 B 클래스의 동작이 정상적으로 이루어지지 않는다.
  • A 클래스에 의존하고 있는 클래스의 망가짐에 대해 걱정해야하고 이는 유지보수의 어려움으로 이어진다.

Stamp(강결합 or 유사약결합)

class A {
  add(data) {
    data.count++;
  }
};
class B {
  constructor(counter) {
    this.counter = counter;
    this.data = {a:1, count:0};
  }
  count() {
    this.counter.add(this.data);
  }
};
const b = new B(new A());
b.count();
b.count();
  • A 클래스 add 메서드의 인자로써 전달 받은 data 객체 때문에 의존이 발생했다.
  • 인자로 전달 받은 data 객체에 count 프로퍼티가 존재하지 않으면 정상적인 작동이 되지 않는다.
  • B 클래스에서 전달해주는 data 객체 수정에 의해 A 클래스 로직이 망가지므로 유지보수에 어려움이 생긴다.

Data(약결합)

class A {
  add(count) {
    return count + 1;
  }
};
class B {
  constructor(counter) {
    this.counter = counter;
    this.data = {a:1, count:0};
  }
  count() {
    this.data.count = this.counter.add(this.data.count);
  }
};
const b = new B(new A());
b.count();
b.count();
  • data 객체에 어떤 프로퍼티가 있든 관계 없이 동작한다.
  • B 클래스의 data 객체를 수정할 때, 이제 다른 클래스의 망가짐을 걱정하지 않아도 된다.

클래스 로직을 작성할 때 낮은 결합도를 고려하기 위해, 해당 로직을 수정할 때 다른 클래스에 영향을 미치는 지 고려하며 작성해야한다.

응집도

Coincidental < Logical < Temporal < Procedural < Communicational < Sequential < Functional

Coincidental

class Util {
  static isConnect() {}
  static log() {}
  static isLogin() {}
  • 전혀 연관관계가 없는 메서드끼리 모아져있다.
  • 주로 개발 시에 Util 함수를 모을 때 발생한다.
  • 여러명이서 개발 시, 필요한 Util 함수를 찾지 못해 여러 개의 똑같은 동작을 하는 함수가 만들어질 가능성이 있다.

Logical

class Math {
  static sin(r) {}
  static cos(r) {}
  static random() {}
  static sqrt(v) {}
}
  • 도메인 의식이 동등한 사람들만 이해할 수 있다.
  • 위 코드처럼 sin, cos, random, sqrt가 수학에서 다뤄지는 내용이라는 것을 알고있는 사람들만 이해할 수 있음.
  • 도메인이 일반적인 내용일 경우 써도 무방하다.

Temporal

class App {
  init() {
    this.db.init();
    this.net.init();
    this.asset.init();
    this.ui.start();
  }
};
  • 시점을 기준으로 관계없는 로직을 묶음.

Procedural

class Account {
  login() {
    p = this.ptoken();
    s = this.stoken();
    if(!s) this.newLogin();
    else this.auth(s);
  }
};
  • 시점 기준이 아닌 절차적인 기준으로 순서가 결정된다.

Communicational

class Array {
  push(V) {}
  pop() {}
  shift() {}
  unshift(v) {}
};
  • 하나의 구조에 대해 다양한 작업이 모여있음.

Sequential

class Account {
  ptoken() {
    return this.pk || (this.pk = IO.cookie.get('ptoken'));
  }
  stoken() {
    if(this.pk) return this.pk;
    if(this.pk) {
      const sk = Net.getSessionFromPtoken(this.pk);
      sk.then(v => this.sk);
    }
  }
  auth() {
    if(this.isLogin) return;
    Net.auth(this.sk).then(v => this.isLogin);
  }
};
  • 실행순서가 밀접하게 관계되며 같은 자료를 공유하거나 출력결과가 연계됨.
  • 함수의 체이닝은 응집성이 좋은 예.
  • 먼저 체이닝 된 메서드가 먼저 실행되어 밀접하게 관계된 실행순서를 지켜줄 수 있기 때문.

Functional

  • 역할모델에 출실하게 단일한 기능이 의존성 없이 생성된 경우.
  • 프로그램을 작성하는 사람들은 Functional 코드를 작성하기 위해 노력해야함.

응집도와 결합도는 서로 반비례 관계다. 결합도를 추구하면 응집도를 챙길 수 없고, 응집도를 추구하면 결합도를 챙길 수 없다.

참조

profile
vgihan's velog

0개의 댓글