본문 바로가기
대외활동/[미션] 우아한테크코스 프리코스 | Java

[우아한테크코스] 프리코스 3주차 : 로또 (lotto)

by HelloJudy 2022. 11. 13.
이 글은 '프리코스' 과정에 작성한 글이며 현재 레포지토리에 있는 코드와 다를 수 있습니다. (리팩토링함)

 

 

🚩 3 주차 목표

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

이 두 가지에 익숙해지는 것을 목표로 하고 있다.


 

[3주 차] 미션 - 로또 🎰

 

🏃 리팩토링 전 프리코스 제출 코드 시점

👉 미션 저장소

📎 Source Code (PR)

📆 기간: 2022.11.09(수) 15:00 ~ 2022.11.15(화) 23:59

 

+ 프리코스 이후 리팩토링 포스팅

객체를 객체스럽게 사용하도록 리팩토링해라.


1. 구현 과정

 

미션은 기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항 세 가지로 구성되어 있다.

일단 처음엔 기능 요구 사항을 채우는데 집중한다. 프로그래밍 요구 사항은 일단 눈으로 체크해두고 구현부터 집중!

그다음 프로그래밍 요구 사항을 만족시키기 위해 리팩터링 하자!

 

🛠️ 프로그램 풀기에 앞서 환경 세팅 확인

 

  • 프로젝트 SDK 확인

 

  • Gradle 확인

 

⚙️ 기능 구현 목록 작성

  • 유저 사용 관점에서 크게 메인 MVP의 프로세스, 필요한 도메인 등을 정리했다.

 

 

🚀 새롭게 도전한 것들 (혹은 구현하면서 고려한 점)

1️⃣ Random Numbers

이전 미션인 숫자 야구 게임과 동일하게 랜덤한 숫자를 받아오는 기능이 필요했다.

이전과 달라진 점은 

  • 숫자 야구 게임: 게임 인스턴스 하나에 랜덤 숫자가 한 번만 불러온다. 그리고 그 숫자가 유지된다.
  • 로또: 게임 인스턴스 하나에 랜덤 숫자를 생성하는 메서드를 여러번 호출한다.

 

그래서 인스턴스 생성 시 랜덤 숫자가 인스턴스 변수에 할당되던 코드에서

2주차: 숫자 야구 게임

 

클래스의 메서드를 호출해서 랜덤 숫자를 받는 코드로 변경했다.

 

 

2️⃣ validator 상속

 

현재 나는 숫자 1개에 대한 validation과 숫자 여러 개에 대한 validation으로 validator를 분리해서 구현하고 있다.

이때 input 값이 숫자인지 확인하는 코드가 중복되었다.

 

  1. input 들어온 숫자가 숫자가 맞는지 확인
  2. 1,2,3,4,5,6 로 들어온 input을 [1,2,3,4,5,6] 형태로 만든 리스트가 숫자로 이루어졌는지 확인

 

  • NumberValidator를

  • NumbersValidator에서 상속받아 구현했다.

 

최대한 중복 코드를 줄이고 객체 지향으로 설계하고 싶어 노력하지만 아직 부족하고 이 방법이 맞는지 확신이 없다!! 

하지만 계속 노력하고 공부해나가자!!

 

 

3️⃣ Enum type

 

📎[참고 자료] 우아한 형제들 기술 블로그, Java Enum 활용기

 

 

구현을 완료하고 리팩터링 하기 위해서 코드를 보는데 역시 마음에 들지 않는다. 🧐 

갑자기 떠오른 요구사항! 

이때 일치하는 숫자 개수와 상금, 보너스 볼 일치 유무를 enum으로 만들 수 있다는 생각을 했다.

enum 상수와 연결된 값을 가져올 수 있도록 했다.

 

 

🧐 구현하면서 고민했던 것들

 

1️⃣ validation은 누구의 역할일까?

validation은 누가 맡아서 해야하나.

 

❓항상 고민했다. validation은 어디서 해야 할까. 현재 폴더 구조에서 Controller, Domain, View 중 누군가 담당해야 한다.

이전 숫자 야구 게임에서는 프로그램의 가장 앞단에서 걸러주어야 한다고 생각하여 View에서 대부분 validation 해주었다.

이번에는 View에서는 정말 input만 받고 완전히 Controller와 Domain으로 그 책임을 넘겨주려고 했지만

 

View에서 사용자 입력에 대해 최소한의 유효성은 확인해주어야 한다.

 

 

예를 들어 구입 금액을 입력받을 때 처리하는 예외 처리두 가지이다.

  1. 숫자가 아닌 값이 들어온 경우
  2. 1,000원으로 나누어 떨어지지 않는 경우

이때 1번 예외는 현재 input을 받는 메서드에서 반환 타입이 int이기 때문에. 즉, 숫자를 입력받기 위해서 최소한의 값의 정합성을 확인하는 것이다

 

그리고 2번 예외는 현재 구현하고자 하는 프로그램 도메인의 규칙에 맞는 입력인지 확인하는 것이다.

 

 

🚩 결론 : 입력을 받는 최소한의 예외 처리는 View에서 나머지 규칙에 따른 예외 처리는 필요한 위치에서! 

 

유효성 검사를 두 가지 성격으로 나누니 고민이 해결되었다!! 그리고 이런 것을 결정하는 것은 정답은 없고 개발자의 설계에 따라 달라진다. 위에서 로또 구매 금액을 받을 때 유효성 검사는 두 가지라 헷갈리지 않게 View에 두 가지 모두 작성했다.

 


 

/* 우와 😃 이제 잘 돌아가나~?? 구현 끝?!!

테스트 돌려볼까!?!

그리고 시작된 오류 전쟁 */

 

 

😫 험난한 트러블 슈팅

1️⃣ ImmutableCollections

 

시도 1) 랜덤 숫자 리스트를 불변 -> 가변으로 변경해서 정렬

 

📎 stack over flow

 

에러의 내용은 불변 컬렉션을 수정하려고 해서 생긴 오류였다.

java.lang.UnsupportedOperationException
	at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:71)
	at java.base/java.util.ImmutableCollections$AbstractImmutableList.sort(ImmutableCollections.java:110)
	at java.base/java.util.Collections.sort(Collections.java:145)

 

프로그래밍 요구사항에 맞추어 camp.nextstep.edu.missionutils에서 제공하는 Randoms를 사용하고 있다.

이때 나는 랜덤 6개의 숫자 리스트를 정렬해서 변경하고자 했다. 

 

현재 코드

불변 컬렉션을 수정하고자 해서 오류가 나고 있다.

주어진 Randoms 클래스의 구현체를 확인해보자.

 

Randoms 클래스의 pickUniqueNumbersInRange 메서드

 

shuffle(numbers).subList(0, count); 를 리턴하고 있다.

subList에 대해서 알아보자.

 

 

✔️ subList

 

Java의 List 사용 시 일부분을 잘라내기 할 경우 해당 메서드를 사용하는 경우가 있다.

 

이때 ArrayList의 SubList는 자신이 생성된 parent 값을 가지고 있다고 한다.

 

그래서 아래와 같이 새로운 리스트에 넣어줄 수 있다.

List<Integer> subList = new ArrayList<>(alist.subList(0, 3));

 

돌아와서

수정할 수 없는 리스트라면 수정할 수 있는 리스트를 생성해주면 된다.

 

(시도1)

 

modifiableLottoNumbers라는 새로운 리스트에 값을 넣어준 뒤 sort 했다.

 

 

하... 이제 되겠지? 

 

 

엉엉 ㅠㅠ 성공쓰 🥴

 

 

시도 2) 원본 데이터를 수정하지 않고 정렬

 

랜덤 숫자 리스트를 정렬하기 위해서 원본 데이터를 수정할 수 있는 형태로 생성했다.

하지만 시간이 지나고 고민하니 객체의 설계상 랜덤 숫자 데이터는 변경되면 안 된다는 생각이 들었다.

 

❗ 또한 원본 데이터의 수정을 가하지 않는 방법이 더욱 좋은 코드라고 생각했다.

그래서 원본 데이터의 수정 없이 정렬하는 형태를 다시 찾아보았다. 

 

이때 발견한 게 Stream API이다. 다음과 같은 특징이 있었다.

 

  • 원본의 데이터를 변경하지 않는다.
  • 일회용이다.
  • 내부 반복으로 작업을 처리한다.

 

Stream API를 제대로 사용해본 적이 없어 아직 2권까지 넘어가지 못한 자바의 정석을 펼쳤다.

 

👉 공부한 내용을 작성한 포스팅! : [Java] 람다와 스트림 (Lambda & Stream)

 

 

역시 큰 그림을 공부하고 세부적인 건 구현하면서 필요할 때마다 그때그때 학습하는 것이 효율적이다.

 

 

다음과 같이 최종적으로 코드를 완성했다.

(시도2)

 

하지만

끝이 아니다.

 

다음 트러블 입장하세요~

 

 

2️⃣ 예외 처리

현재 에러 처리에 대한 요구사항과 주어진 테스트 케이스이다.

이때 평소처럼 에러를 던져줬는데 계속 실패했다. ㅠㅠㅠㅠ

 

 

왜..? 왜 실패야???

(요구사항을 다시 읽고) 출력하라고?? print? 오케이

 

시도 1) 에러를 던졌을 때 그 에러 메세지를 출력하도록 구현해봤다. 🧐

 

결과는...  땡!!!

출력하라며!!! 

 

진정하고... 위 코드는 잘못된 게 맞다. 에러를 던져줬을 때 catch문에서 print를 수행하고 코드가 종료되지 않고 그대로 진행된다. 

 

시도 2) 테스트 코드 뜯어보기

 

@Test
    void 예외_테스트() {
        assertSimpleTest(() -> {
            runException("1000j");
            assertThat(output()).contains(ERROR_MESSAGE);
        });
    }

 

이 코드에서 output()[ERROR]가 포함되었는지로 테스트 성공 여부를 판단하고 있다.

 

그래서 output()이 어떤 값이 넘어오고 있는지 출력해봤다.

 

또잉.. 똑똑 출력 거기 없소? output()에 아무 값도 넘어오지 않는다.

throw new IllegalArgumentException(NON_NUMERIC_ERROR_MESSAGE); 를 했는데... 

 

이유를 곰곰이 생각해보며 위에 다른 케이스에도 output()을 출력해보았다.

Application에서 로직을 실행시켜 출력된 값들이 output()에 전부 담겨있었다.

 

 

그리고 계속 고민하다가 머리가 너무 아파서 양치를 하다가 갑자기 떠올랐다.. 

기존 Application.java

 

Application.java에서 에러가 나왔을 때 그 에러 메세지 출력을 여기에서 해줘야 하나?! 

수정된 Application.java

세상에 ㅠㅠㅠ 이거구나 ㅠㅠ 이거네... 마침내 깔끔하게 출력된 에러 메시지를 만날 수 있었다.. 크흡

 

🐙문어🐙지지 않았어 ㅠㅠ

정말 여기 문제에 갇혀 하루를 날려버렸다...

 


 

마지막으로 변수명 짓는 건 참 어렵다 ㅠㅠ

변수명 지을 때 파파고와 함께...😍


2. 최종 폴더 구조 🗂️

├─main
│  └─java
│      └─lotto
│          │  Application.java        -  로또 게임 애플리케이션 실행 
│          │
│          ├─controller
│          │      LottoGame.java      -  domain-view를 연결하여 게임 순서 결정
│          │
│          ├─domain
│          │      CompareLotto.java   -  구매한 로또 번호와 당첨 번호를 비교
│          │      Lotto.java          - 로또 클래스
│          │      LottoResult.java    - 당첨 내역
│          │      MatchCount.java     - 당첨 개수와 상금 enum 클래스
│          │      RandomNumbers.java  - 랜덤 6개 숫자 리스트를 만드는 클래스 
│          │
│          ├─util
│          │      Transform.java      - data의 형태를 변환하고 형변환하는 util 함수
│          │
│          ├─validator
│          │      NumbersValidator.java   - 여러개 숫자에 대한 예외 처리
│          │      NumberValidator.java    - 한 개 숫자에 대한 예외 처리
│          │
│          └─view
│                  InputView.java     - Console에서 Player의 input을 받기
│                  OutputView.java    - Console에서 Player에게 output 보여주기
│
└─test
    └─java
        └─lotto
            │  ApplicationTest.java
            │  LottoTest.java
            │
            ├─domain
            │      CompareLottoTest.java
            │
            └─util
                    TransformTest.java

 


3. 구현 완료

 


마무리

 

이번 주에 깨달은 것은 개발에는 체력이 중요한 것 같다.

폭발적으로 성장하는 것도 중요하지만 안되는 것에도 포기하지 않는 체력(강한 마음)이 제일 중요한 것 같다.

 

개발을 하다보면 모든 일이 물 흘러가듯 쉽게 흘러가지 않는다.

이제 다 알았다고 생각하지만 절대 그렇지 않고 더 모르는게 생기고 좌절하는 순간을 맞이하게 된다.

(과거에 나도 자신감 뿜뿜해서 참여한 프로젝트에 좌절한 적도 있다.)

 

그러한 상황에서 포기하지 않고 다시 시작할 수 있는 힘을 키우자.

지금 이렇게 공부하고 몰입하여 개발하는 과정은 체력을 키우는 과정인거 같다.

너무 높고 커보이는 벽 앞에서 포기하고 뒤돌지만 않는다면 언젠가는 해결할 수 있다.

 

개발에서 얻은 깨달음이지만 앞으로 삶을 살아가면서 자세도 배우게 되는 것 같다.

요즘 일상이 개발 그 잡채(?)인거 같은데...

비개발 직군이랑도 이야기합니다.. 저두(?)

 

 

자바 강의를 들으며 공부하고 있었지만 이렇게 자바 언어로 프로그래밍한 경험은 몇 주되지 않았다.

1주차에는 코드 구현도 벅차 허둥거렸지만 (자바의 타입과 클래스는 왜이렇게 단순하지 않아~!!)

코드 구현에 좀 더 익숙해지니 그때 쓰지 못한 신경을 좀 더 코드 퀄리티에 쓸 수 있었고,

또 어느정도 여유가 생기니깐 테스트 코드 작성도 좀 더 공부해볼 수 있었다.

 

단지 3주만 지났는데 성장하고 있다는게 느껴지고 아직 정말 시작일 뿐이다. 파이팅🥴

 

반응형

댓글