Skip to content

banjjoknim/java-lotto-game

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

로또 게임

규칙

  • 1등 : 6개 번호 모두 일치
  • 2등 : 5개 번호 일치 + 나머지 1개가 보너스 번호 일치
  • 3등 : 5개 번호 일치
  • 4등 : 4개 번호 일치
  • 5등 : 3개 번호 일치
  • 로또 당첨 금액은 고정되어 있는 것으로 가정한다.
    • 1등 : 2,000,000,000원
    • 2등 : 30,000,000원
    • 3등 : 1,500,000원
    • 4등 : 50,000원
    • 5등 : 5,000원

구현할 기능 목록

  • 로또 구입 금액을 입력받는다.
  • 입력받은 구입 금액이 적절한 값인지 겅증해야 한다.
  • 구입 금액에 해당하는 로또를 발급해야 한다(로또 1장의 가격은 1000원이다).
  • 발급된 로또의 번호는 랜덤으로 정해지며 서로 다른 6개의 숫자로 구성되어야 한다.
  • 당첨 번호를 입력받는다.
  • 보너스 볼의 번호를 입력받는다.
  • 로또 번호가 서로 다른 6개의 숫자인지 검증해야 한다.
  • 로또 번호가 1 ~ 45 사이의 자연수인지 검증해야 한다.
  • 구입 금액이 1,000의 배수인지 검증해야 한다.
  • 규칙에 따른 당첨 여부를 확인해야 한다.
  • 당첨금 총액을 계산해야 한다.
  • 수익률을 계산해야 한다(%로 표시해도 된다).

피드백 및 개선사항

  • 객체의 상태값을 객체 외부에서 비교하는 것을 조심하자.
  • 메서드를 구현할 때 정적 팩토리 메서드를 이용하는 것 또한 고려해보자.
  • 드문드문 보이는 하드코딩에 주의하자.
  • 객체의 불변성과 안전성을 보장하기 위해 깊은 복사의 개념을 유의하도록 하자.
  • 클래스의 크기가 커졌다면 세부적으로 클래스를 분리하는 것도 고려해보자.
  • 예외를 처리할 때 발생할 수 있는 다양한 시나리오(Stack Over flow 등)에 대해서도 고려하도록 하자.
  • 객체지향 생활 체조 원칙의 규칙3을 적용할 수 있도록 노력해보자. 좀 더 객체지향에 가까워질 것이다.
  • 코드의 가독성에 신경 쓰자(코드 컨벤션 참조).
  • 화폐단위의 계산 또는 실수형 계산을 할 때는, BigDecimal을 사용하는 것이 좋다.
  • 객체 생성 시 뿐만 아니라 입력을 받을 때도 유효성을 검증해주면 좀 더 안전한 설계가 될 것이다.
  • 메서드 내에서만 사용되는 변수를 전역 변수(static)로 선언하는 것은 좋지 못하다.
  • 테스트를 작성할 때 @BeforeEach등의 어노테이션을 활용할 수 있도록 하자.
  • 특별히 비즈니스 로직이 있지 않다면 테스트 작성을 굳이 할 필요가 있는지에 대해 생각해보자.
  • 확실하게 경우의 수가 존재하는 테스트의 경우, 모든 경우의 수를 검증하는 것이 좋을 것이다.
  • 객체의 상태값으로 갖는 IntegerWrapping하게 된다면, 해당 상태에 대한 유효성 검증을 따로 분리할 수 있게 된다. 이는 객체지향 생활 체조 총정리에서 규칙 3(모든 원시값과 문자열을 포장한다)에 대한 이야기를 하는 것이다.
  • getter로 가져온 값을 변경하여도 본래의 값에는 영향이 없도록 해야한다.
  • 메서드 내에서만 사용되는 변수를 전역변수, static 영역으로 가져오는 것은 좋지 못하다.
  • 비즈니스 로직이 없다면 테스트 작성을 과연 해야하는지에 대해서 고민해보자.
  • 경우의 수가 확실하게 정해져있는 경우, 모든 경우의 수에 대해서 테스트를 하는 것이 신뢰도를 높일 수 있다.
  • 리팩토링을 할 때, 테스트 코드 또한 리팩토링의 대상이다.
  • 무작정 메서드 추출을 한다고해서 좋은 것이 아니다. 적절하게 사용해야 한다.
  • Arrays.asList() 등의 팩토리 메서드를 적절히 이용하도록 하자.
  • 적절한 변수 네이밍을 통해 불필요한 변수할당을 줄이도록 하자.
  • 상수 역시 변수와 마찬가지로 의미있는 네이밍을 통해 가독성을 높이도록 하자.
  • Optional을 좀 더 숙지하고 효율적으로 사용하도록 하자.
  • 메서드가 배치되는 순서 또한 가독성에 영향을 미친다. 일반적으로 getter, setter 는 최하단으로 내리며, 만약 @Override한 메서드가 있다면 해당 메서드가 getter, setter 보다 더 하단에 위치해야 한다. 그리고 호출되는 순서에 맞게 배치하는 것이 좋다.
  • equals를 재정의하려거든 hashCode도 재정의 하도록 하자. - 아이템11 - equals를 재정의하려거든 hashCode도 재정의하라
  • 자주 쓰이는 인스턴스 같은 경우, 미리 보관해뒀다가 바로 꺼내쓸 수 있게 하면 효율적이다. 캐싱 개념을 사용해보자. - 해당 코멘트
  • View에 의존하는 코드란, View에 변화가 생겼을 때 Model에 영향을 미치게 되는 코드를 말한다. ViewModel은 독립적이어야 하며, 그렇지 않을 경우 ViewModel이 영향을 받게되는 상황(출력 포맷이 변한다던지 하는 변화)에 대응하기 위해 일일이 모든 코드를 수정하게 될 것이다. 따라서 View에 의존하는 코드를 작성하지 말아야 한다. 이는 MVC 패턴에 어긋난다. - 해당 코멘트
  • ViewModel의 책임 소재를 명확하게 해야한다(InputView에서는 입력(요청)에 대한 모든 처리를 담당하는 식의..) - 해당 코멘트
  • Set을 비롯한 자료구조를 사용하는 방법도 고려해보자. - 해당 코멘트
  • 제대로 된 VO(Value Object) 객체가 무엇인지 알고 제대로 사용하자.
  • 정적 팩토리 메서드는 생성자의 다음 위치에 있어야 한다.
  • 조건의 변경에 따라 상수명에 변화가 없도록 적절한 상수명을 고려하도록 하자. 상수명에 조건이 들어간다면 조건이 변경될 때 상수명도 변경되어야 할 것이다.
  • 테스트하기 힘든 것을 최대한 테스트할 수 있도록 분리함으로써 안정성을 보장하려고 노력하자.
  • 객체에게 메시지를 보내자!!!
  • 객체를 생성하는 정적 팩토리 메서드를 사용함에 있어서, 기존의 생성자를 외부에서 사용하지 않게 될 경우 해당 생성자를 private로 접근 제한을 해야한다. 이는 해당 코드를 사용해야하는 개발자들에게 혼란을 야기할 수 있기 때문이다. 그리고 객체를 생성하는 경우의 수가 하나뿐인데 굳이 정적 팩토리 메서드를 사용해야 하는지에 대해 충분히 고민해보고 사용하도록 하자.
  • 가능한한 노가다(...)식 코딩은 지양하고 적절한 파라미터의 상정을 통해 추상화하도록 노력하자. - 해당 코멘트
  • View단에서 도메인 로직이 수행되는지 잘 검증하도록 하자. View단은 단순히 결과를 파라미터로 받아와서 출력하는 역할만 하는 것이 바람직하다. - 해당 코멘트
  • 테스트시 불필요한 어노테이션을 추가하는 것보다는 적절한 메서드 선언을 통해 처리해주는게 좋지 않을까? - 해당 코멘트
  • 하나의 테스트코드에서 여러 테스트를 동시에 진행할 경우에는 가급적 assertAll()을 사용해주는 것이 좋다. - Assertion 메소드(assertTrue, assertEquals, assertAll 등)
  • 테스트 케이스를 작성할 때, 오름차순 또는 내림차순 등으로 가독성이 좋게 작성하도록 하자.
  • private 메서드는 실제로 호출되는 위치에 가까이 있어야 한다. 이는 가독성에 큰 영향을 미칠 확률이 높다.
  • @ParameterizedTest와 적절한 @xxxSource 어노테이션을 이용해서 테스트를 해보자.
  • public 메서드를 private 메서드보다 더 위에 명시해주도록 하자.
  • 반복적으로 사용되는 인스턴스는 캐싱하여 사용하도록 해보자.
  • try-catch 문으로 예외처리할 때, 만약 재귀의 형태로 처리한다면 StackOverflow가 발생할 수 있으므로 주의하여 반복문이나, 꼬리 재귀 등과 같은 형태로 코드를 작성하도록 하자.
  • MVC패턴에서 ViewModelController를 통해서 객체를 주고받아야 한다. 그러니까, ViewModel은 서로를 몰라야 한다. 즉, View에서 바로 Model 객체를 리턴하면 안된다는 뜻이다.
  • 예외처리나 단순한 출력 문자열의 경우, 상수로 사용하지 말고 하드코딩을 해도 괜찮을 것 같다. 다만, 자주 사용된다, 의미를 파악하기 어렵다 라는 두 가지 경우에 해당하는 경우 상수로 처리하는 것이 적절해보인다.
  • 테스트 코드의 경우에는 번거롭게 상수 추출을 하지않아도 될 것 같다.
  • 적절한 일급컬렉션의 사용을 고려해보자.
  • 전략 패턴등을 사용하는 것은 좋지만, 목적을 명확하게 하고 그에 맞게 사용하도록 하자(Lotto를 만들고 싶은건지, LottoNumber를 만들고 싶은건지...).
  • 숫자형의 경우, 중간에 _를 추가해도 동일한 값으로 본다. 가독성을 높일 수 있게 작성하도록 해보자.
  • 좀 더 명확하게 의도를 드러낼 수 있도록 변수명을 정하도록 하자.
  • 추상 메서드가 단 하나 뿐인 인터페이스를 함수형 인터페이스라고 부른다. 이런 함수형 인터페이스는 @FunctionalInterface 어노테이션을 붙여준다.
  • 인터페이스를 통해 테스트하기 어려운 것들을 테스트할 수 있게 된다. 이는 구현체를 직접 정의하는 것을 통해 값을 원하는대로 조작할 수 있기 때문에 가능한 일이다. 또, 함수형 인터페이스의 경우에는 람다식으로도 결과 값을 조작할 수 있다.
  • 캐싱은 트레이드오프이다. 실제로 캐싱에서는 캐시 히트를 늘리는 (=캐시 미스를 줄이는)것이 중요하다. 그리고 이번 미션에서 캐싱을 하는 이유는 오버헤드를 줄이기 위함이 가장 크다. 미리 생성해둔 객체의 사용을 통해서 오버헤드를 줄이는 것이다.
  • MVC의 각 요소의 역할을 생각했을 때, MV서로를 몰라야 한다는 관점에서 CMV를 연결해주는 역할을 잘 수행해야 한다. 때문에 Controller에서 변환을 처리해주는 것이 적합하다고 볼 수 있다. 실제 스프링 프로젝트에서도 뷰에서 입력받은 값을 스프링이 DTO 객체로 파싱해주고, @Valid or @Validated와 같은 어노테이션으로 입력 값을 검증할 수 있다.
  • 테스트 코드가 무엇을 테스트하는지 충분히 고민해보도록 하자. 즉, 목적을 명확히 하고 그에 맞게 테스트를 작성하도록 하자. 가능한한 중복되지 않도록! 이미 테스트가 충분하다면 굳이 필요한 테스트인가? 에 대해서 고민해보자. 또, 테스트 코드는 input에 대한 정확한 output을 검증하기 위해서 사용하는 것이라 생각하고 명확하게 결과를 도출해낼 수 있게 작성하도록 노력하자.
  • 캐싱을 했다면, 그리고 생성자가 불필요해졌다면 생성자의 접근제한자를 private로 변경하여 혼란의 여지를 줄이고 의도한대로(캐싱한 객체만 사용하게끔) 사용하도록 하자.
  • 정적 팩터리 메서드에 관련해서 흔히 사용하는 네이밍 룰이 있다. 참고하여 사용하도록 하자.
  • 값을 꺼내서 외부에서 연산하지 말자...
  • VO는 불변객체로 만들어져야 한다. 여기서 Money 객체의 연산 기능들의 반환 타입은 자기 자신과 같아야할 것이다(값을 포장했기 때문에?). 그리고 VO 객체는 equals, hashCode를 오버라이드 하여 동일성을 확인할 수 있게 해야한다.
  • 인터페이스 자체를 테스트하는 것이 아니라, 테스트하기 어려운 부분들에 존재하는 테스트하기 힘든 코드를 분리하는데 그 방법이 interface를 이용하여 테스트하기 힘든 로직을 분리(여기서는 랜덤로직)하여 테스트하기 어려운 코드를 테스트하기 쉽게 만드는 것이 목적이다. 테스트하기 힘든 코드를 외부로 꺼내서(interface) 테스트 영역을 확보하도록 하자. 인터페이스의 구현체에 대한 테스트를 작성하기 위한 것이 아닌, 정확하게 목표로하는 기능에 대한 테스트를 명확하게 하기 위해서 리팩토링을 진행한 것이다(어렵지만 잘 이해해보도록 하자).
  • 캐싱을 하든 뭘 하든 변경사항을 잘 반영하도록 하자...
  • 가능하면(?) static 메서드보다 생성자에서 validation을 하도록 하자(이유는 아직 정확하게 이해하지 못했다..). 하지만 그렇다고 static 메서드에서 유효성 검증을 하면 안된다는 것은 아니다. 필요하면 얼마든지 할 수 있다.
  • 원시값을 포장하는 경우, VO일 확률이 높다.
  • 불변객체로 만드는 것은 선택적인 요소이다. 필수가 아니다.

BigDecimal을 포장한 Money 객체(VO)에 대한 고찰 및 피드백

  • 불변객체로 만들 수 있게 됨으로써 불변객체의 장점(안정성)을 얻을 수 있다.
  • 역할과 책임을 명확하게 만들어줄 수 있다.
  • 객체 생성 시점(정확하게는 객체가 생성되기 직전에)에 유효성 검증을 할 수 있게 됨으로써 잘못된 객체 생성을 방지한다.
  • 해당 객체(여기서는 Money)에 관련된 역할들만 따로 분리함으로써 응집성이 높아지고 의존성이 낮아진다(객체화 시켰을 때 흔히 나오는 장점). 그리고 이를 통해 코드의 유지보수성이 좋아진다.
  • 객체의 네이밍 자체로 이 객체가 어떤 행위를 할지 유추할 수 있게된다(네이밍의 중요성!!).
  • Moneyadd, multiply, divide 등의 연산 기능을 수행했을 때, BigDecimal로 결과값을 리턴하게 되면 해당 값에 대한 유효성 검증을 할 수 없게 된다. 따라서 Money의 연산 기능들의 리턴 타입은 Money여야 하지 않을까?

추가 피드백

  • 상황에 맞는 설계와 구현 방법을 찾도록 하자.
  • 반복문 대신 재귀 함수로 구현할 수도 있다.
  • 원시 타입과 문자열을 포장하도록 하자.
  • 적절한 Collection(자료구조)을 활용하도록 하자.
  • 객체에 메시지를 보내자(enum 포함)

참고자료


About

로또 게임

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages