[프리코스 1주차] 코드를 의심하기

pengooseDev·2023년 10월 25일
2
post-thumbnail

미끼를 물다

알 수 없는 불편함이 느껴지는 영화.
곡성을 보았는가?

그렇다면, 수 년이 지난 언젠가.
유튜브 알고리즘에 올라온 영화의 해석을 본 적도 있을 것이다.

우리가 당연하게 생각하며 보았던, 영화의 모든 장면들은.
사실 굉장히 이질적이고 기괴한 것들이었다.

우리가 영화를 보며 느꼈던 모종의 이질감과 위화감은 우리의 본능이 보낸 신호가 아니었을까.

생각을 정리하기도 전, 많은 정보가 주어진다면.
우리는 의문은 갖지 못한다.

다만, 우리의 본능은 위화감이라는 신호를 계속해서 보낸다.
그것이 불편함이다.

스스로 작성한 코드를 곱씹다, 느낀 그 소름돋는 위화감을 함께 살펴보자.


코드는 간단한 야구게임을 구현하는 예제이다.
각 객체의 역할을 확인해보자.

Player

  • 유효한 야구 번호인지 확인한다.
  • getter를 제공해 번호를 제공한다.
// Model/Player.js
import { validateBaseballNumber } from '../utils/validation.js';

export class Player {
  #number;

  constructor(number) {
    this.validation(number);

    this.#number = number;
  }

  validation(number) {
    validateBaseballNumber(number);
  }

  get _number() {
    return this.#number;
  }
}

Computer

  • Player를 상속받는다.
  • 주어진 숫자를 비교하여 strike와 ball의 결과를 계산한다.
// Model/Computer.js
import { Player } from './Player.js';

export class Computer extends Player {
  compareNumber(userNumber) {
    this.validation(userNumber);

    return this.#checkResult(this._number, userNumber);
  }

  #checkResult(answer, userNumber) {
    const defaultValue = { strike: 0, ball: 0 };

    return answer.reduce((acc, current, index) => {
      if (current === userNumber[index]) {
        acc.strike += 1;
        return acc;
      }

      if (userNumber.includes(current)) acc.ball += 1;

      return acc;
    }, defaultValue);
  }
}

위화감의 신호

1. 이중검증

  async #guessNumber() {
    const userNumber = await this.#view.readUserNumber(); // 지점1
    
    return this.#game.compareNumber(userNumber); // 지점2
  }

유저에게 입력을 받아 이를 Model에서 비교하는 로직으로
구현 당시에는 큰 문제를 느끼지 못한 코드이다.

  • 지점 1 : View에서 입력받은 숫자에 대한 검증을 진행한다.

  • 지점 2 : Player를 상속받은 Computer가 메서드를 이용해 정답을 확인한다.
    해당 메서드에서 다시 한 번 Model에서 숫자에 대한 검증을 진행하며, 탄탄한 안전성을 확보할 수 있었다.

문득, 보기엔 큰 문제가 없는 코드같다. 코드 작성당시 나 또한 나쁘지 않은 코드라 생각하며 어느정도 만족했다.

다만, 동일한 검증을 두 번 한다는 부분에서 알 수 없는.
그러나 무시하고 넘어가고싶도록 사소한 위화감이 신호를 보내기 시작했다.


2. 자식 클래스를 위한 private field 개방

원래의 코드이다.

export class Player {
  #numberList;

  constructor(numberList) {
    this.validation(numberList);

    this.#numberList = numberList;
  }

  validation(numberList) {
    if (numberList.length !== BASEBALL_NUMBER.DIGIT)
      throw new CustomError(MESSAGE.ERROR.INVALID_DIGITS);

	if (new Set(numberList).size !== numberList.length)
  	  throw new CustomError(MESSAGE.ERROR.DUPLICATE_NUMBERS);
	
	if (!numberList.every(Number))
	  throw new CustomError(MESSAGE.ERROR.INVALID_TYPE);
	
	if (!numberList.every(isBaseballNumber))
	  throw new CustomError(MESSAGE.ERROR.OUT_OF_RANGE);
  }

  get _number() {
    return this.#number;
  }
}

Player를 상속받는 Computer가 해당 validation을 사용하려면 해당 필드를 public field로 공개해야만 사용이 가능했다.

사실 이러한 것들을 변경하는 데, 당장 큰 cost가 들지는 않는다.
다만, 계속해서 알 수 없는 위화감과 불편한 기분이 드는 것은 사실이다.

이런 감정을 못본채 하며, 내가 한 선택은 다음과 같았다.


미끼를 물다

import { validateBaseballNumber } from '../utils/validation.js';

export class Player {
  #number;

  constructor(number) {
    this.validation(number);

    this.#number = number;
  }

  validation(number) {
    validateBaseballNumber(number);
  }

  get _number() {
    return this.#number;
  }
}
    1. 해당 메서드를 public field로 전환하였다.
    1. validation 로직을 Util함수로 분리하였다.

그 당시에는 모르고 지나쳤지만, 지금에선 소름이 돋는 부분이다.

문제의 본질이 나타났다.

야구 번호에 대한 validation 로직을 Util함수로 분리를 해야했는가?

위에서 언급한 "이중검증"과 같은 맥락이다.

현재 내 코드는 다음과 같은 문제를 가지고 있었다.


문제가 발생한 흐름 분석

1. Player는 진정한 Player로 추상화 되었는가?

export class Player {
  #number;

  constructor(number) {
    this.validation(number);

    this.#number = number;
  }

  validation(number) {
    validateBaseballNumber(number);
  }

  get _number() {
    return this.#number;
  }
}

아니다. Player는 BaseballNumber이다.


2. 잘못된 상속을 받은 Computer

Computer은 Player. 아니 BaseballNumber를 상속받는다.
당연히 잘못됐다.

BaseballNumber가 번호 비교를한다?
복권 샀는데, 복권이 구매자에게 당첨여부를 알려주는 경우가 있던가?

number에 대한 검증 로직이 private에서 public으로 바뀐 이유는 Computer가 BaseballNumber가 되려고 하기 때문이다.

문제점의 본질이 나타났다.

이를 빠르게 해결해보자.


해결책

문제를 파악했으니 해결은 간단하다.

  1. Player를 BaseballNumber 객체로 바꾼다.
  2. validation 유틸함수는 다시 private 필드로 캡슐화한다.
  3. Computer는 BaseballNumber를 상속받는 것이 아니라 number Field로 사용한다.
  4. 유저가 정답을 입력할 때, BaseballNumber 객체의 인스턴스로 생성한다.

변경된 코드

Player => BaseballNumber

import { BaseballNumberError } from '../Model/Error.js';
import { BASEBALL_NUMBER, TYPE, NUMBER } from '../constants/baseballNumber.js';
import { ERROR } from '../constants/error.js';
import { isBaseballNumber } from '../utils/baseballNumberUtils.js';

export class BaseballNumber {
  #numberList;

  constructor(numberList) {
    // 데이터 정규화
    const normalizedInput = this.#normalizeInput(numberList);
    // 검증
    this.#validation(normalizedInput);
    // 할당
    this.#numberList = normalizedInput.map(Number);
  }

  #normalizeInput(input) {
    //...codes
  }
  
  // 검증 로직을 다시 private화
  #validation(numberList) {
    if (numberList.length !== BASEBALL_NUMBER.DIGIT)
      throw new BaseballNumberError(ERROR.MESSAGE.INVALID_DIGITS);

    if (new Set(numberList).size !== numberList.length)
      throw new BaseballNumberError(ERROR.MESSAGE.DUPLICATE_NUMBERS);

    if (numberList.includes(NUMBER.ZERO))
      throw new BaseballNumberError(ERROR.MESSAGE.OUT_OF_RANGE);

    if (!numberList.every((value) => !isNaN(value)))
      throw new BaseballNumberError(ERROR.MESSAGE.NOT_A_NUMBER);

    if (!numberList.every(isBaseballNumber))
      throw new BaseballNumberError(ERROR.MESSAGE.OUT_OF_RANGE);
  }

  get _numberList() {
    return this.#numberList;
  }
}

외부에 넓게 퍼져있던 validation을 다시 캡슐화하였다.

또한, 어느정도의 유연성을 제공하기 위해, 생성자에서 입력받는 answer는 '123', 123, [1, 2, 3] 등의 타입을 지원한다. 물론, 안정성을 위해 validation도 철저히!


Computer

import { SCORE } from '../constants/baseballGame.js';
import { NUMBER } from '../constants/baseballNumber.js';
import { BaseballNumber } from './BaseballNumber.js';

export class Computer {
  #answerList;

  constructor(number) {
    this.#answerList = new BaseballNumber(number)._numberList;
  }

  compareNumber(userNumber) {
    const userNumberList = new BaseballNumber(userNumber)._numberList;

    return this.#checkResult(this.#answerList, userNumberList);
  }

  #checkResult(answer, userNumber) {
    const defaultScore = { strike: NUMBER.ZERO, ball: NUMBER.ZERO };

    return answer.reduce((score, current, index) => {
      if (current === userNumber[index]) {
        score.strike += SCORE.UNIT;
        return score;
      }

      if (userNumber.includes(current)) score.ball += SCORE.UNIT;

      return score;
    }, defaultScore);
  }
}

컴퓨터는 BaseballNumber를 field로 사용한다.

Computer가 BaseballNumber의 상위 Layer가 됨으로써 더 이상 BaseballNumber의 Validaiton 로직을 public으로 둘 필요가 없어졌다.

또한, 다른 사용처에서 BaseballNumber에 대한 검증이 필요한 경우 숫자를 생성자에 전달하여 인스턴스를 생성하면 모든 문제가 해결된다.


결론

코드를 짤 때, 꾸준히 의심하자.

의심만으로는 보이지 않을 때,
휴식시간을 가짐으로써 나를 코드로부터 분리하자.

객체들의 관계가 정리되며 객관적인 시야가 생긴다.

0개의 댓글