김동욱2
440
2017-08-11 16:59:43.0 작성 2017-08-11 17:17:40.0 수정됨
5
2692

코드의 의존성을 분리하는 방법을 예시 코드로 알아봅니다.


추상화, 의존성 분리, 캡슐화 등등... 말은 참 쉽지만 실전에서 와닿기 힘들 수 있습니다.

자바스크립트로 [Button]을 눌렀을 때 [Text]에 버튼을 누른 횟수를 표시하는 코드를 모델링해보겠습니다. (객체를 설계해보겠습니다.)


1. 어설픈 모델링


class Text {
  constructor(){
    this.view = document.createElement('div');
    this.view.textContent = "[0]";
    document.body.append(this.view);
  }
}

class Button {
  constructor(){
    this.view = document.createElement('button');
    this.view.textContent = "INCREMENT";
    document.body.append(this.view);

    this.clicked = 0;
    this.view.onclick = () => {
      text.view.textContent = `[${this.clicked++}]`;
    }
  }
}

let text = new Text();
let button = new Button();


위 코드는 잘 작동하지만 Button을 사용하기 위해서는 다음과 같은 의존성이 있습니다.

  • 전역 스코프에 text라는 객체가 있을 것
  • text는 view라는 객체를 속성으로 가질 것
  • text.view는 textContent라는 String을 속성으로 가질 것

혹은

  • 전역 스코프의 text라는 객체가 있을 것
  • text는 view라는 HTMLElement를 속성으로 가질 것

이 때문에

let text = new Text();
let button = new Button();

위 코드를

let button = new Button();
let text = new Text();

이렇게만 바꿔도 작동하지 않으며, 현실적으로 재사용하고 확장하기에 무리가 있습니다.


2. 추상화

  • 전역 스코프에 text라는 객체가 있을 것
  • text는 view라는 객체를 속성으로 가질 것
  • text.view는 textContent라는 String을 속성으로 가질 것

이 세가지 항목을 최대한 줄여볼 수 있을까요?
전역 스코프 사용을 자제하고, 모델을 약간 추상화해보겠습니다.

class Text {
  constructor(){
    this.view = document.createElement('div');
    document.body.append(this.view);
    this.render(0);
  }

  render(number){
    this.view.textContent = `[${number}]`;
  }
}

class Button {
  constructor(text){
    this.view = document.createElement('button');
    this.view.textContent = "INCREMENT";
    document.body.append(this.view);

    this.clicked = 0;
    this.view.onclick = () => {
      text.render(this.clicked++);
    }
  }
}

let text = new Text();
let button = new Button(text);


이런 방식으로 의존성이 줄어들 수 있겠습니다. 위 코드에서 Button의 의존성은

  • 생성자의 인자가 render(Number)를 속성으로 갖는 객체 일 것

의존성을 줄이려다보니, Text에 render라는 메소드를 만들게 됐습니다.
화면에 보이는 결과는 같지만 의존성이 줄었을뿐만 아니라, Text의 모습을 제어하는 코드 역시 자연스럽게 Text에 캡슐화가 되었습니다.


// 의사 코드
interface Renderable {
  void render(Number number);
}

class Button {
  constructor(Renderable renderer){
    // ...
  }
}


JS가 정적 타입 언어라면, 위처럼 Text의 생성자에 좀 더 제약사항을 걸어서 코드의 안정성과 가독성을 높힐 수도 있겠습니다.

이렇게 Renderable이라는 추상계층을 통해서 의존성을 줄일 수 있습니다.


3. 확장성

의존성이 단순하기 때문에 Button의 역할을 확장하기가 수월합니다.

새로운 요구사항

  • Button에 Renderable한 객체들을 여러개 연결하고 싶음


class Text {
  // ...
}

class Balls {
  constructor(){
    this.view = document.createElement('div');
    document.body.append(this.view);
  }

  render(number){
    this.view.innerHTML = '';
    for(let i=0; i<number; i++) {
      let ball = document.createElement('div');
      ball.style.background = 'orange';
      ball.style.width = ball.style.height = '1em';
      ball.style.borderRadius = '50%';
      ball.style.display = 'inline-block';
      this.view.appendChild(ball);
    }
  }
}

class Button {
  constructor(renderers){
    this.view = document.createElement('button');
    this.view.textContent = "INCREMENT";
    document.body.append(this.view);

    this.clicked = 0;
    this.view.onclick = () => {
      this.clicked++;
      renderers.forEach(renderer => renderer.render(this.clicked));
    }
  }
}

let text = new Text();
let balls = new Balls();
let button = new Button([text, balls]);


이렇게 해서 Button은 여러 Renderable과 느슨하게 결합 할 수 있습니다.

이제 Button의 의존성은 (엄밀하게 말하면 의존성은 Renderable 인터페이스 뿐입니다)

  • 생성자 인자의 타입은 Array<Renderable> 일것

4. 동적인 참조

그런데 아직도

let text = new Text();
let balls = new Balls();
let button = new Button([text, balls]);

let button = new Button([text, balls]);
let text = new Text();
let balls = new Balls();

로 바꿀 수는 없는 점이 아쉽습니다.

새로운 요구사항

  • Button을 생성하는 시점이 아니라, 생성된 이후에도 Button에 Renderable들을 연결하고 싶음


class Text {
  // ...
}

class Balls {
  // ...
}

class Button {
  constructor(renderers = []){ // 기본값을 할당하는 코드입니다.
    this.view = document.createElement('button');
    this.view.textContent = "INCREMENT";
    document.body.append(this.view);

    this.clicked = 0;
    this.renderers = renderers;
    this.view.onclick = () => {
      this.clicked++;
      this.renderers.forEach(renderer => renderer.render(this.clicked));
    }
  }

  connect(renderer){
    this.renderers.push(renderer);
  }
}

let text = new Text();
let button = new Button([new Balls()]);
button.connect(text);


이제 Button에 동적으로 Renderable들을 연결 할 수 있습니다.


5. 의존성 없애기!

Button이 Renderable들의 render 코드를 호출하는 대신에,
이벤트 주도(감시자 패턴, 출판-구독 패턴..) 방식으로 Renderable이라는 제약사항을 없앨 수도 있습니다.

새로운 요구사항

  • Balls, Text을 Renderable에서 탈피시키며 의존성을 완전히 없애기


class Text {
  constructor(){
    this.view = document.createElement('div');
    document.body.append(this.view);
    this.number = 0;
    this.count();
  }

  count(){
    this.view.textContent = `[${this.number++}]`;
  }
}

class Balls {
  constructor(){
    this.view = document.createElement('div');
    document.body.append(this.view);
  }

  generate(){
    this.view.innerHTML = '';
    let number = Math.floor(Math.random()*10);
    for(let i=0; i<number; i++) {
      let ball = document.createElement('div');
      ball.style.background = 'orange';
      ball.style.width = ball.style.height = '1em';
      ball.style.borderRadius = '50%';
      ball.style.display = 'inline-block';
      this.view.appendChild(ball);
    }
  }
}

class Button {
  constructor(text){
    this.view = document.createElement('button');
    this.view.textContent = text;
    document.body.append(this.view);

    this.clickHandlers = [];
    this.view.onclick = () => {
      this.clicked++;
      this.clickHandlers.forEach(handler => handler(this.clicked));
    }
  }

  addClickHandler(handler) {
    this.clickHandlers.push(handler);
  }
}

let balls = new Balls();
let text = new Text();

// increment button
let button = new Button("INCREMENT");
button.addClickHandler(() => {
  text.count();
});

// button for both
let button2 = new Button("CLICK ME");
button2.addClickHandler(() => {
  balls.generate();
  text.count();
});


이제 Button은 인스턴스별로 원하는 hook(callback, handler, listener, observer 등..)을 등록하여 입맛대로 쓸 수 있습니다.

* 아시는 것처럼 DOM에는 당연히 native로 각종 이벤트 핸들러를 부착할 수 있습니다, JS를 예시로 들다보니 같은 작업을 중복한 꼴이 되었습니다만, 지금 예시로 든 GUI 뿐 아니라 그 어떤 코드에서도 의존성에서 탈피하기 위해서 같은 원리를 적용 할 수 있음을 말하고자 합니다.


맺음말.

의존성에서 벗어나야 좋은 코드라는데, 위 처럼 의존성이 하나도 없는게 최고군요!

  • 의존성이 없으면 그만큼 확장성이 있지만, 원하는 대로 그 쓰임새를 강제 할수가 없습니다.

예를 들어서 현금,부동산 등을 등록 할 수 있는 회계사가 있다고 해봅시다.
회계사는 현금부동산과 현금으로평가할수있는 인터페이스로 느슨하게 연결되어 있습니다.


interface 현금으로평가할수있는 {
  금액 현금으로평가();
}


이를 통해서 회계사는 현금이나 부동산 등을 현금으로 평가하여 다룰 수 있습니다. 또한 비트코인이라는 새로운 객체에도 현금으로평가할수있는이라는 명시적인 인터페이스만 구현해준다면 쉽게 회계사와 연결 할 수 있습니다.

이렇게 적절한 의존성을 통해서 쓰임새의 방향을 어느 정도 강제하고, 확장의 가이드가 될 수 있습니다.

---

디자인 패턴을 공부해도 막상 실제 코드엔 적용하기가 쉽지 않습니다. 저는 코드의 구조에 그 합당한 이유가 있어야 한다는 생각을 합니다. 아무런 필요도 준비도 없이, 디자인패턴들을 되새김질하며 마치 공식처럼 적용하려고 하기보다는, 재사용, 확장성, 추상화의 키워드를 핵심 컨셉으로 "필요에 따라서 리팩토링" 해나가면 좋겠습니다.

16
14
  • 댓글 5

  • 꾸아앙
    753
    2017-08-14 08:40:16.0

    좋은글 감사합니다

    아직 객체지향적인 설계를 제대로 하지 못해서 이것저것 찾아보고 있었는데

    많은 도움이 됐습니다

    0
  • unigoon3
    251
    2017-08-14 19:26:07.0 작성 2017-08-14 19:28:53.0 수정됨

    뭔가 재미지네요. 랜더러들(뭐가 들었는지는 모르지만 디스플레이하는 함수를 갖는 객체들)을 받는 함수를 둔다는 것. 말하자면 버튼 클래스는 요구사항이 추상적/유연하기 . 때문에(좀 더 유연한 design by contract) 의존성이 줄어든다 이렇게 이해해도 될지 모르겠네요. 고정된 결합이 아니라 관절처럼 유연한 결합 -> 의존성이 낮춰진다. 유연한 결합은(커플링 낮춤) 즉 낮은 의존성. 본래 커플링 낮춤은 낮은 의존성을 의미한 것일테니. 그러면 의존성이 낮은 좋은 설계의 구체적 방법의 하나는 요구사항을 추상적/유연하게 하라..인터페이스라는 것의 목적이 본래 그런 것일테고..뫼비우스의 띠 처럼 뱅뱅 도네요.

    0
  • 김동욱2
    440
    2017-08-16 06:56:21.0

    네 객체 간에 결합도를 조절하는게 항상 딜레마죠..

    너무 추상화하면 껍데기뿐인 코드가 되고. 너무 구체화하면 재사용이 힘들어지구..

    1
  • 아이와사
    12
    2017-08-17 03:46:44.0

    좋은 글 감사합니다

    0
  • 수송대원아님
    3k
    2017-08-18 13:47:23.0

    재미있게 읽고 갑니다.

    0
  • 로그인을 하시면 댓글을 등록할 수 있습니다.