공대생

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

우아한 테크코스/프리코스

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

상수나무 2022. 11. 19. 21:48

3주차 목표

  1. 클래스(객체)를 분리하는 연습
  2. 도메인 로직에 대한 단위 테스트를 작성하는 연습

2주차부터는 혼자서 모든 걸 다 잘하려고 하는 마음에 힘들었던 것 같은데 우테코에서 단계별로 과제의 목표를 주는 것을 보고 하라는 것만 잘해도 실력이 성장해있겠다는 생각이 들었다..

 

3주차 과제

로또 게임을 구현하는 과제였다. 로또 게임은 다음과 같은 절차로 진행된다.

  1. 로또 구입 금액을 입력하면 금액에 맞는 개수만큼의 로또를 발행한다. (로또는 1개당 1,000원이다.)
  2. 로또 당첨 번호를 입력한다. (입력은 “,”로 구분한다.)
  3. 보너스 번호를 입력한다.
  4. 발행한 로또에 대해 당첨 결과와 수익률을 출력한다.

실행 결과 예시를 보면 게임 이해가 더 쉬울 것 같다.

구현 과정

설계: 클래스는 도메인 로직을 찾으면 분리하기 편하다.

핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다.

과제 요구 사항에 이런 요구 사항이 있어서 도메인 로직에 대해 공부했다.

결론적으로 말하면 프로그램에서 문제에 대한 의사결정을 하는 코드를 도메인 로직이라고 한다.

개인적으로는 전체 기능 목록에서 입출력이나 네트워크를 담당하는 UI 로직을 제외하는 식으로 도메인 로직을 찾아내보았다.

기능 목록

기능 목록을 세세하게 작성해놓으면 도메인 로직과 UI 로직을 쉽게 분리할 수 있다.

1. 전체 세부 기능 목록

 2. 조금 더 큰 단위의 기능 목록

3. UI로직을 뺀 도메인 로직 기능목록

클래스 분리

도메인 로직 기능 목록을 바탕으로 클래스를 총 3개로 분리했다.

  1. 로또 발행 클래스
  2. 로또 결과 판단 클래스
  3. 예외처리 클래스

처음에는 사용자 입출력 기능도 클래스로 분리했는데 이렇게되면 App 클래스와 다른 클래스들 사이에서 계속 클래스를 불러와야하는 점이 비효율적이라고 판단해서 App 클래스에 UI 로직을 넣어 도메인 클래스와 UI 클래스를 분리하도록 설계했다.

 

구현: 남의 좋은 점 훔쳐먹기

항상 코드를 제출하고 잠깐씩이라도 자괴감에 빠지고는 했다. 다른 사람들의 PR을 보면서 세상에 개발을 잘하는 사람은 많다는 것을 뼈저리게 깨닫고, 나의 위치를 알게된다. 자괴감에 빠져있기 보다는 다른 사람들의 좋은 점들을 훔쳐먹자! 라는 생각으로 이번 과제에서는 타인의 코드에서 좋다고 생각하는 지점들을 카피해보는 시간을 가졌다.

1. enum 객체를 이용한 변수 상수화

이전에는 변수를 상수화할 때 단순히 객체로 선언해서 정리했다. 그러나 코드리뷰를 하면서 Object.freeze() 함수를 이용해 객체를 동결할 수 있다는 것을 알게 되었다. 한 번 선언된 객체의 값을 변경하지 못하도록 하는 객체 동결을 통해 객체가 불변성을 지니게 하는 것이다.

그리고 나는 모든 상수화 할 데이터들을 하나의 객체에 넣어서 공백으로 변수를 분리했는데, 종류별로 다른 객체를 생성하는 것이 더 가독성이 좋아보여서 이 부분도 코드에 적용해보았다.

const COMMAND = Object.freeze({
  MONEY: '구입금액을 입력해 주세요.\n',
  LOTTONUM: '개를 구매했습니다.',
  WINNING: '\n당첨 번호를 입력해 주세요.\n',
  BONUS: '\n보너스 번호를 입력해 주세요.\n',
  RESULT: '\n당첨 통계\n---',
  YIELD: '총 수익률은 ',
});

const ERROR = Object.freeze({
  MONEY_DIVISIBLE: '[ERROR] 구입 금액은 1,000원 단위로 입력해야 합니다.',
  MONEY_DIGIT: '[ERROR] 구입 금액은 정수로 입력해야 합니다.',
  WINNING_DIGIT: '[ERROR] 각 당첨 번호는 정수여야 합니다.',
  WINNING_LENGTH: '[ERROR] 당첨 번호는 6개여야 합니다.',
  WINNING_RANGE: '[ERROR] 각 당첨 번호의 숫자 범위는 1 - 45까지 입니다.',
  WINNING_DUPLICATED: '[ERROR] 당첨 번호는 중복될 수 없습니다.',
  BONUS_DIGIT: '[ERROR] 보너스 번호는 정수여야 합니다.',
  BONUS_RANGE: '[ERROR] 보너스 번호의 숫자 범위는 1 - 45까지 입니다.',
  BONUS_DUPLICATED: '[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다.',
});

module.exports = { COMMAND, ERROR };

2. private 변수, 메소드 이용하기

이는 우테코가 3주차 과제에서 사용하길 유도한 부분이기도 한 것 같다. (요구사항에 클래스 안의 private 변수를 먼저 정의해놓았다.) 이전에는 모든 변수가 어디서든 사용될 수 있다는 생각에 변수에 private를 붙일 생각을 하지 못했다.

하지만 다른 사람들의 코드를 보니 객체간의 의존성을 줄이기 위해 각 클래스 안에서만 사용하는 private한 변수나 메소드를 많이 사용하는 것을 보고 이를 적용시켜보았다. 이렇게 코딩하니 클래스 밖에서 참조하면 안되는 변수를 참조할 때 바로 오류를 띄워서 코딩하기가 더 수월했던 것 같고, 각 클래스에서 참조해도 되는 변수와 안되는 변수를 구분해 놓으니 가독성도 좋아진 것 같다.

+) 다음 과제에서는 static도 많이 사용해보고싶다.

class App {
  #money;
  #lottoArr;
  #winningArr;
  #bonus;

	...
}

 

에러: 테스트코드가 어떤 출력을 원하는지 꼼꼼하게 확인하기

각 클래스 별로 기능 구현을 다 하고 우테코에서 미리 작성해놓은 Application 테스트를 실행했는데 계속 한 부분에서 에러가 났다. 발행한 로또를 출력하는 부분에서 계속 에러가 발생했다.

처음에는 아무리 실행해봐도 테스트가 원하는 결과와 코드가 출력하는 결과의 다른 점을 찾지 못했다. 결국 당일에 해결이 안되어서 다음날에 다시 코드를 보면서 엄청난 차이를 발견했다.

출력하는 발행한 로또의 타입이 달랐다.

테스트 코드가 원하는 것: 배열 형태의 문자열

나: 배열

ㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜ현타가 쩔었다.

save(lotto) {
    this.lottoArr.push(lotto);
    Console.print(`[${lotto.join(', ')}]`);
  }

 

테스트: 도메인 별 단위테스트는 코드의 신뢰성을 높인다.

2주차 피드백에서 첨부해 준 제이슨님의 강의를 보았다. 코드를 모두 구현한 후에 class 별로 테스트 파일을 만들어서 해당 클래스의 메소드들이 제대로된 값을 출력하는지 확인하는 단위테스트를 작성하는 과정을 볼 수 있었다.

도메인 별로 테스트 코드를 작성하니 test를 실행했을 때 어떤 도메인에서 에러가 생기는지 확인하기 쉬웠고, 작은 함수부터 하나하나 테스트를 돌려보는 과정을 통해 내 코드가 기능을 제대로 수행한다는 확신을 얻을 수 있었다.

테스트코드를 작성하지 않았을 때는 테스트를 실행할 때마다 입력값을 바꾸고 타입을 바꾸는 등 실제 코드를 변경해야해서 상당히 번거로운 작업이었는데, 함수 mocking을 통해 실제 코드를 바꾸지 않고도 테스트를 할 수 있는 점이 개발을 몇 배 더 편하게 만들어주는 것 같다.

ExceptionTest.js

const { MoneyExceptions, BonusExceptions } = require('../src/Exceptions');
const WINNING = [1, 2, 3, 4, 5, 6];

describe('예외처리 클래스 테스트', () => {
  test('구입 금액이 정수가 아니면 예외가 발생한다.', () => {
    expect(() => {
      new MoneyExceptions('30a0').check();
    }).toThrow('[ERROR]');
  });

  test('구입 금액이 1,000원으로 나누어 떨어지지 않으면 예외가 발생한다.', () => {
    expect(() => {
      new MoneyExceptions('3050').check();
    }).toThrow('[ERROR]');
  });

  test('보너스 번호가 정수가 아니면 예외가 발생한다.', () => {
    expect(() => {
      new BonusExceptions('3.5').check(WINNING);
    }).toThrow('[ERROR]');
  });

  test('보너스 번호가 1 - 45 사이의 수가 아니면 예외가 발생한다.', () => {
    expect(() => {
      new BonusExceptions('65').check(WINNING);
    }).toThrow('[ERROR]');
  });

  test('보너스 번호가 당첨번호와 중복되면 예외가 발생한다.', () => {
    expect(() => {
      new BonusExceptions('1').check(WINNING);
    }).toThrow('[ERROR]');
  });
});

 

후기(주저리)

벌써 다음주가 프리코스 마지막 과제이다. 시간이 어찌나 빠르게 흘러가는지, 프리코스를 통해 배우는 것들이 너무 많았는데 마지막이라는 게 너무 아쉽다 ㅜㅜ 그냥 1년동안 프리코스 계속하고싶다 ㅜㅜㅜ

마지막인 만큼 더욱더 화이팅있게 최선을 다해야겠다!!

어제보다 더 나은 오늘의 내가 되기 위해 :)

 

PR 링크

제가 작성한 로또 게임 코드는 다음과 같습니다.

구경와서 리뷰 한번 해주시면 정말 많은 도움이 됩니다!!

https://github.com/woowacourse-precourse/javascript-lotto/pull/285

 

[로또] 최예송 미션 제출합니다. by to06109 · Pull Request #285 · woowacourse-precourse/javascript-lotto

PR 타입 기능 목록 작성 로또 게임 기능 구현 리팩토링 테스트 코드 작성 테스트 결과 Application 테스트 코드를 모두 정상적으로 통과합니다.

github.com

 

참고블로그

https://velog.io/@eddy_song/domain-logic

 

비즈니스 로직, 도메인 로직이 도대체 뭐지?

🙄 내 앱은 아직 비즈니스가 아닌데요...?

velog.io

Comments