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원
camp.nextstep.edu.missionutils
에서 제공하는 Randoms
및 Console
API를 사용하여 구현해야 한다.camp.nextstep.edu.missionutils.Randoms
의 pickUniqueNumbersInRange()
를 활용한다.camp.nextstep.edu.missionutils.Console
의 readLine()
을 활용한다.기능을 구현하기 전 docs/README.md
에 구현할 기능 목록을 정리해 추가한다.
Git의 커밋 단위는 앞 단계에서 docs/README.md
에 정리한 기능 목록 단위로 추가한다.
그냥 무작정 객체를 분리했다간 오류가 났을 때 다시 처음부터 짜야할 것 같다는 생각에 객체를 잘 분리하기 위한 효율적인 방법들은 어떤 것들이 있을지 생각해보았다.
검색을 통해 확인을 해봤는데 객체를 분리하기 위한 패턴은 정말 다양하다는 것을 알았다. 싱글톤 패턴, 빌더 패턴, 전략 패턴, 어댑터 패턴 등등... 그렇게 다양한 패턴이 존재하고 있다는 것을 알았지만 실제로 이 패턴들 중에서 어떤 방식이 적합할 지 감이 오지 않았다.
그래서 일단 각 기능을 만들 때 한 가지 일만 함녀서 만드는 것에 집중하면서 객체를 분리해보기로 하였다.
코드를 작성하기에 앞서 어떤 클래스 함수 앞에 static
키워드가 앞에 붙고 어떤 함수 앞에는 안 붙어 있는 경우를 볼 수가 있었다. 나는 클래스를 분리하고 함수를 만들기 위해서는 static
키워드의 역할을 제대로 알아야 할 필요가 있다는 생각이 들었다.
Java에서는 메모리 영역이 존재하는데 메모리 영역은 static 영역
과 heap 영역
으로 나누어져 있다는 것을 알았다.
핵심 부분을 정리하자면 다음과 같다.
사전 공부를 마치고 기능 구현에 들어갔다. 기능 구현을 하면서 필요한 클래스 및 패키지를 분리했고, 클래스를 만들어 기능별 구현을 시작했다. 코드까지 첨부하면서 설명하면 내용이 길어질 것 같아서 내가 구현했던 기능 목록들만 나열하겠다.
IllegalArgumentException
발생시키기IllegalArgumentException
을 발생시키기 → [ERROR] 입력 금액은 천 원 단위어야 합니다.pickUniqueNumbersInRange(1, 45, 6)
프로그래밍 요구사항을 보면 함수 15라인으로 제안하는 요구사항이 있다. 이 기준은 main() 함수에도 해당된다. 공백 라인도 한 라인에 해당한다. 15라인이 넘어간다면 함수 분리를 위한 고민을 한다.
정상적인 경우를 구현하는 것보다 예외 상황을 모두 고려해 프로그래밍하는 것이 더 어렵다. 예외 상황을 고려해 프로그래밍하는 습관을 들인다. 예를 들어 로또 미션의 경우 아래와 같은 예외 상황을 고민해 보고 해당 예외에 대해 처리를 할 수 있어야 한다.
비즈니스 로직과 UI 로직을 한 클래스가 담당하지 않도록 한다. 단일 책임의 원칙에도 위배된다.
현재 객체의 상태를 보기 위한 로그 메시지 성격이 강하다면 toString()을 통해 구현한다. View에서 사용할 데이터라면 getter 메서드를 통해 데이터를 전달한다.
최근에 등장하는 프로그래밍 언어들은 기본이 불변 값이다. 자바는 final 키워드를 활용해 값의 변경을 막을 수 있다.
인스턴스 변수의 접근 제어자는 private으로 구현한다.
Lotto 클래스는 numbers를 상태 값으로 가지는 객체이다. 그런데 이 객체는 로직에 대한 구현은 하나도 없고, numbers에 대한 getter 메서드만을 가진다.
Lotto에서 데이터를 꺼내지(get) 말고 메시지를 던지도록 구조를 바꿔 데이터를 가지는 객체가 일하도록 한다.
필드(인스턴스 변수)의 수가 많은 것은 객체의 복잡도를 높이고, 버그 발생 가능성을 높일 수 있다. 필드에 중복이 있거나, 불필요한 필드가 없는지 확인해 필드의 수를 최소화한다.
테스트를 작성하면 성공하는 케이스에 대해서만 고민하는 경우가 있다. 하지만 예외에 대한 부분 또한 처리해야 한다. 특히 프로그램에서 결함이 자주 발생하는 부분 중 하나는 경계값이므로 이 부분을 꼼꼼하게 확인해야 한다.
테스트 코드도 코드이므로 리팩터링을 통해 개선해나가야 한다. 특히 반복적으로 하는 부분을 중복되지 않게 만들어야 한다.
테스트를 위한 편의 메서드를 구현 코드에 구현하지 마라. 아래의 예시처럼 테스트를 통과하기 위해 구현 코드를 변경하거나 테스트에서만 사용되는 로직을 만들지 않는다.
테스트를 위한 편의 메서드를 구현 코드에 구현하지 마라. 아래의 예시처럼 테스트를 통과하기 위해 구현 코드를 변경하거나 테스트에서만 사용되는 로직을 만들지 않는다.
가독성의 이유만으로 분리한 private 함수의 경우 public으로도 검증 가능하다고 여겨질 수 있다. public 함수가 private 함수를 사용하고 있기 때문에 자연스럽게 테스트 범위에 포함된다. 하지만 가독성 이상의 역할을 하는 경우, 테스트하기 쉽게 구현하기 위해서는 해당 역할을 수행하는 다른 객체를 만들 타이밍이 아닐지 고민해 볼 수 있다. 다음 단계를 진행할 때에는 너무 많은 역할을 하고 있는 함수나 객체를 어떻게 의미 있는 단위로 분할할지에 초점을 맞춰 진행한다.
잘보고갑니다!