길고 길었던 우아한테크코스의 여정이 끝이 났다.

비록 합격이라는 좋은 결과를 얻지는 못했지만, 돌이켜보면 개발자로서 많은 것을 얻었던 시간이었다. 그중에서 객체 지향과 관련해서는 4주간의 프리코스 내내 스스로를 괴롭혔다.

1주차, 2주차, 그리고 4주차까지 미션을 진행하면서 내 코드에 대해 매번 만족했었다. “그래, 이정도면 됐다! 이 정도면 객체도 잘 분리했고, 요구사항도 잘 지켜졌고, 이보다 더 나아질 수는 없다!” 그리고 코드 리뷰를 받거나 공통 피드백을 확인하면 내가 제출했던 코드가 얼마나 잘못 작성되었는지 알 수 있었다.

해당 미션에서 무언가를 잘 지켜내면, 또 다음 미션에서 잘못된 무언가가 나오고, 이게 4주간 반복되었다. 객체 지향적인 코드를 작성하는 것은 정해진 것 없이 끝이 없는 작업이라는 것을 깨달았다. 그 말은 좋은 코드에 정해진 답도 없다를 의미하는 것 같았다.

하지만 완벽은 아니더라도, 더 나은 코드는 작성할 수 있지 않을까…?

프리코스 1주차, 그리고 코드 리뷰 글을 읽다보면 중간중간 객체지향 생활 체조 원칙 이 나온다. “이 코드는 객체지향 생활 체조 원칙 ~~에 의하면 ~~이렇게 바꾸는 것이 더 낫다고 하더라고요~” 를 수도 없이 봤다.

꽤 유명한 책이라 하였지만 이번 프리코스를 하면서 처음 알게 됐다. 여러 블로그의 글을 읽어보니 몇가지의 원칙으로 정리된 것을 확인할 수 있었다. 좋은 코드의 작성을 우선은 이 원칙을 기준으로 하기로 하였다. 나보다 훨씬 잘 쓰시는 분들이 많기 때문에 원칙에 대해서는 깊게 다루지는 않을 것이다. 총 4번 제출한 코드를 기준으로 무엇이 미흡했는지 한번 고민해보고자 한다.

한 메서드에 오직 한 단계의 들여 쓰기만 한다.

한 메서드가 깊어진다는 것은 하나의 메서드가 하나의 기능을 하지 못할 확률이 높다. 또한 깊이가 깊어진다는 것은 곧 가독성의 하락을 불러온다. 메서드의 길이가 길어질 것 같으면 해당 메서드를 여러 개로 분리하면 해결할 수 있다. 4주차까지 유일하게 잘 지켜온 부분이라 생각이 들었지만 4주차 크리스마스 미션 PromotionController 에서 그 생각은 와장창 깨져버렸다.

🧨 입력값 예외 처리 부분에서 적용 실패

// 크리스마스 프로모션 _ PromotionController
...
async #inputVisitDate() {
  while (true) {
    try {
      return await InputView.readDate();
    } catch (error) {
      Console.print(error.message);
    }
  }
}
...

입력값을 받고 유효성 검사한 후 예외 처리 메시지를 출력한다. 추가적으로 프로그램을 종료시키지 않고 재입력을 받아야 한다.

전자까지는 잘 지켜졌지만, 입력값을 잘못 입력했을 때 다시 입력받는 과정이 추가되면서 메서드가 너무 깊어졌다. 처음에는 while문 대신 재귀로 함수를 받았는데, 오류 값이 지속적으로 입력될 때 차지하는 메모리의 용량이 너무 커졌다. 그래서 while문을 적용하였지만…더 나은 방법이 있을까?

💎 해결 방안

  async #inputVisitDate() {
    let date = null;

    while (date === null) {
      date = await this.#handleInputDate();
    }

    return date;
  }

  async #handleInputDate() {
    try {
      return await InputView.readDate();
    } catch (error) {
      Console.print(error.message);
      return null;
    }
  }

두 개의 메서드로 분리하게 된다면 위 문제를 해결할 수 있다. 사실 전반적인 코드의 길이는 길어지긴 했지만 가독성 측면에서 매우 깔끔해진 것을 확인할 수 있다.

모든 원시값은 문자열을 포장한다.

🧨 유지 보수 잘할 수 있겠니…?

텍스트만으로는 이해하기 쉽지 않다. 4주차 미션이었던 크리스마스 프로모션 문제를 예로 설명해보자. 우선 총 금액을 아래와 같이 새로운 변수에 할당하였다.

const totalMoney = 123_000;

해당 금액으로 증정 상품 제공 여부도 파악해야 하고, 10,000 원이 넘는 금액인 지도 파악해야 한다. 그러면 아래와 같은 코드가 나올 것이다.

if (totalMoney >= 10_000) true;

const giveAwayCount = Math.floor(totalMoney / 25_000);

totalMoney 변수가 이곳 저곳에서 쓰이고 있다. 만약에 totalAmount 값을 유효성 검사하는 곳에도 사용한다면 중복된 값을 가진 변수가 코드 곳곳에 흩어지는 현상이 발생할 것이다. 만약에 코드를 수정해야 한다면 여러 곳을 찾아봐야 해서 유지보수가 힘들어진다.

💎 해결 방안

class TotalAmount {
  #menu;
  #totalAmount;

  constructor(menus) {
    this.#totalAmount;
    this.#calculateAmount(menus);
  }

  #calculateAmount(menus) {...}

  getTotalAmount() {
    return this.#totalAmount;
  }

  getGiveawayCount() {
    return Math.floor(this.#totalAmount / NUMBER.champagne);
  }
}

프리코스 내내 수도 없이 강조했던 능동적인 객체의 첫걸음은 행동하는 객체이다. 도메인의 역할이 오직 값만 가져오는 것이 아니라 객체에 책임과 역할을 부여하며 캡슐화하는 것이다.

TotalAmount 라는 클래스를 생성하여 원시값을 포장하였다. 해당 객체 안에서는 유효성 검사를 할 수도 있고, 총 금액 계산, 증정 메뉴 개수 카운트 등 여러 작업을 수행할 수 있다. 그리고 계산된 값만이 반환된다.

변수가 여러 곳에 흩어지지 않고 해당 TotalAmount 클래스 안에서만 구현하여 비즈니스 로직이 하나의 객체로 응집되었다. 이정도면 충분히 능동적인 행동을 하는 객체다운 객체를 생성한 것 같다.

한 줄에 점을 하나만 찍는다.

User.getMoney().getGiveawayCount(); // ❌

위와 같이 객체에 접근할 때 . 을 두번 이상 찍으면 안된다.

와… 이건 사실 좀 심화된 내용을 다루는 것 같아서 어려웠다. 흠… 의도한 것은 아니었지만 지난 4주간의 코드를 살펴보니 위 규칙을 어긴 코드는 없었다.

처음에는 . 을 하나만 찍으라는 것이 map 함수나 reduce 함수를 여러 번 사용하는 것도 해당되나..? 생각했지만 객체의 결합도가 높아져 결합도가 강해지는 것을 막는 것이 목표라고 한다.

흠… 이 부분에 대해서는 DTO를 알아야 하고 여러모로 공부할 점이 많아서 공부하고 나중에 한번에 정리 해야겠다. 🥲

모든 엔티티를 작게 유지한다.

모든 엔티티를 작게 유지한다는 것은 50줄이 넘는 클래스와, 파일이 10개 이상인 패키지를 지양하는 원칙이다. 보통 코드의 길이가 50줄이 넘는 클래스는 한 가지의 일이 아닌 그 이상의 일을 하고있을 확률이 높으며, 무슨 일을 하고 있는지도 정확히 파악이 어려워진다.

🧨 삐빕! 50줄을 초과하였습니다

흠… 맞는 말이다. 실제로 4주차 미션에서 BenefitAmount 클래스는 70줄이 넘는다. 코드를 작성하면서도 너무 길다는 생각을 안한 것은 아니다. 혜택을 정리하고 계산하는 작업 자체가 코드가 길어질 수 밖에 없기 때문에 충분히 가능하다고 스스로 합리화 하였다.

class BenefitAmount {
  #benefitList;
  #totalAmount;

  constructor(props) {...}

  #calculateDDayAmount() {...}

  #calculateWeekendAmount(order) {...}

  #calculateWeekAmount(order) {...}

  getBenefitList() {...}

  getBenefitAmount() {...}

export default BenefitAmount;

기능을 제대로 구현하지도 않았는데 벌써부터 읽기 싫어진다. 2개의 클래스로 분리가 가능할 것 같아서 살펴보니 BenefitAmountCalculator 라는 클래스와 BenefitAmountManager 라는 클래스로 나누어 전자에서 계산된 값을 후자에서 상속받아 사용하면 클래스의 기능적 무게가 훨씬 가벼워질 것 같다.

💎 해결 방안

// 총금액을 계산하는 클래스
class BenefitAmountCalculator {

  #calculateGiveaway() {...}

  #calculateDDayAmount() {...}

  #calculateWeekendAmount(order) {...}

  #calculateWeekAmount(order) {...}

export default BenefitAmount;
// 총금액을 관리하는 클래스

import BenefitCalculator from "./BenefitCalculator.js";

class BenefitAmountManager {
  #benefitList;
  #totalAmount;

  constructor(props) {...}

  getBenefitList() {...}

  getBenefitAmount() {...}

}

2개를 초과하는 인스턴스 변수를 가진 클래스를 쓰지 않는다.

인스턴스 변수가 많아질수록 클래스의 응집도는 낮아진다는 것을 의미한다. 클래스는 하나의 상태를 유지하고 관리하는 것두개의 독립된 변수를 조율 하는 두가지 종류로 나뉜다고 한다. 이것은 최대한 클래스를 많이 분리하게 함으로써 응집도를 높이는 역할을 한다.

🧨 이게 맞나…? 이렇게 하는게 맞아…?

사실 이 부분도 진짜 감이 안잡힌다.

class BenefitAmount {
  constructor(props) {
    ...
  }
}

#createBenefitObject({ order, event, totalAmount }) {
  return new BenefitAmount({
    order,
    benefitList: event.getEvent(),
    totalAmount,
  });
}

기존에는 인자 3개였지만, 이 원칙을 보고 최대 2개로 수정하고자 여러번 코드를 뜯어보았다. 하지만 매번 실패하였고 오랜 고민 끝에 props 로 인자를 묶어서 객체로 표현하였다. 생활체조 원칙도 지켜지고, 가독성 측면에서는 개선이 되었으나 아직까지 딱히 마음에 들지 않는다.

인자를 2개만 받도록 계속 수정중이다…

혹시 팁이 있으시다면… 댓글 남겨주시면 감사하겠습니다.

Getter, Setter, Property를 사용하지 않는다.

나왔다… 정말 고민이 많았던 Getter Setter 를 사용하지 않는 법. 물론 코드 작성에 정해진 법은 없었지만 3주차 공통 피드백에 이 말이 쓰여진 것을 봤을 때 내 코드가 한참 잘못되기는 했구나를 깨달았고 고민했던 것 같다.

우선 왜 Getter를 쓰면 안될까?

위에서 객체는 책임을 지게 해라! 라는 말을 기억하는가? 해당 객체에게 책임이 있으면 그 책임을 다른 객체에게 미루지 않고 본인이 시켜야 하는 것이다! 물론 그 말이 모든 코드에서 Getter를 쓰지 말아야 한다는 아닌 것으로 나는 이해했다. 지양하면 좋고, 없으면 더 좋다. 아예 안쓰는 방법도 분명 존재하겠지만 현재 나는 최대한 지양하는 방향 위에 서있다.(앞으로 학습하면서 안쓰는 날이 오지 않을까..? ㅎㅎ)

아래 코드를 살펴보자.

// BenefitAmount

class BenefitAmount {
  #benefitList;

  getBenefitList() {
    if (this.#totalAmount < NUMBER.benefitStandard) this.#benefitList.fill(0);
    return this.#benefitList;
  }

  getBenefitAmount() {
    return this.#benefitList;
  }
}
// PromotionController

class PromotionController {

  const benefitAmount = new BenefitAmount().getBenefitAmount();

  return benefitAmount.reduce((acc, cost) => {
      return acc + cost;
  }, 0);

과연 위 코드가 객체 지향적인 코드일까? benefitAmount 에서 맡을 수 있는 책임을 PromotionController 에 전가하여 해당 컨트롤러에서 계산이 이루어지고 값을 반환한다. 위 코드는 객체의 책임, 자율성을 무시한 코드이다.

💎 해결 방안

// BenefitAmount
getBenefitList() {
  if (this.#totalAmount < NUMBER.benefitStandard) this.#benefitList.fill(0);
  return this.#benefitList;
}

getBenefitAmount() {
  return this.getBenefitList().reduce((acc, cost) => {
    return acc + cost;
  }, 0);
}

// PromotionController
return new BenefitAmount().getBenefitAmount();

Reference

이유와 솔루션으로 정리하는 객체지향 생활체조 원칙

더 나은 소프트웨어를 향한 9단계: 객체지향 생활 체조(1)