[회고] 우테코 프리코스 5기 3주차 회고

황준승·2022년 11월 15일
4
post-thumbnail

본 회고는 3주차 미션 수행을 하면서 제가 어떤 식으로 구현을 해내고 객체 설계 및 분리를 하려고 했는지 과정을 중심으로 작성한 글입니다.

코드 보러가기

📝 (계획 1) TDD 방식으로 개발하자 !!

앞서 우테코 2주차 미션에서 TDD를 해본 경험을 토대로 내가 생각하는 이상적인 TDD 개발방식을 정의했었다.

더 자세한 내용을 알고 싶다면 내가 작성한 우테코 프리코스 5기 2주차 회고를 읽어보자.

이 방식을 토대로 코드를 구현하기 전에 순서도 -> 기능 구현 목록을 작성 -> 객체 설계를 직접 해보았습니다. 그런 다음 테스트 코드 -> 기능 구현 -> 테스트 -> 커밋 과정을 반복했습니다 👍

제가 이번 미션을 통해서 코드 구현, 리팩토링 과정을 통해 3번기능 구현 목록 을 새로 작성하고 5번객체 설계를 다시 하면서 굉장히 고생했습니다 😭😭

이 부분에 대해서 좀 더 자세히 알고 싶으시다면 이 링크를 클릭하시면 확인 가능합니다!!

📝 (계획 2) bottom-up 방식의 개발을 하자!!

목표: 도메인 로직에 따라 각 기능 별로 클래스를 분리를 하고 구현을 한 다음 각각의 클래스를 끼워맞추기만 하면 전체 로또 게임 기능이 완성되는 이상적인 개발방식을 하자.

위의 이상적인 개발을 하기 위해 객체 지향의 원리를 이해하고 좋은 객체 설계를 위한 5원칙인 SOLID에 대해 학습하였습니다.

뿐만 아니라 유지보수하기 좋은 객체 구조에 대해 계속 고민하고 학습하고 이번 미션에 적용하려고 노력했습니다.

🕹️ (구현 1) 객체는 느슨하게... 느슨하게...

DI를 학습하다.

DI(Dependency Injection)란?
우리는 앞서 객체를 연결할 때 상위 객체 안에 새로운 객체의 인스턴스를 생성하는 방식으로 구현을 하였습니다.

하지만 DI의 경우 서로 다른 두 객체를 연결하기 위해 외부에서 객체를 생성해서 넣어주는 것을 의존성 주입이라고 합니다.

좀 더 자세한 설명과 예시를 알고 싶다면 제가 작성한 글인 JS로 알아보는 DI vs IOC vs DIP 에서 확인 가능합니다!!!

생성자 주입을 통해 DI를 구현, 내가 만든 각 객체들의 결합도를 낮추고 종속성을 감소시키며 테스트를 용이하게 만들었습니다.

DI를 통해 bottom-up을 이뤄내다!!

// 이때 this.#lotto, this.#bonus 모두 생성자이다.

  #drawBonus() {
    Console.readLine('보너스 번호를 입력해 주세요.\n', input => {
      this.#bonus = new Bonus(input);

      const lottoPayment = new LottoAdjustment({
        draw: new LottoDrawFactory({ lotto: this.#lotto, bonus: this.#bonus }),
        payment: this.#lottoStore,
      });

      lottoPayment.print();
    });
  }

각각의 클래스의 public 메서드 테스트 코드 작성 -> 클래스 구현 -> 테스트 -> 커밋 식의 TDD과정을 반복하여 기능 구현을 해나갔습니다.

구현한 각각의 클래스를 App에서 생성자 주입을 통해 DI를 구현 해 통합 테스트까지 통과했습니다!!

첫번째 구현 완료 후 객체 구조도

DI의 단점...?

예시를 통해서 DI의 단점을 한 번 살펴보자.

class Car() {
  constructor() {
    // ...
  }
  
  ...
}

class Mom {
   constructor(dish) {
     this.dish = dish;
   }
  
  cook () {
    console.log(`${this.dish.cook()}요리 완성`)!!
  }
}
  
const mom = new Mom(new Car());

위의 예시는 Mom과 Car 객체과 생성자 주입을 통해 DI를 구현하였다.

우리 현실 세계에서는 차로 요리를 할 수도 없고, 위의 코드 내에서도 cook()이라는 함수도 존재하지 않는다.

하지만 Mom객체는 아무것도 모르고 Car객체에게 요리를 하라고 명령을 하고 있다.

이처럼 Mom객체와 Car객체는 아무 이유없이 각자의 역할에 대해 서로 너무 믿고 있었고 의존적이었다고 볼 수 있다.

  • DI의 단점을 이해하고 단순히 생성자 주입을 통한 DI구현을 하는 것이 아니라 리팩토링을 통해 상위 모듈과 하위 모듈의 의존성을 줄이기 위해 의존성을 분리하려고 노력했습니다.
  • 의존성을 분리하기 위해 자주 쓰이는 다양한 디자인 패턴들을 학습하고 리팩토링 과정에서 적용하려고 노력했습니다.

📌 (리팩토링 1) 의존성을 분리하자.

LottoAdjustment클래스LottoStore클래스 사이의 의존성이 존재한다. 이를 추상화 메서드를 통해 의존성을 분리하려고 한다.

Factory Pattern(팩토리 패턴) 사용하여 의존성 분리하기

팩토리 메서드 패턴
객체 생성 처리를 서브 클래스로 분리해 처리하도록 캡슐화하는 패턴

팩토리 메서드 패턴

저의 프로젝트 같은 경우 LottoStore, Lotto, Bonus클래스를 통해서 입력값을 받고 해당 입력값에 대한 에러처리와 입력값을 가져옵니다.

최종 당첨 결과값을 도출하는 LottoAdjustment에게 입력값을 전달해줄 경우 굉장히 많은 값을 전달해야합니다.
(ex) 보너스 번호, 로또 번호, 구매한 로또 번호, 입력한 돈의 값 등등)

직접 인자값을 전달할 경우 유지보수적인 측면에서 굉장히 좋지 못하다고 판단, 팩토리 메서드 패턴을 사용하여 원하는 객체의 인스턴스값을 자유롭게 받아오는 방식으로 구현하려고 노력하였습니다.

코드를 보시면 쉽게 이해갈 것입니다.

class LottoDrawFactory {
  ...
  constructor({ lotto, bonus, lottoStore }) {
    this.#lotto = lotto;
    this.#bonus = bonus;
    this.#lottoStore = lottoStore;
  }

  getInstance(type) {
    switch (type) {
      case VARIABLE_FACTORY.lotto:
        return this.#lotto;
      case VARIABLE_FACTORY.bonus:
        return this.#bonus;
      case VARIABLE_FACTORY.lottoStore:
        return this.#lottoStore;
      default:
        throw new Error(VARIABLE_FACTORY.factoryTypeError);
    }
  }
  ...
}

리팩토링 후 객체 구조입니다.

📌 (리팩토링 2) UI 로직을 분리하자.

기능 구현을 모두 마치고 요구사항을 다시 한 번 살펴보다가 UI로직과 핵심로직을 구분하라는 요구사항을 보고 급하게 객체 구조를 수정하였습니다.

Strategy Pattern(전략 패턴) 사용하여 의존성 분리하기

전략 패턴(Strategy Pattern)
행위를 클래스로 캡슐화해 동적으로 행위를 자유롭게 바꿀 수 있게 해주는 패턴

전략 패턴에 대한 글

객체 구조 그림 중 UI 하위 클래스인 LottoPaymentComponent, LottoWinCountComponent, LottoIncomeComponent를 한 번 살펴보자.

각각의 클래스에는 화면에 보여주기 위한 print()함수가 무조건적으로 존재해야하고 이를 제한하기 위해 Component라는 상위 클래스에 print() 함수를 오버라이딩 함수로 선언 하여 하위 클래스에게 print()함수 사용을 강제하였습니다.

코드

class Component {
  #ERRORMESSAGE = '[ERROR] YOU SHOULD DECLARE OVERIDING';

  print() {
    throw new Error(this.#ERRORMESSAGE);
  }
}


class LottoIncomeUI extends Component {
  ...
  
  print() {
    Console.print(this.#template());
  }

  ...
}

class LottoPaymentUI extends Component {
  ...
  
  print() {
    const { lottos } = this.state;

    Console.print(this.#template());
    lottos.forEach(lotto => {
      const result = JSON.stringify(lotto.sort((x, y) => x - y)).replace(
        /,/g,
        ', ',
      );
      Console.print(result);
    });
  }
  
  ...
}

class LottoWinCountUI extends Component {
  
  ...
  
  print() {
    Console.print(this.#LOTTOSTATIC);
    Console.print(this.#SLASH);
    this.#template().forEach(setence => Console.print(setence));
  }

  ...
}

📌 (리팩토링 3) LottoAdjustment의 책임을 분리하자.

[리팩토링 2]의 객체 설계도를 보면 LottoAdjustment 클래스 내부에 1)수익률을 계산하는 함수와 2)맞춘 로또 개수를 구하는 함수 두 가지가 존재한다.

이는 객체 단일 책임원칙에 위배된다고 판단하고 아래와 같이 객체 구조를 수정하였습니다.
(+ LottoAdjustment -> LottoCalculator로 클래스 명을 수정하였습니다.)

Template Method Pattern(템플릿 패턴) 사용하여 의존성 분리하기.

템플릿 메서드 패턴

어떤 작업을 처리하는 일부분을 서브 클래스로 캡슐화해 전체 일을 수행하는 구조는 바꾸지 않으면서 특정 단계에서 수행하는 내역을 바꾸는 패턴

템플릿 메서드 패턴에 대한 글

수익률을 계산하는 LottoIncome 클래스, 로또를 맞춘 갯수의 수를 저장하는 LottoWinCount 클래스 모두 로또를 맞춘 갯수의 수를 저장한 데이터 자료구조를 필요로 한다.

그래서 이 로또를 맞춘 갯수의 수를 저장하는 배열 [0,0,0,0,0] 을 상위 클래스인 LottoCalculator 클래스에서 저장하고 구매한 로또와 당첨 로또의 수를 비교하여 해당 배열에 값을 채운다.

하위 클래스인 LottoIncome 클래스, LottoWinCount 클래스는 로또를 맞춘 갯수의 수를 저장하는 배열의 값을 이용하여 원하는 결과값을 만들어 낸다.

코드를 보시면 아마 쉽게 이해하실 수 있을 것입니다.

코드


class LottoCalculator {
  ...

  #ERROR_MESSAGE = 'OVERIDING_ERROR';

  constructor(inputs) {
    this.#lotto = inputs.getInstance(VARIABLE_FACTORY.lotto);
    this.#bonus = inputs.getInstance(VARIABLE_FACTORY.bonus);
    this.payment = inputs.getInstance(VARIABLE_FACTORY.lottoStore);

    this.#scoreBoard = [0, 0, 0, 0, 0];

    this.#validate().#compareLotto();
  }

  getResult() {
    throw new Error(this.#ERROR_MESSAGE);
  }

  #compareLotto() {
    this.payment
      .getLottos()
      .map(lottoToBuy => [
        this.#matchLottoFor(lottoToBuy),
        this.#matchBonusFor(lottoToBuy),
      ])
      .forEach(([lottoCount, bonusCount]) => {
        this.#setScoreToMatch([lottoCount, bonusCount]);
      });
  }
  
  ...
}
  
...

class LottoWinCount extends LottoCalculator {
  constructor(inputs) {
    super(inputs);

    this.#rank = {
      fifth: 0,
      fourth: 0,
      third: 0,
      second: 0,
      first: 0,
    };

    this.convertRankData();
  }

  getResult() {
    return this.#rank;
  }
  
  ...
}
  
...

class LottoIncome extends LottoCalculator {
  constructor(inputs) {
    super(inputs);

    this.#income = '';
    this.#calculateIncome();
  }

  getResult() {
    return this.#income;
  }

  #calculateIncome() {
    this.#income = this.#roundUpFor()
      .#makeDecimalFirst()
      .#checkDecimalPoint();
  }
  
  ...
}

📌 아쉬운 점

  • 객체 분리와 설계구조에 대해 너무 많이 아쉬움이 남습니다. 오히려 추상화 클래스때문에 구조가 복잡해진 느낌도 있고 개인적으로는 Bonus, Lotto, LottoStore 와 App 사이의 의존성도 없애면 더 좋았을 것 같다.
profile
다른 사람들이 이해하기 쉽게 기록하고 공유하자!!

0개의 댓글