우아한테크코스 5기 - 프리코스 3주차 미션[lotto]

core·2022년 11월 19일
0

우아한테크코스

목록 보기
5/6

서론

2주 차 미션에서는 함수를 분리하고 각각의 함수마다 기능 테스트 코드 작성하는 실습 진행하는 것을 목표로 진행했다.

이번 3주 차 미션에서는 2주 차에서 진행한 내용에서 다음과 같은 목표가 추가된다.
1. 클래스(객체)를 분리하는 연습
2. 도메인 로직에 대한 단위 테스트를 작성하는 연습

자발적으로 객체를 분리하면서 기능 구현을 별로 해본 경험이 없는 나로써는 딱 안성맞춤인 과제였다. 하지만 기능 구현을 완료하고 그에 따라 테스트 코드를 작성하는 습관이 아직 덜 들여진 것 같다. 좀 더 많은 작업을 하면서 테스트 코드를 습관적으로 작성할 줄 아는 습관을 빨리 들일 수 있도록 해야겠다.


기능 구현 유의사항

문제 풀기에 앞서 다음과 같은 진행 방식을 유의하면서 기능을 구현해야 한다.

  • 미션은 기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항 세 가지로 구성되어 있다.
  • 세 개의 요구 사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다.
  • 기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다.

🚀 기능 요구 사항

로또 게임 기능을 구현해야 한다. 로또 게임은 아래와 같은 규칙으로 진행된다.

- 로또 번호의 숫자 범위는 1~45까지이다.
- 1개의 로또를 발행할 때 중복되지 않는 6개의 숫자를 뽑는다.
- 당첨 번호 추첨 시 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑는다.
- 당첨은 1등부터 5등까지 있다. 당첨 기준과 금액은 아래와 같다.
    - 1등: 6개 번호 일치 / 2,000,000,000원
    - 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원
    - 3등: 5개 번호 일치 / 1,500,000원
    - 4등: 4개 번호 일치 / 50,000원
    - 5등: 3개 번호 일치 / 5,000원
  • 로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 한다.
  • 로또 1장의 가격은 1,000원이다.
  • 당첨 번호와 보너스 번호를 입력받는다.
  • 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역 및 수익률을 출력하고 로또 게임을 종료한다.
  • 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 종료한다.

🎯 프로그램 요구 사항

  • JDK 11 버전에서 실행 가능해야 한다. JDK 11에서 정상적으로 동작하지 않을 경우 0점 처리한다.
  • 프로그램 실행의 시작점은 Application의 main()이다.
  • build.gradle 파일을 변경할 수 없고, 외부 라이브러리를 사용하지 않는다.
  • Java 코드 컨벤션 가이드를 준수하며 프로그래밍한다.
  • 프로그램 종료 시 System.exit()를 호출하지 않는다.
  • 프로그램 구현이 완료되면 ApplicationTest의 모든 테스트가 성공해야 한다. 테스트가 실패할 경우 0점 처리한다.
  • 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 이름을 수정하거나 이동하지 않는다.
  • indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
    • 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
  • 3항 연산자를 쓰지 않는다.
  • 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어야 한다.
  • JUnit 5와 AssertJ를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.

추가된 요구 사항

  • 함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다.
    • 함수(또는 메서드)가 한 가지 일만 잘 하도록 구현한다.
  • else 예약어를 쓰지 않는다.
    • 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
      else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다.
  • Java Enum을 적용한다.
  • 도메인 로직에 단위 테스트를 구현해야 한다. 단, UI(System.out, System.in, Scanner) 로직은 제외한다.
    • 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 분리해 구현한다.
    • 단위 테스트 작성이 익숙하지 않다면 test/java/lotto/LottoTest를 참고하여 학습한 후 테스트를 구현한다.

라이브러리

  • camp.nextstep.edu.missionutils에서 제공하는 RandomsConsole API를 사용하여 구현해야 한다.
  • Random 값 추출은 camp.nextstep.edu.missionutils.RandomspickUniqueNumbersInRange()를 활용한다.
    사용자가 입력하는 값은 camp.nextstep.edu.missionutils.ConsolereadLine()을 활용한다.

LOTTO 클래스를 사용하며 구현해야 한다.

✏️ 과제 진행 요구 사항

  • 기능을 구현하기 전 docs/README.md에 구현할 기능 목록을 정리해 추가한다.

  • Git의 커밋 단위는 앞 단계에서 docs/README.md에 정리한 기능 목록 단위로 추가한다.


진행 과정의 순서

⭐️ 객체 분리를 위한 디자인 패턴 공부

그냥 무작정 객체를 분리했다간 오류가 났을 때 다시 처음부터 짜야할 것 같다는 생각에 객체를 잘 분리하기 위한 효율적인 방법들은 어떤 것들이 있을지 생각해보았다.

검색을 통해 확인을 해봤는데 객체를 분리하기 위한 패턴은 정말 다양하다는 것을 알았다. 싱글톤 패턴, 빌더 패턴, 전략 패턴, 어댑터 패턴 등등... 그렇게 다양한 패턴이 존재하고 있다는 것을 알았지만 실제로 이 패턴들 중에서 어떤 방식이 적합할 지 감이 오지 않았다.

그래서 일단 각 기능을 만들 때 한 가지 일만 함녀서 만드는 것에 집중하면서 객체를 분리해보기로 하였다.

💻 인스턴스형 객체와 클래스형 객체에 대한 공부

코드를 작성하기에 앞서 어떤 클래스 함수 앞에 static키워드가 앞에 붙고 어떤 함수 앞에는 안 붙어 있는 경우를 볼 수가 있었다. 나는 클래스를 분리하고 함수를 만들기 위해서는 static키워드의 역할을 제대로 알아야 할 필요가 있다는 생각이 들었다.

Java에서는 메모리 영역이 존재하는데 메모리 영역은 static 영역heap 영역으로 나누어져 있다는 것을 알았다.

핵심 부분을 정리하자면 다음과 같다.

1. static 영역

  • Garbage Collector가 관리하는 영역은 아니며 단순히 class만 모여 있는 공간을 의미(객체가 아니다.)한다. 그래서 static 영역의 메모리를 무분별하게 사용하면 메모리 누수가 발생할 수 있다.
  • class 안에 존재하는 필드(class 변수)가 static으로 지정이 된다면(static 멤버) 이 멤버는 동일한 클래스의 모든 객체들에 의해 공유가 되고(클래스가 생성될 때마다 각각 생성이 되는 것이 아니라 단 하나로 생성이 되기 때문), 객체가 사라져도 이 멤버는 사라지지 않는다.
  • static 메서드는 static 멤버들만 접근 가능하고, 객체가 생성되지 않는 상황에서도 사용 가능하다.
  • static 메서드this 키워드를 사용할 수 없다.
  • static 메서드에 대하여 오버라이딩이 불가능하다.

2. heap 영역

  • Garbage Collector가 관리하는 영역이며 객체들이 모여 있는 공간(new 연산을 통해 생성)이다. 메모리 누수의 걱정은 없다.

🖥️ 프로그램 기능 구현 시작

사전 공부를 마치고 기능 구현에 들어갔다. 기능 구현을 하면서 필요한 클래스 및 패키지를 분리했고, 클래스를 만들어 기능별 구현을 시작했다. 코드까지 첨부하면서 설명하면 내용이 길어질 것 같아서 내가 구현했던 기능 목록들만 나열하겠다.

✅ ”구입금액을 입력해 주세요.” 출력 기능, 구입 금액 입력 기능(수정)

✅ 문자열로 입력 받은 금액에 대한 검증 후 정수형으로 반환 기능

  • 입력 금액의 맨 앞자리가 0이면 IllegalArgumentException 발생시키기
  • 만약 1000원 단위의 금액이 아니면 IllegalArgumentException 을 발생시키기 → [ERROR] 입력 금액은 천 원 단위어야 합니다.

✅ 입력한 금액에 대하여 로또 개수 구하기 기능

✅ 로또 개수 출력 기능 → “8개를 구매했습니다.”

✅ 로또 개수 만큼 6개의 숫자를 랜덤으로 생성하는 기능

  • pickUniqueNumbersInRange(1, 45, 6)

✅ 생성된 로또 번호 오름차순으로 정렬하기 기능

✅ 입력 형식 (, 구분해서 입력) 검증 포함한 당첨 번호 입력 받기 기능

✅ 각각의 당첨 번호 유효성 검증

  • 각각의 당첨 번호의 범위 검증 (1 ≤ 당첨 번호 ≤ 45)
  • 각각의 당첨 번호 중복 여부 검증

✅ 보너스 번호 입력 받기 기능

✅ 보너스 번호 유효성 검증 기능

✅ 당첨 통계 출력 기능

✅ 모든 로또의 번호와 당첨번호를 비교해 각각의 일치 개수에 대하여 몇 개의 로또가 있는지 정보 출력 기능

✅ 총 수익률 계산 기능(소수점 둘째 자리에서 반올림)


3주차 진행 후 받은 공통 피드백

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

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

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

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

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

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

비즈니스 로직과 UI 로직을 한 클래스가 담당하지 않도록 한다. 단일 책임의 원칙에도 위배된다.
현재 객체의 상태를 보기 위한 로그 메시지 성격이 강하다면 toString()을 통해 구현한다. View에서 사용할 데이터라면 getter 메서드를 통해 데이터를 전달한다.

🔔 연관성이 있는 상수는 static final 대신 enum을 활용한다

🔔 final 키워드를 사용해 값의 변경을 막는다

최근에 등장하는 프로그래밍 언어들은 기본이 불변 값이다. 자바는 final 키워드를 활용해 값의 변경을 막을 수 있다.

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

인스턴스 변수의 접근 제어자는 private으로 구현한다.

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

Lotto 클래스는 numbers를 상태 값으로 가지는 객체이다. 그런데 이 객체는 로직에 대한 구현은 하나도 없고, numbers에 대한 getter 메서드만을 가진다.
Lotto에서 데이터를 꺼내지(get) 말고 메시지를 던지도록 구조를 바꿔 데이터를 가지는 객체가 일하도록 한다.

🔔 필드(인스턴스 변수)의 수를 줄이기 위해 노력한다

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

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

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

🔔 테스트 코드도 코드다

테스트 코드도 코드이므로 리팩터링을 통해 개선해나가야 한다. 특히 반복적으로 하는 부분을 중복되지 않게 만들어야 한다.

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

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

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

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

  • 테스트를 위해 접근 제어자를 바꾸는 경우
  • 테스트 코드에서만 사용되는 메서드

🔔 단위 테스트를 할 때 테스트하기 어려운 부분은 분리하고 테스트 가능한 부분을 단위 테스트한다.

🔔 private 함수를 테스트 하고 싶다면 클래스(객체) 분리를 고려한다

가독성의 이유만으로 분리한 private 함수의 경우 public으로도 검증 가능하다고 여겨질 수 있다. public 함수가 private 함수를 사용하고 있기 때문에 자연스럽게 테스트 범위에 포함된다. 하지만 가독성 이상의 역할을 하는 경우, 테스트하기 쉽게 구현하기 위해서는 해당 역할을 수행하는 다른 객체를 만들 타이밍이 아닐지 고민해 볼 수 있다. 다음 단계를 진행할 때에는 너무 많은 역할을 하고 있는 함수나 객체를 어떻게 의미 있는 단위로 분할할지에 초점을 맞춰 진행한다.

profile
코어의 서버 탐험기

1개의 댓글

comment-user-thumbnail
2022년 11월 20일

잘보고갑니다!

답글 달기