Javascript

우아한 테크코스 6기: 프리코스 3주차 Front-End 피드백 정리

Jinmidnight 2023. 11. 12. 13:09

우아한 테크코스 6기 프리코스 3주차 과제에 대한 공통 피드백이 주어졌다.

프리코스 4주차 과제를 하기에 앞서, 더 나은 과제 수행을 위해 해당 피드백을 꼼꼼히 살펴보며 정리하고자 한다.


3주차 공통 피드백

함수(메서드) 라인에 대한 기준

프로그래밍 요구사항을 보면 함수 15라인으로 제한하는 요구사항이 있다. 이때 공백 라인도 한 라인에 해당한다. 15라인이 넘어간다면 함수 분리를 위한 고민을 한다.

-> 조금이라도 기능을 여러 개 가지고 있는 함수로 판단되면, 함수 분리를 고민해보자

 

 

발생할 수 있는 예외 상황에 대해 고민한다

정상적인 경우를 구현하는 것보다 예외 상황을 모두 고려해 프로그래밍하는 것이 더 어렵다. 예외 상황을 고려해 프로그래밍하는 습관을 들인다. 예를 들어 로또 미션의 경우 아래와 같은 예외 상황을 고민해 보고 해당 예외에 대해 처리를 할 수 있어야 한다.

  • 로또 구입 금액에 1000 이하의 숫자를 입력
  • 당첨 번호에 중복된 숫자를 입력
  • 당첨 번호에 1~45 범위를 벗어나는 숫자를 입력
  • 당첨 번호와 중복된 보너스 번호를 입력

 

-> 프로그래머라면 정상 상황보다 예외 상황을 더욱 많이 생각하여 작업할 수 있어야 한다

 

 

비즈니스 로직과 UI 로직을 분리한다

비즈니스 로직과 UI 로직을 한 클래스가 담당하지 않도록 한다. 단일 책임의 원칙에도 위배된다.

class Lotto {
   #numbers

   // 로또 숫자가 포함되어 있는지 확인하는 비즈니스 로직
   contains(numbers) {
       ...
   }

   // UI 로직
   print() {
       ...
   }      

}

-> UI 로직은 MVC의 Views에서만 담당할 수 있도록 하자. 비즈니스 로직Views 이외의 클래스들에서 담당하도록 하자

 

 

객체의 상태 접근을 제한한다

필드는 private class 필드로 구현한다. 객체의 상태를 외부에서 직접 접근하는 방식을 최소화 하는 이유에 대해서는 스스로 찾아본다.

class WinningLotto {
   #lotto
   #bonusNumber

   constructor(lotto, bonusNumber) {
       this.#lotto = lotto
       this.#bonusNumber = bonusNumber
   }
}

-> private class 필드를 사용하는 이유는 클래스 내부에서만 사용할 수 있도록 하는 캡슐화와 외부에서 임의로 데이터 값을 바꾸는 것을 방지할 수 있는 자료 보호 때문이다.

 

 

객체는 객체스럽게 사용한다

Lotto 클래스는 numbers를 상태 값으로 가진다. 그런데 이 객체는 로직에 대한 구현은 하나도 없고, numbers에 대한 getter 메서드만을 가진다.

class Lotto {
   #numbers

   constructor(numbers) {
       this.#numbers = numbers
   }

   getNumbers() {
       return this.#numbers
   }
}

class LottoGame {
   play() {
       const lotto = new Lotto(...)

       // 숫자가 포함되어 있는지 확인한다.
       lotto.getNumbers().contains(number)

       // 당첨 번호와 몇 개가 일치하는지 확인한다.
       lotto.getNumbers().stream()...
   }
}

Lotto에서 데이터를 꺼내지(get) 말고 메시지를 던지도록 구조를 바꿔 데이터를 가지는 객체가 일하도록 한다.

class Lotto {
   #numbers

   constructor(numbers) {
       this.#numbers = numbers
   }

   contains(number) {
       // 숫자가 포함되어 있는지 확인한다.
       return ...
   }

   matchCount(other) {
       // 당첨 번호와 몇 개가 일치하는지 확인한다.
       return ...
   }
}

class LottoGame {
   play() {
       const lotto = new Lotto(...)

       lotto.contains(number)
       lotto.matchCount(...)
   }
}

-> 데이터를 가지는 객체로직을 구현하도록 해야한다. 상태 데이터를 꺼내 로직을 처리하도록 구현하지 말고, 객체에 메시지를 보내 일을 하도록 리팩토링한다

 

 

필드의 수를 줄이기 위해 노력한다

필드의 수가 많은 것은 객체의 복잡도를 높이고, 버그 발생 가능성을 높일 수 있다. 필드에 중복이 있거나, 불필요한 필드가 없는지 확인해 필드의 수를 최소화한다.

예를 들어 총상금 및 수익률을 구하는 다음 객체를 보자.

class LottoResult {
   #result = new Map()
   #profitRate
   #totalPrize
}

위 객체의 profitRatetotalPrize는 등수 별 당첨 내역(result)만 있어도 모두 구할 수 있는 값이다. 따라서 위 객체는 다음과 같이 하나의 필드만으로 구현할 수 있다.

class LottoResult {
   #result = new Map()

   calculateProfitRate() { ... }

   calculateTotalPrize() { ... }
}

-> 불필요한 필드가 없는지 README 파일을 체크 및 수정하며 구현하자

 

 

성공하는 케이스 뿐만 아니라 예외에 대한 케이스도 테스트한다

테스트를 작성하면 성공하는 케이스에 대해서만 고민하는 경우가 있다. 하지만 예외에 대한 부분 또한 처리해야 한다. 특히 프로그램에서 결함이 자주 발생하는 부분 중 하나는 경계값이므로 이 부분을 꼼꼼하게 확인해야 한다.

test("보너스 번호가 당첨 번호와 중복되는 경우에 대한 예외 처리", () => {
   mockQuestions( ["1000", "1,2,3,4,5,6", "6"]);
   expect(() => {
       const app = new App();
       app.play();
   }).toThrow("[ERROR]");
});

-> 예외처리에 관한 부분은 모두 꼼꼼히 테스트 코드를 작성하자.

 

 

테스트를 위한 코드는 구현 코드에서 분리되어야 한다

테스트를 위한 편의 메서드를 구현 코드에 구현하지 마라. 아래의 예시처럼 테스트를 통과하기 위해 구현 코드를 변경하거나 테스트에서만 사용되는 로직을 만들지 않는다.

  • 테스트를 위해  # prefix를 바꾸는 경우
  • 테스트 코드에서만 사용되는 메서드

 

-> 테스트 코드구현 코드에 영향을 주지 않도록 작성하자

 

 

단위 테스트하기 어려운 코드를 단위 테스트하기

아래 코드는 Random 때문에 Lotto에 대한 단위 테스트를 하기 힘들다. 단위 테스트가 가능하도록 리팩터링한다면 어떻게 하는 것이 좋을까?

const MissionUtils = require("@woowacourse/mission-utils");

class Lotto {
   #numbers

   constructor() {
       this.#numbers = Randoms.pickUniqueNumbersInRange(1, 45, 6)
   }
}
---
class LottoMachine {
   execute() {
       const lotto = new Lotto()
   }
}

올바른 로또 번호가 생성되는 것을 테스트하기 어렵다. 테스트하기 어려운 것을 클래스 내부가 아닌 외부로 분리하는 시도를 해 본다.

const MissionUtils = require("@woowacourse/mission-utils");

class Lotto {
   #numbers

   constructor(numbers) {
       this.#numbers = numbers
   }
}

class LottoMachine {
   execute() {
       const numbers = Randoms.pickUniqueNumbersInRange(1, 45, 6)
       const lotto = new Lotto(numbers)
   }
}

위 코드는 A 상황을 B로 바꾼 것이다.

이처럼 단위 테스트를 할 때 테스트하기 어려운 부분은 분리하고 테스트 가능한 부분을 단위 테스트한다. 테스트하기 어려운 부분은 단위 테스트하지 않아도 된다. 남은 LottoMachine은 어떻게 테스트하기 쉽게 바꿀 수 있을지 고민해 본다.

-> mockRandom과 같은 mock 함수를 활용하여 LottoMachine을 테스트 해볼 수 있을 것 같다. 테스트 코드를 작성할 때, 메소드 시그니처를 수정하여 테스트 하기 좋게 리팩토링할 수 없는지 고민하는 습관을 가지자

 


추가 학습 자료

getter를 사용하는 대신 객체에 메시지를 보내자

* getter를 사용하는 경우: 출력을 위한 값 등 순수 값 프로퍼티를 가져오기 위해서 사용

* 아래 코드의 문제는 cars 클래스에서 car 객체의 position 값을 직접 꺼내 비교한다는 것이다. position 값을 비교하는 작업은 car 클래스 내부에서 해야될 일이다.

// Cars 클래스에 구현된 method
public List<String> findWinners() {
    final int maximum = cars.stream()
              .map(car -> car.getPosition())		// Car객체의 position = 자동차가 움직인 거리
              .max(Integer::compareTo)
              .get();
           
    return cars.stream()
            .filter(car -> car.getPosition() == maximum)
            .map(Car::getName)
            .collect(Collectors.toList());
}

 

메서드 시그니처를 수정하여 테스트하기 좋은 메서드로 만들기

 

* 테스트 코드의 장점

  • 제품의 안정성을 높여준다.
  • 기능의 추가 및 수정으로 인한 부작용(Side-Effect)를 줄일 수 있다.
  • 불안감 없이 코드 작성을 할 수 있도록 도와준다.
  • 디버깅을 쉽게 해준다.
  • 개발 과정에서 반복적인 작업들을 하지 않도록 도와준다.
  • 더 깔끔하고 재사용성이 좋은 코드 작성을 가능하게 해준다.

 

* 메서드의 시그니처를 수정하는 것만으로 테스트하기 좋은 메서드로 만들 수 있다

// Before: number를 의도하는 값으로 넣을 수 없음
public void move() {
    final int number = random.nextInt(RANDOM_NUMBER_UPPER_BOUND);

    if (number >= MOVABLE_LOWER_BOUND) {
        position++;
    }
}

// After: number를 의도하는 값으로 넣을 수 있음
public void move(int number) {
    if (number >= MOVABLE_LOWER_BOUND) {
        position++;
    }
}