공부하는 블로그

#7 - 분석: 내가 만든 소프트웨어를 실제 세상으로 본문

design patterns

#7 - 분석: 내가 만든 소프트웨어를 실제 세상으로

devtimothy 2019. 3. 28. 21:19

#7 - 분석: 내가 만든 소프트웨어를 실제 세상으로

본 블로깅은 Head first OOAD: 세상을 설계하는 객체지향 방법론 (한빛미디어) 책을 Typescript 문법으로 전환하며 공부하는 글입니다.
글을 읽기 전에, 광고 배너 한번씩만 클릭 부탁드립니다. 블로그 운영에 큰 보탬이 됩니다 :)

소프트웨어가 출시되고 사용자들이 사용하기 시작하면 많은 불만이 나오기 시작한다. 지난 시간 개발한 강아지 음성 인식 기능에 대한 컴플레인으로 "옆집 개가 짖어도 문이 열린다" 라는 피드백이 들어올 수 있다.

여기서 현실 세계와 프로그래머가 꿈꾸는 완벽한 세계 사이의 간극이 벌어진다.

  • 가령 내가 개발한 프로그램의 경우에도 알림톡 기능과 사용자용 예약 앱을 만들면서 "알림톡 하단 버튼에 예약 상세정보 페이지 링크를 걸어서, 해당 레스토랑의 공지사항을 확인할 수 있게 한다면 매장주나 고객들이 유용하게 사용하겠지?" 라고 생각했는데 현실은 그런거 없다.
  • 고양이용 급식기를 만든적이 있는데, 매 8시간마다 사료를 일정량 배급해줄 수 있으면 완벽하겠지? 했는데 고양이가 배가 고파서 급식기 죽빵을 갈겨서 못쓰게 만든 경우도 있다. :sob:

위와 같은 (실제 사례이다.) 사례들로 인해서 우리는 충분한 경우의 수와 테스트가 필요하다.

문제점 찾기

현제 예제 프로그램의 문제는 "옆집 개가 짖어도 강아지 문이 열린다."라는 것이다. 그렇다면 어떻게 해야할까?

  • 강아지 소리 인식기가 모든 강아지들의 소리를 "듣는" 것까지는 좋다.
  • 강아지 소리 인식기가 소리를 듣고 그 집의 개인지를 판단해야 한다.
  • 그 집 개가 짖는게 맞다면 문을 여는 요청을 강아지 문에 보낼 수 있다.

물론 방법은 한가지만 정답이 아니다. 접근 방법이 다양할 수 있으니 상황에 알맞게 동작할 수 있으면 된다. 그리고 유스케이스를 다른 사람들에게 설명할 수 있는 것이 좋겠다. (나도 이해하지 못한 것을 남에게 설명할 수는 없으니까.)

유스케이스 업데이트

주 경로
  1. 주인 강아지가 밖에 나가려고 짖는다.
  2. 강아지 소리 인식기가 강아지 소리를 "듣는다"
  3. 주인 강아지가 짖으면 강아지 소리 인식기가 여는 요청을 강아지 문에 보낸다.
  4. 강아지 문이 열린다.
  5. 주인 강아지가 밖으로 나간다.
  6. 주인 강아지가 화장실을 이용한다.
    1. 문이 자동으로 닫힌다
    2. 주인 강아지가 들여보내달라고 짖는다
    3. 강아지 소리 인식기가 강아지 소리를 "듣는다"
    4. 주인 강아지가 짖으면, 강아지 소리 인식기가 여는 요청을 문에 보낸다.
  7. 주인 강아지는 안으로 들어온다.
  8. 문이 자동으로 닫힌다.
대체 경로들

2.1. 주인이 강아지가 짖는 것을 듣는다.

3.1. 주인이 리모컨의 버튼을 누른다.

6.3.1. 주인이 강아지가 짖는 것을 듣는다.

6.4.1. 주인이 리모컨의 버튼을 누른다.

강아지 소리 저장 유스케이스
  1. 주인 강아지가 강아지 문에 "대고" 짖는다.
  2. 강아지 문이 주인 강아지의 소리를 저장한다.

여기서 특이점은 강아지 소리 저장 유스케이스가 추가되었다는 점이다. 사실 이전 포스팅에서 고려가 되었어야 하는 부분이었을 것이다. 분석이라는 것이 바로 이러한 사태를 대비하는 일이다.

구현

이제 본격적으로 코드 구현을 할 것이다. 여기서는 세가지 케이스의 구현을 살펴볼 예정이다.

#1 - 간단한게 제일 좋지!

export type DogDoor = InstanceType<typeof DogDoor>;
export const DogDoor = class {
  private _open: boolean;
  private _allowedBark: string;

  constructor() {
    this._open = false;
  }

  public open(): void {
    console.log("The dog door opens.");
    this._open = true;
    setTimeout(() => {
      this.close();
    }, 5000);
  }

  public close(): void {
    console.log("The dog door closes.");
    this._open = false;
  }

  public isOpen(): boolean {
    return this._open;
  }

  get allowedBark(): string {
    return this._allowedBark;
  }

  set allowedBark(allowedBark: string) {
    this._allowedBark = allowedBark;
  }
}
import { DogDoor } from "./DogDoor";
export type BarkRecognizer = InstanceType<typeof BarkRecognizer>;
export const BarkRecognizer = class {
  private _door: DogDoor;
  constructor(dogDoor: DogDoor) {
    this._door = dogDoor;
  }
  public recognize(bark: string): void {
    console.log(`BarkRecognizer: Heard a ${bark}`);
    if (this._door.allowedBark === bark) {
      this._door.open();
    } else {
      console.log("this dog is not allowed.")
    }
  }
};

#2 - 객체지향적으로 짜보자!

import { Bark } from "./Bark";

export type DogDoor = InstanceType<typeof DogDoor>;
export const DogDoor = class {
  private _open: boolean;
  private _allowedBark: Bark;

  constructor() {
    this._open = false;
  }

  public open(): void {
    console.log("The dog door opens.");
    this._open = true;
    setTimeout(() => {
      this.close();
    }, 5000);
  }

  public close(): void {
    console.log("The dog door closes.");
    this._open = false;
  }

  public isOpen(): boolean {
    return this._open;
  }

  get allowedBark(): Bark {
    return this._allowedBark;
  }

  set allowedBark(allowedBark: Bark) {
    this._allowedBark = allowedBark;
  }
}
export class Bark {
    private _sound: string;
    constructor(sound: string) {
        this._sound = sound;
    }

    get sound(): string {
        return this._sound
    }

    public equals(bark: Object): boolean {
        if (bark instanceof Bark) {
            const otherBark: Bark = bark;
            return this.sound === bark.sound;
        }
        return false;
    }
}
import { DogDoor } from "./DogDoor";
import { Bark } from "./Bark";
export type BarkRecognizer = InstanceType<typeof BarkRecognizer>;
export const BarkRecognizer = class {
  private _door: DogDoor;
  constructor(dogDoor: DogDoor) {
    this._door = dogDoor;
  }
  public recognize(bark: Bark): void {
    console.log(`BarkRecognizer: Heard a ${bark.sound}`);
    if (this._door.allowedBark.equals(bark)) {
      this._door.open();
    } else {
      console.log("this dog is not allowed.")
    }
  }
};

위 경우는 Bark 객체가 비교를 처리하도록 위임한다. 여기서 위임에 대해 자세히 알아보자.

  • BarkRecognizer는 강아지 짖는 소리를 듣고 Bark 객체에 강아지 소리를 넣어 recognize() 메소드에 보낸다.
  • recognize() 메소드는 allowedBark 를 호출해서 주인 강아지의 소리를 담은 객체를 가져온다.
  • 강아지 소리 인식기는 소리 비교를 Bark 클래스에 위임한다. equals 메소드를 이용해서 객체가 같은지 먼저 알아본다.
  • 소리가 같은지 알아본다.

느슨한 결합의 장점

여기서 BarkRecognizer의 코드를 자세히 보자.

public recognize(bark: Bark): void {
    console.log(`BarkRecognizer: Heard a ${bark.sound}`);
    if (this._door.allowedBark.equals(bark)) {
      this._door.open();
    } else {
      console.log("this dog is not allowed.")
    }
  }

만약 Bark 클래스에서 wav 파일로 저장한다고 생각해보자. 강아지 소리 비교를 Bark 객체에 위임했기 때문에 BarkRecognizer의 코드는 변경할 필요가 없다. 위임은 객체를 다른 객체들의 구현상의 변화로부터 보호한다.

그동안 나의 어리석은 사고방식으로 인해서 얼마나 코드를 지저분하게 짰었는지, 하나 고치면 얼마나 많은 부분을 건드려야했는지 반성해본다.

좀 더 나아가서...

여태까지 구현한 코드는 사실 짖는 소리 하나만 인식을 한다. 개가 멍멍하고만 짖지는 않을 것이다. 낑낑대기도 할 것이고, 인식기에서도 소리를 여러가지를 녹음할 수 있다면 더 좋을 것이다.

위로 올라가서 유스케이스를 자세히 보면, 강아지 소리를 인식하랬지 강아지의 특정한 짖는 소리를 인식하라고 하진 않았다.

힝 속았지

유스케이스에서 명사들을 찾아보자. 유스케이스에서 사용되는 명사들은 대개 시스템에서 작성하고 집중해야 할 클래스들이다.

  • (주인) 강아지
  • 강아지 소리 인식기
  • 강아지 문
  • 주인
  • 요청
  • 리모콘
  • 버튼
  • 안/밖
  • 강아지 소리

사실 안, 밖, 주인, 요청 등의 단어는 클래스로 쓰이지는 않는다. 이는 시스템을 이해하고 상식 선에서 쓰여야 할 부분이다. 또한 리모콘에 버튼이 있어서, 버튼에 대한 클래스도 따로 만들지는 않았다.

#2의 케이스에서는 사실 유스케이스 3번을 오해했다. 초점이 주인 강아지에 가있기보다는 주인 강아지의 소리에 가있었다.

유스케이스에 대해서

유스케이스는 좋은 소프트웨어를 만드는 시작이다. 유스케이스는 클래스를 찾기 쉽게 만든다. 유스 케이스 없이도 잘만 프로그램 짠다! 라고 할 수 있다. 물론 유스케이스 없이 프로그래밍 잘 하는 사람들도 많다. 그러나 코드를 동작하게 하는 데 필요한 노력을 줄이려면 유스케이스가 요구사항을 정하는 데 도움이 될 것이다.

사실 책에서는 이렇게 설명을 하지만, 현대 우리의 소프트웨어 개발에서는 어떤 것을 할 수 있을까? 이슈를 발행해서 한단계씩 격파해 나가는 것 아닐까 생각한다. 깃헙 같은 좋은 프로그램들이 요새는 많지 않는가. ㅎㅎ

위에서 명사를 분석했다고 해서 문법에 너무 초점을 맞출 필요는 없다. ~하는 것이 있고, 그것이 코드로 구현할 필요가 있는지만 잘 판단한다면 그것만으로도 좋다.

잘 만든 유스케이스는 시스템이 하는 일을 명확히 그리고 정확히, 이해하기 쉬운 언어로 설명합니다.

유스케이스를 잘 만든 후에, 유스케이스의 본문 분석을 통해 시스템에서 필요한 클래스들을 빠르고 쉽게 찾을 수 있습니다.

Dog 클래스는 왜 없나요?

강아지는 시스템 외부에 존재하고, 시스템 외부의 것들은 나타낼 필요가 없다.

강아지는 SW 객체가 아니다. 시스템에 생물에 대한 정보를 오래 가지고 있어야 하는 게 아니면 생물체를 클래스로 나타내는 일은 대개 없다.

강아지 클래스가 있더라도 나머지 시스템에 큰 도움은 되지 않는다. 강아지 문에 강아지를 저장 할 수 없다.

코드상에서 DogDoor.allowedBarks는 강아지를 나타낸다. BarkRecognizer.recognize 의 목적 역시 어떤 강아지가 짖는지를 알아내는 것이다.

유스케이스 안의 명사들이 우리 시스템의 클래스가 되지 않더라도, 유스케이스에서 명사들에 집중하세요.

우리가 가지고 있는 클래스들이 어떻게 유스케이스가 설명하는 시스템의 동작을 도와줄 수 있는지를 생각해보세요.

유스케이스의 동사들은 대개 객체의 메소드이다. 유스케이스에서 동사들을 살펴보면,

  • 열린다
  • 닫힌다
  • 버튼을 누른다

이다. 동사들은 보통 우리가 구현해야 할 메소드들의 후보군이다.

다음 시간에는 클래스 다이어그램과 UML에 대해서 알아보고, 코드를 마저 구현해보자.

Comments