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

·2022년 11월 16일
0
post-thumbnail

0. 프로젝트 가이드라인 확인

이번에 만들 것은 로또(우테코 2주차 깃허브 링크)였다. 간단히 말하면 로또를 구매하고, 당첨 번호와 보너스 번호를 입력하여 로또에 당첨된 결과를 알려주는 것이었다.

추가된 요구 사항은 크게 정리하자면

  1. 함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다.
  • 함수(또는 메서드)가 한 가지 일만 잘 하도록 구현한다.
  1. else를 지양한다.
  • 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
  • 때로는 if/else, switch문을 사용하는 것이 더 깔끔해 보일 수 있다. 어느 경우에 쓰는 것이 적절할지 스스로 고민해 본다.
  1. 도메인 로직에 단위 테스트를 구현해야 한다. 단, UI(Console.readLine, Console.print) 로직에 대한 단위 테스트는 제외한다.
  • 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다.

적고 보니 추가된 요구 사항을 제대로 지키지 않은 것 같다. 함수가 한 가지 일만 하도록 하는 것에 집중해서 길이가 15라인을 넘는지 아닌지는 체크하지 않았다. 다음에는 15라인을 확인해가면서 해야겠다.

else를 지양하는 것도 다소 논란이 되는 부분인 것 같긴 한데, else를 사용하면 함수의 depth가 깊어져서 가독성이 떨어지는 부분 때문에 early return을 권장한다고 생각한다. 물론 else구문이나 switch 구문을 사용하는 것이 나은 경우도 있겠지만, 이번 경우에는 if구문을 사용하는게 나아 보였다.

도메인 로직이라는 말은 잘 몰라서 검색을 해 봤다. 간단히 말하면 코드에는 크게 도메인 로직과 서비스 로직이라는 게 있는데, 도메인 로직은 실제 비즈니스에 상관 있는 코드, 서비스 로직은 그 비즈니스를 하기 위해서 도와주는 로직이다. 이론상으로는 간단한데 분리하는게 쉽지는 않았다. 관련된 자료를 찾아보던 중 도메인과 서비스를 분리하기 어렵다면 기능 단계의 재정의가 필요하다는 자료를 읽었고, 반복되는 재정의로 도메인과 서비스를 분리했다. 마지막까지 계속 도메인 로직과 서비스 로직 정의를 반복할 정도로 헷갈리는 부분이었다.

가이드라인을 확인한 후
1. 기능 목록 정리하기
2. 테스트 코드 작성(단위 테스트)
3. 기능 구현
4. pull request 보내기
5. node로 구현 확인하기
순으로 진행하였다.
이 글의 마지막에는 피드백과 소감을 적어 보았다.

깃허브 레포지토리는 여기이니 코드 리뷰 남겨 주시면 감사하겠습니다. 😄

1. 기능 목록 정리하기

고심 끝에 정리한 최종 기능 목록은 다음과 같다. 처음부터 이렇게 작성한 건 아니고 계속 수정을 거듭했다. 이번 기능 목록은 앞부분을 체크박스로 만들었다. 커밋에서 어떤 부분을 작업했는지 명확하게 알려 주기 위해서다. 그리고 옆에 이 기능을 어떤 함수로 만들어 줬는지 적어 주었다.

# 로또

## 💼 기능 목록

### 📍 도메인

- [x] 유효한 로또 구입 금액을 입력받는다 - Purchase#validate()

  - [x] e) 로또 금액이 숫자가 아닌 경우
  - [x] e) 로또 금액이 0보다 작은 경우
  - [x] e) 로또 금액 금액이 1000원으로 나누어 떨어지지 않는 경우

- [x] 숫자를 랜덤하게 뽑아 오름차순 정렬한다. - MyLotto#generateRandom()

  - [x] 숫자는 중복되지 않는다.
  - [x] 숫자는 1 ~ 45 사이이다.
  - [x] 숫자는 6개이다.

- [x] 유효한 숫자 6개를 입력받는다. - Lotto#validate()

  - [x] e) 숫자 범위가 1 ~ 45가 아닌 경우
  - [x] e) 숫자가 6개가 아닌 경우
  - [x] e) 중복이 존재하는 경우

- [x] 유효한 보너스 번호를 입력받는다. - Bonus#validate()

  - [x] e) 숫자 범위가 1 ~ 45가 아닌 경우

- [x] 로또 번호와 당첨 번호를 비교한다. - Result#compare()

  - [x] 3개 일치 : 5,000원
  - [x] 4개 일치 : 50,000원
  - [x] 5개 일치 : 1,500,000원
  - [x] 5개 일치 + 보너스 번호 일치 : 30,000,000원
  - [x] 6개 일치 : 2,000,000,000원

### 📍 서비스

- [x] 로또 구입 금액/1000개를 구매했다는 메시지를 띄운다. -
      Service#printLottoCount()

- [x] 중복되지 않은 6개의 숫자를 로또 구매 개수만큼 출력한다. -
      Service#printLottoNumbers()

- [x] 당첨 번호 입력 메시지를 띄운다. - Service#printGetWinningNumber();

- [x] 보너스 번호 입력 메시지를 띄운다. - Service#printGetBonusNumber();

- [x] 당첨 통계를 출력한다. - Service#printResult()

2. 테스트 코드 작성(단위 테스트)

테스트 코드 자체는 수월하게 작성했다. 다만 이번 테스트에서는 로또 번호의 유효성 검사를 할 때 Lotto 내부의 validate()함수를 부르지 않고 그냥 new Lotto()로 불러줬다. 그 이유는 Lotto 클래스 내부의 constructor에서 자동으로 validate()를 불러주도록 코드가 작성되었기 때문이다. 이 과정을 보면서 constructor 내부에 작성된 코드는 클래스를 부를 때 자동으로 실행되는 것을 알 수 있었다.

describe('로또 클래스 테스트', () => {
  test('로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.', () => {
    expect(() => {
      new Lotto([1, 2, 3, 4, 5, 6, 7]);
    }).toThrow('[ERROR]');
  });

3. 기능 구현

기능을 구현할 때는 이러한 조건도 있었다.

  • 제공된 Lotto 클래스를 활용해 구현해야 한다.
  • numbers의 # prefix를 변경할 수 없다.
  • Lotto에 필드를 추가할 수 없다.
class Lotto {
  #numbers;

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

  validate(numbers) {
    if (numbers.length !== 6) {
      throw new Error();
    }
  }
  // TODO: 추가 기능 구현
}

일단 Lotto에 필드를 추가할 수 없다는 조건은 이 클래스에서 로또 당첨 번호, 내 로또 번호, 보너스 번호를 한 번에 받아오지 말라는 제약 조건을 만든다고 생각했다. 따라서 클래스를 분리해 각 번호를 받아오는 클래스를 만들기로 했다.

# prefix라는 개념은 몰라서 알아보았다. 변수 앞에 #을 붙이면 private 필드가 되며, 이 변수는 클래스 내부에서만 접근 가능한 필드가 된다. 개념 자체는 이해했는데 #을 왜 붙여야 하는 지는 이해가 잘 되지 않았다. 어차피 이 값을 lotto.#numbers로 받아오지 않아도 다른 경로로 받아올 수 있는데 굳이 붙여야 하나 하는 생각이 들었다. 안전성 때문이라고는 하는데, 큰 오류를 발생시킬 수 있는 구체적인 예제를 아직 못 찾아서 아직까지는 잘 모르겠다. 자바 빈 설계 규약에 따르면 무조건 getter과 setter을 만들어야 한다는데, setter을 사용하지 않으면 굳이 만들어야 하나 싶기도 했다.

처음에 클래스를 분리하고 import해도 클래스를 다른 파일에서 사용할 수 없어서 당황스러웠다. 알고보니 module.exports를 하지 않아서였다. 이전까지는 module.exports가 뭔지 몰랐지만 기능을 사용하는데 문제가 없어서 무시하고 넘어갔는데, 클래스 분리를 하면 반드시 export를 해 줘야 import를 할 수 있다는 것을 알게 되었다.

또한 고민이 되었던 지점은 상수 분리였다. 하드코딩을 피하기 위해서 상수를 생성하고 상수를 한 폴더 constant에 모아 두었는데 메시지들을 통일성 없게 export 했다는 생각이 들었다. 메시지의 종류를 그룹화하면서도 통일성 있게 export 하는 방법의 고민이 필요해 보인다.

4. pull request 보내기

이번에는 pull request를 보내고 나서도 부족한 부분이 계속 보여 수정을 거듭했다. 덕분에라고 하긴 뭐하지만 pull request를 보내고 나서도 내 레파지토리에 push하면 pull request에 반영된다는 것을 확인할 수 있었다.

5. node로 구현 확인하기

저번 주차에는 node를 돌려 보라는 말이 npm test로 테스트 케이스를 돌려 보라는 이야기인줄 알았다. 그런데 숫자 야구 피드백 강의에서도 프로그램을 실행하는 걸 보니 아무래도 이상해서 찾아 보았다. 알고보니 터미널에 node src/App.js 를 입력하면 프로그램을 실행할 수 있었다.

터미널로 실행해 보니 내 프로그램은 입력을 전혀 받지 못하고 Console.print 부분만 실행되고 있었다. 저번 주차에도 예감하고 있었지만 아무래도 Console.readLine 부분에서 문제가 생긴 것 같았다. 깃허브 discussion을 뒤져본 결과 Console.readㅣine은 비동기 코드지만 async/await 구문을 사용하면 테스트 케이스에서 오류가 생기므로 동기적으로 처리해줘야 한다는 것을 알 수 있었다. 그래서 콜백 함수를 콜백 함수에 담아 주었다.

getLottoCount() {
    Console.readLine(ServiceMessage.PURCHASE_INPUT, (amount) => {
      this.printLottoCount(amount);
    });
  }

이해를 돕기 위해 살짝 생략해 주었다. 이런 식으로 Console.readLine 함수 값이 있어야 다음 printLottoCount()가 호출이 되도록 연결해 주면 비동기 함수를 동기적으로 처리할 수 있다.

다만 이 과정을 거치면서 저번 주는 0점일수도 있겠다.. 라는 생각이 들었다. node에서 실행해 보니 제대로 작동하지 않았기 때문이다. ㅎㅎ 하.. 그래도 이제라도 node로 실행시키는 방법을 알아서 다행이라고 생각한다.


피드백 정리

공통 피드백

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

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

    • 이 부분은 시간에 쫓겨 작성하다 보니 잊어버린 부분이다.. 이번 주차에는 10라인으로 줄었으니 이 부분을 좀 더 신경 써야겠다.
  • 발생할 수 있는 예외 상황에 대해 고민한다

    • 다른 예외는 구현했는데 당첨 번호와 중복된 보너스 번호를 입력하는 경우는 생각하지 못했다. 예외 상황을 더 많이 고민해 봐야겠다.
  • 비즈니스 로직과 UI 로직을 분리한다

    • 비즈니스 로직과 UI로직을 분리하라는 이야기는 이해가 간다. UI로직은 쉽게 바꿀수도 있지만 비즈니스 로직은 거의 변하는 경우가 없기 때문이다. 하지만 아직 비즈니스 로직과 UI로직을 구분하는 것이 어렵다. 많은 경험과 연습으로 해결해야겠다.
  • 객체의 상태 접근을 제한한다
    필드는 private class 필드로 구현한다. 객체의 상태를 외부에서 직접 접근하는 방식을 최소화 하는 이유에 대해서는 스스로 찾아본다.

    • private 필드에 대한 이해가 처음에는 부족해서 private 필드를 무시하고 코드를 짰는데, 나중에 private 필드로 구현하려고 변경하였다. 이제는 private 코드에 대한 이해가 생겼으니 처음부터 고려해서 짜려고 한다.
  • 객체는 객체스럽게 사용한다

    • class에서 메시지를 보내는 방법은 시도해본 적 없어서 아직까지 좀 헷갈리기는 한데 다음 과제에서 메시지를 보내 getter을 최소화시키려는 노력을 해봐야겠다.
  • 필드의 수를 줄이기 위해 노력한다

    • 리팩토링 할 때 필드가 중복되어 선언되지 않았는지도 확인해 볼 지점인 듯 하다.
  • 성공하는 케이스 뿐만 아니라 예외에 대한 케이스도 테스트한다

    • 오히려 예외에 대한 케이스만 테스트하고 성공하는 케이스를 테스트 안해봤다.. 성공하는 테스트는 node로 실행시키며 확인했는데 테스트 케이스를 보고 다른 사람이 어떤 테스트를 했는지 알 수 있어야 하므로 성공하는 케이스를 테스트해 봐야겠다.
  • 테스트 코드도 코드다

    • 테스트 코드에 대한 효율성은 전혀 생각하지 않고 짰기 때문에 굉장히 뜨끔하는 부분이다. 테스트 코드도 리팩토링 할때 효율성을 고려했는지 확인해 봐야겠다.
  • 테스트를 위한 코드는 구현 코드에서 분리되어야 한다

    • 테스트를 위한 코드는 그냥 짤 줄 몰라서 이런 건 만들지도 않았다..
  • 단위 테스트하기 어려운 코드를 단위 테스트하기

    • 최대한 단위 테스트를 할 수 있도록 리팩토링을 고민 하라는 이야기이다. 다음 주차에는 일단 짜고 리팩토링하는 시간을 오래 가져야 할 것 같다.

소감

이번에는 빨리 진행해서 다른 사람의 코드도 살펴보고 피드백도 받고 싶었는데 나에겐 빡빡한 과제였다.. 한 주가 끝나면 자신감이 생겨서 이 정도도 할수 있구나! 하고 뿌듯해지는데 다음주 과제를 시작하면 모르는 게 너무 많아서 다시 쪼그라든다. 코드를 작성하는 시간보다 모르는 개념을 찾아보고 이 개념을 왜 써야 하는지 이해하는 시간이 더 많은 것 같다. 모르는 게 많은 건 좀 슬프지만 그래도 공부하는 방법을 점점 더 알게 되고 있다. 아직 3주차지만 혼자 공부한 것보다 많은 것을 배우고 있다.

다른 사람의 피드백은 역시 중요하다는 생각이 든다. 내가 혼자 짰을 때는 꽤 괜찮은 코드인 기분이었는데 피드백을 받고 여러가지 기준을 적용해보니 부족한 점이 많이 느껴진다. 부족한 점을 지적받아서 오히려 기분이 좋다. 다른 사람에게 피드백 받아 더 나은 코드를 짤 수 있는 귀한 기회이기 때문이다!!! 다음 주면 끝난다는 사실이 많이 아쉽다.

profile
전 이것도 몰라요

0개의 댓글