[NextStep] 모각코 + 로또미션 Feedback

pengooseDev·2023년 8월 20일
1

오늘은 준님이 준비하신 오프라인 세션겸 모각코가 있었다.

재밌는 세션들이 많았지만, 코드리뷰 사항을 분석하고 문제점을 개선하는 데, 집중하다보니.... 모든 세션이 끝나버렸다. 😅 머쓱...

하지만 큰 울림을 얻어갈 수 있었다.
그건 바로...


로또 STEP 1,2 Code Review

이전 미션에서 나의 문제점이 확장성을 고려하지 못한 코드였다면, 이번 미션에서 드러난 나의 문제점은 정반대인 오버 엔지니어링이었다.

멘붕이었다. 🤤

분명 아침까지만 하더라도, PR을 날리며 "아름답다..." "미쳤다..."라고 말을 했는데, 야무지게 뚜드러 맞은 것이다.

5시간 동안 세션에 관련된 사항을 전부 잊은 채, 노트북에 머리를 박고 다른 팀원들 뿐 아니라 closed된 모든 PR들을 하나하나 분석했다. DI의 장단점을 다시 공부한 뒤, 나의 코드를 분석하는 시간을 가졌다. 시간은 정말 빠르게 지나갔다.

DI 로직을 제거하고, 불필요한 추상화를 걷어내니 모각코 시간이 끝나 짐을 싸야했다. 급하게 commit을 날리고 시무룩 시무룩 😥 짐을 정리하였다.

이번엔 잘 짰다고 생각했는데... 아직까지도 tradeoff를 계산하지 못하며, 너무 부족하다는 느낌이 강하게 들었다.

그래도 어쩌랴. 덤덤히 앞으로 나아가야지.

짐을 싸느라 마지막으로 나가게 되었는데, 마침 준님이 이야기를 듣고 격려의 말씀을 남겨주셨다.

한 번씩 오버 엔지니어링언더 엔지니어링의 정점을 찍어봐야, 그 경계를 잘 알게 되는 것 같아요! 😀

어흑.. 😥


나의 오버 엔지니어링

위에서 언급했듯이 이번 피드백의 내용은 오버 엔지니어링이었다. 이유는 크게 4가지로 나뉘었다.

  1. 의존성 주입(DI)을 사용한 근거가 명확했는가?
  2. Class를 잘 사용하고 있는가?
  3. 추상화가 과도하게 이루어지지 않고있나?
  4. 각주가 유의미한 역할을 하고있는가?

우선 현재 코드에 존재하는 의존성 주입 로직에 대해 살펴보도록 하자.

현재 코드

플레이어가 로또 복권을 구매해야 한다.
LottoCorporation(로또 회사)는 로또를 판매할 Store 객체를 DI 받는다. Store는 판매할 LottoTicket 생성자 함수와 가격을 DI받아 판매한다.

class App {
  #view;
  #lottoCorporation;
  #controller;

  constructor() {
    this.#view = new View();
    this.#lottoCorporation = new LottoCorporation(new Store(PRODUCTS));
    this.#controller = new GameController(this.#view, this.#lottoCorporation);
  }

  // ...codes
}

Store는 DI 받은 상품(여기선 LottoTicket)을 판매하는 역할을 진행한다.

import { Validator } from '../utils/Validator';

export class Store {
  #products = new Map();
  #validator = Validator.Store;

  constructor(products) {
    Object.entries(products).forEach(([productName, productData]) => {
      this.#products.set(productName, productData);
    });
  }

  buyProduct(productName, purchaseAmount) {
    this.#validator.validateProductName(this.#products, productName);
    const { price: productPrice, product } = this.#products.get(productName);
    this.#validator.validatePurchaseAmount(purchaseAmount, productPrice);

    const productAmount = this.#calculateProductAmount(
      productPrice,
      purchaseAmount
    );

    return Array.from({ length: productAmount }, () => {
      return new product();
    });
  }

  #calculateProductAmount(productPrice, purchaseAmount) {
    return parseInt(purchaseAmount / productPrice);
  }
}

DI를 왜 사용했는가?

편해서 사용했다. 코드를 작성하는 사람의 머릿속에는 모든 데이터 구조와 객체의 상호작용 흐름이 영화처럼 흘러간다.
하지만, 다른 사람의 눈에는 그렇지 않다.

아무리 가독성 좋은 코드를 작성하고 JSDoc을 도입한다 하더라도, 코드의 복잡도가 올라가면 이해하기 어려워 더 많은 비용이 발생한다는 것이다.


DI의 장점과 단점

DI를 하면 객체 간의 결합도가 낮아져 확장성과 유지보수에 강점을 갖는다.
하지만, 단점도 존재한다. 코드의 복잡성 증가하며, 주입된 객체의 추적이 어려워져 가독성이 떨어진다.

그래서 DI 로직을 제거하기로 했다.

DI를 무엇으로 대체할 것인가?

해당 도메인의 Class에 결합도가 높더라도 직접 할당 하는 것도 고려해볼 수 있겠다. 예를 들어 팩토리 패턴이 그 예시이다.

class Store {
  constructor() {}
  
  getProduct(productName) {
    if (productName === "LottoTicket") return new LottoTicket();
    // ... 상품들
    
    throw new Error('존재하지 않는 상품입니다.');
  }
 }

다만, 이것 또한 과도한 추상화가 아닐까?
또한, 갯수를 확인하는 로직까지 추가적으로 구현해야 한다.

분명, 확장성이 굉장히 뛰어나지만, 추상화의 레벨과 코드의 복잡도가 여전히 필요 이상으로 높다고 볼 수 있기 때문이다.

결국 욕심을 덜어낸 Class

import { LottoTicket } from './';

class LottoStore {
  #ticket = LottoTicket; // 생성자 함수
  #price = LOTTO_TICKET.DEFAULT_PRICE;
  
  constructor() {}

  buyLottoTicket(purchaseAmount) {
    const quantity = parseInt(purchaseAmount / productPrice);
    
    return Array.from({ length: productAmount }, () => {
      return new this.#ticket();
    });
  }
}

구현은 굉장히 간단하다. 하지만, 이러한 의사결정에 도착하는데 참 많은 것들의 tradeoff를 계산해야한다. 앞으로 꾸준히 연습하도록 하자.

첫 피드백부터, 평생 들고가야 할 한 가지 교훈을 얻었다.

가독성과 확장성을 신경쓰되, 불필요한 추상화를 경계해라


현재 생각을 정리해보면

  • 객체는 역할별로 최대한 분리하되, 과도하게 분리하지 않는다.
  • 반복되는 부분을 추상화하여 분리하는 것을 고민해본다. 다만, 확장성을 운운하며 과도한 추상화를 진행하는 것은 반드시 지양한다.
  • 불필요한 각주는 지양하고 JSDoc을 최대한 활용하도록 한다. 남이 읽는다는 기준으로 객관안을 발동하여 작성을 진행한다.

쉽지 않다 쉽지 않아..!!

0개의 댓글