나의 독학은

[프리코스 3주차] 로또 게임 회고 본문

회고/우아한테크코스 6기 프리코스 회고

[프리코스 3주차] 로또 게임 회고

안종혁 2023. 11. 9. 16:34

😊 3주차 목표

  • <구현 → 테스트 코드 → 커밋> 순서 지키기
  • 객체지향 코드를 위해 노력하기
  • 1주차 및 2주차 피드백반영하기
  • 손이 가는대로 코드를 작성하지 말고 미리 생각하기

😊 로또 게임 구현

☆ 코드의 전문입니다.

 

사용자를 위한 어떤 프로그램을 만들건지 생각하기

이번 요구사항은 1등 당첨금(20억)만큼의 돈을 쓰면 얼만큼의 수익률이 생길지 궁금해하는 사용자가 이 로또게임을 많이 이용할 것이라 생각했고 이에 맞게 설계와 예외처리를 진행했다.

 

MVC 사용

[학습]

3주차 요구사항에 "도메인 로직에 대해 단위테스트 하기, 도메인 로직과 UI 로직을 분리해라" 가 나왔기 때문에 도메인 로직에 대해 알아봤다.

 

이 글에서 은행 업무를 예시로 어떤 로직이 도메인이고, UI 인지 설명 해줘서 도움을 많이 받았다.

도메인로직에 대해 정확히 모르면 꼭!! 읽어 보길 강추한다.

 

디스코드의 글을 읽으며 알게 된 MVC패턴을 도메인로직과 UI로직들을 분리하기 위해 3주차 미션에 적용하고 싶었다.

 

다음은 MVC 패턴 학습에 쓰였던 블로그 글이다.

소프트웨어 설계의 근본 원칙, MVC창시가자 말하는 MVC의 본질

 

다음은 MVC 패턴 학습에 쓰였던 10분 테코톡이다.

제리의 MVC

해리&션의 MVC

베루스의 MVC

 

학습한 결과 MVC패턴은 비지니스 로직인 Model과 변경에 취약한 View의 분리를 통해 유지보수 및 변경에 용이함을 위해 고안된 디자인 패턴 중 하나인 것을 알게 되었다.

 

나의 코드가 MVC패턴 본질을 잘 지킬 수 있도록 다음과 같은 역할에 집중했다. 

Model : 정제된 데이터를 꺼내 쓸 수 있는 역할

View : 사용자와 인터랙션하는 역할

Controller : View와 Model을 연결하는 역할

 

[적용 과정]

유튜브와 블로그들을 여러 개의 탭을 띄우고, 영상 속 코드와 내 코드를 비교해 가면서 천천히 구현했다.

제리님 영상의 8분 26초에 나오는 "MVC의 5가지 원칙" 을 보며 Model과 View간의 분리가 잘 되었는지 확인했고,

해리&션님 영상의 6분 48초에 나오는 "이게 바로 Controller다." 와 베루스님 영상의 5분 17초에 나오는 "Controller" 를 통해 Controller는 모델과 뷰를 연결하는 역할만 하고 있는지 확인했다.

 

특히, 이번주 미션은 객체지향 코드로 구현하기 위해 코드 한 줄을 구현하더라도 객체가 맡을 책임과 수행할 메시지에 대해 생각할 시간을 충분히 가졌고 <구현 → 테스트 코드 → 커밋> 순서와 학습한 MVC패턴도 지키려고 노력했기에 구현 속도가 매우 더뎠다.

 

[적용 결과]

적용하며 2개의 고민을 해결해서 소개한다.

 

[입력값의 검증은 어디서 할지에 대한 고민 해결]

View는 Model에서 데이터를 받을 때만 연결이 되어야 하기 때문에 View에서 입력값 검증을 하면 안된다고 생각했고, Controller도 View와 Model을 연결하는 역할이라서 Controller도 아니라고 생각했다.

 

그래서, Model에서 입력값인 String 타입을 검증하기로 했다.

이 때, 생성자에서 검증을 하도록 했는데 그 이유는 검증을 통해서 객체가 만들어 지면 이 객체는 보장된 객체란 것을 의미한다고 생각하기 때문이다.

 

예시로, 사용자 입력으로 생성되는 Price 객체이다.

사용자 입력으로 Price 객체가 만들어지고, 생성자에서 검증하는 모습

만약, 이 객체가 생성된다면 사용자가 올바르게 입력했다는 것을 보장한다고 생각했기 때문이다.

마치 일급컬렉션을 사용한 객체는 보장된 객체인 것처럼 말이다.

 

["사용자 입력이 오류면 다시 그 부분부터 받아야 한다" 中 3주차 요구사항]

이 요구사항 처리하는게 진짜 어려웠다.

new로 사용자 입력을 다시 받지 않으면 새로운 사용자입력이 들어와도 이전 사용자입력 메모리에 할당된 입력 데이터가 그대로 남아있게 된다 (로또 가격1500원을 입력하고, error 난 후, 3000원을 입력하면 랜덤로또가 1개만 생성된다).

그래서, 사용자 입력을 new로 다시 받아야 했다.

 

그렇다면 Model, View, Controller 중 어디서 사용자 입력을 new 로 할 것인가?

 

처음에는 가격을 입력 받는 InputView 에서 new Price 객체를 생성해서 Controller 에게 넘어주는 식으로 했지만, View는 Model 의 데이터에 직접 관여하면 안된다는 규칙을 보고, 다시 고민하였다. 

 

많은 고민 끝에, "MVC 창시자가 말하는 MVC의 본질"이란 글의 MVC스러운 협력에서 답을 찾게 되었다.

"뷰에는 데이터를 보여주고, 사용자가 인터랙션을 할 수 있게 만들어주는 최소한의 코드만 들어간다.
나머지는 컨트롤러에게 위임한다."

 

그래서, 아래 코드 처럼 Controller 에서 Price 객체를 만들고, Price 에게 사용자 입력(String 타입)을 검증하여 사용자 입력을 new 로 만들었다.

그리고, Price 객체에 오류가 있다면 Controller 에서 View에게 다시 입력하라는 메시지를 주도록 구현했다.

이를 통해 생성된 Price 는 보장된 객체이자 깔끔한 데이터만 사용할 수 있다 생각했고, View 도 최소한의 코드로 사용자와 인터랙션만 할 수 있다고 생각했다.

private Price createPrice() {
    try {
        return new Price(InputView.inputPriceForLotto()); // Price에서 사용자입력값을 검증하는 동시에 보장된 객체 반환
    } catch (IllegalArgumentException e) {
        System.out.println(e.getMessage());
        return createPrice(); // Price에서 사용자입력에 오류가 나면, 다시 입력을 받도록 createPrice 를 호출
    }
}

이를 통해 사용자의 입력을 받는 InputView의 역할이 엄청 가벼워졌다.

많은 생각을 통해 해결했지만 좋은 코드인지는 불확실하다. 아시는 분은 피드백을 주시면 정말 감사하겠습니다.

 

객체지향 처럼 단언하지 말고, 이 역시 꾸준히 고민해보자.

 

일급컬렉션 사용(?)

여러개의 랜덤로또들과 한 개의 당첨번호를 비교해서 결과를 출력하는 요구사항이 있었기 때문에 여러개의 랜덤로또들을 담을 무언가가 필요했다.

끝내 List<Lotto> 를 생각하게 되었지만, 객체를 List 에 담으면 값을 어떻게 꺼내는 것인지 이해가 가지 않던 나는 이를 활용하기 까지 구현했던 코드를 2번 엎고 List<Lotto> 타입을 사용하는 객체를 3번이나 바꾸게 되었다.

그래도 이러한 시행착오 덕분에 Lotto 객체에 검증 하는 로직이 있다면 List<Lotto> 는 보장된 Lotto 들만 있음을 알 수 있었고, 일급컬렉션의 글의 일급컬렉션을 사용하는 모든 List는 검증로직을 안써도 된다는 말에 깊은 공감을 할 수 있었다.

다만, 나머지 일급컬렉션의 장점들은 아직 공감을 하지 못했다.

일급컬렉션을 사용하며 나머지 장점들을 깨달을 수 있게 앞으로도 생각 많이 해야지

 

더불어, 2주차에서 객체의 private한 상태를 public 한 곳에 노출 시켰는데, 이에 대한 고민의 해결은 원시타입을 일급컬렉션 처럼 Wrapping하면 된다는 것을 알게 되었다.

하지만, Wrapping만 했을 뿐 데이터의 변경을 위해 상태를 노출 시키며 미션을 완주하였기 때문에 이는 여전히 해결해야할 문제로 남았다.

 

행동과 책임이 있는 Enum 사용기

Enum을 사용하라는 요구사항에 맞춰 단순히 당첨등수를 반환하는 열거형 Enum을 사용하고자 했다.

그러자, if문이 우후죽순으로 늘어나게 되었다.

/* Enum 객체인 Rank */
public enum Rank {
    FIRST(1), SECOND(2), THIRD(3)
}

/* matchBall은 로또에서 맞춘 번호의 개수, bonusball은 보너스번호의 당첨여부 */
if (matchBall == 6 && !bonusball) {
    return Rank.FIRST;
}
if (matchBall == 5 && bonusball) {
    return Rank.SECOND;
}
if (matchBall == 5 && !bonusball) {
    return Rank.THIRD;
}
...

 

if문의 중복이 맘에 들지 않았던 나는 처음으로 단순 열거형 Enum이 아닌 객체처럼 책임이 있고, 행동이 있는 Enum을 사용하게 되었다.

이 과정에서 우아한기술블로그의 코드를 참고하고, 자바의 정석을 보며 Enum을 다시 학습하게 되었다.

 

행동과 책임이 있는 Enum을 구현하는 일은 쉬운일이 아니었고, 많은 시행착오를 겪는 과정에서 Enum 구현에 집중한 나머지 <구현 → 테스트 코드 → 커밋>을 지키지 못해서 아쉬웠다.

 

그래도, Enum을 사용해서 String과 int와 boolean이 결합된 쉬운 자료구조를 만들 수 있는건 완전 대박이다.

Map<boolean, Map<int, String>> 같은 이중구조로 된 Map을 사용해나 싶었는데 Enum을 통해 깔끔하게 정리되었고,

당첨 등수의 기준이 달라지게 되더라도 리팩토링을 쉽게 할 수 있다는 느낌을 받게 되었다.

 

2주차에 이은 테스트 코드의 장점

MVC패턴의 본질을 지키고, 일급 컬렉션의 올바른 사용, 책임과 메시지가 있는 객체지향 코드로 미션을 구현하기 위해 집을 갈때도, 씻을 때도, 잠이 들기 전에도 객체간의 의존 관계를 어떻게 설정할지, 누구에게 메시지를 보낼건지 등을 생각했었다.

이런 생각들을 다음날 아침에 바로 적용하기 위해 이미 구현했던 코드를 지우고 생각해낸 방법으로 다시 구현하는 과정을 엄청 많이 반복하였다.

 

이 과정에서 테스트 코드를 미리 작성한 덕분에 많은 시행착오를 겪더라도 구현한 로직이 틀리지 않았음을 검증하는 시간이 매우 단축되었다.

다만, Enum을 구현하면서 머릿속이 복잡해지는 바람에 이 후로는 기능을 구현하며 테스트코드를 같이 작성하지 못한 것은 비밀이다(????)

 

정복하지 못한 객체 지향

미션 구현 도중에 List<Lotto> 를 이용하는 Lottos (제출 시에는 RandomLottos로 변경) 란 객체를 생성해서 써야하기 때문에 설계를 다시 해야했다.

이 때, 오브젝트의 설계부분을 다시 읽었고, 특히 '정보 전문가'를 생각하며 책임을 잘 수행할 객체를 선별하기위해 많은 고민을 했다.

그리고, '조영호님의 객체지향'이란 세미나를 시청하며 연관관계와 의존관계를 학습하며 설계에 적용했다.

 

연관관계와 의존관계의 코드는 아래와 같다.

A → B : 연관관계
class A {
    private B b;
}

A ---> B : 의존관계
class A {
    public B method (B b) {
        return new B();
    }
}

 

연관관계와 의존관계 중 어떤 것으로 객체 간의 관계를 설정해야 할까?

좋은 예시가 있다.

 

보통 사람들은 장바구니(Order)와 장바구니 항목(List<Order>)을 연관관계(객체참조)를 이용해서 강한 결합을 해야 한다고 생각한다.

하지만, 이 둘의 관계는 의존관계로 해야한다. 그 이유는 장바구니와 장바구니항목의 생성시기가 다르기 때문이다.

장바구니가 생성되는 시기는 사용자가 앱을 키자마자이고, 장바구니 항목은 사용자가 앱을 사용해서 물품을 담아야만 생기기 때문이다.

같이 생성되고, 수정되고, 삭제되는 객체들을 연관관계(객체참조)를 이용해서 결합해야 한다.

그래서, 장바구니와 장바구니 항목은 연관관계로 강한 결합을 할 것이 아니라, 변경에 용이할 수 있도록 서로 다른 객체로 결합을 느슨하게 할 수 있는 것이다.

 

하지만, 이 깨달음을 얻고도 나는 객체간의 좋은 의존 관계를 맺지 못했다.

Enum을 구현하면서 스파게티 코드가 되버렸고, 무엇보다 설계하지 못했던 기능들이 나오는 바람에 구현하는데 급급했다.(이 때가 5일차여서 설계를 다시하기 보다는 구현이 급했다.)

결국 private 상태를 꺼내오는 get 메서드를 남발하고 있었고, MVC 패턴만 지킬 뿐이지 객체지향 코드를 완전 지키지 못했다.

1주차부터 객체지향에 대해 많은 시간을 쏟았지만 아직도 정복하지 못했다.

 

그 이유를 생각해 보았는데 아마 설계가 많이 부족해서 아닐까 싶다.

3주차 미션 시작 전에 설계했던 README와 구현을 끝낸 README의 차이가 워낙 심했기 때문이다.

 

그래서 차이가 워낙 심했던 이유를 생각해봤다.

1. 객체를 의인화 하지 못함. 이런 것도 객체로 해야되는 걸 몰랐음.

→ 이번에 원시타입을 Wrapping 하고 일급컬렉션도 알게 되며 이런 것도 객체로 봐야하구나를 알게 되었으니 해결인가?

 

2. 기능을 작은 단위로 나누지 못해서 구현 과정 중 추가되는 기능이 많음

→ 요구사항을 보며 기능을 더 세부적으로 나누지 못한다. 근데, 함수분리를 하며 느낀건데 함수분리를 할수록 기능을 작은 단위로 나눌 수 있는 눈이 생기는 거 같다.

 

3. 사용자의 입력부터 구현하고 있었음.

→ 조영호님의 세미나를 보며 깨달은게 내가 구현할 때, 사용자의 입력부터 구현하기 시작했으니 당연하게 절차지향적으로 짠 것 같다. 그리고, 이번 3주차 코수타에서 핵심 기능을 먼저 생각하고, 구현하라는 가르침에 "어, 이러면 객체지향적으로 짤 수 있지 않을까?" 란 생각을 하게 되었다.

 

4주차는 코치님의 가르침대로 먼저 핵심 기능을 생각하고, 구현해보며 객체지향을 정복해보자!

 

그 외 노력들

  • 1,2주차에는 에러메시지를 사용자가 프로그램을 이용할 때, 헷갈리지 않는 조건으로 설정했었다. 객관적으로 좋은 에러메시지란 무엇인가를 알기 위해 좋은 에러 메시지를 만들기 위한 6가지 원칙을 보았고, 좋은 에러 메시지를 만들도록 노력했다.
  • 마크다운 사용법을 보며 리드미를 깔끔하게 작성하도록 노력했다.
  • 2주차 피드백을 보고, 리드미 기능 목록과 커밋메시지에 클래스 및 함수명을 넣지 않고 커밋했다.(중간부터 시작) 
  • 1주차 피드백의 테스트 강의 영상을 보며 @BeforeEach 사용과 테스트코드를 직관적으로 이해할 수 있게 좋은 네이밍으로 선언하도록 노력했다.
  • 좋은 네이밍을 위한 규칙을 학습하고 함수 분리를 하며 나의 코드를 읽는 사람이 영어지문을 읽는 느낌을 받을 수 있도록 노력했다.

😊마치며 

독학 할 때는 코딩하려고 밖에 나와있는 시간은 10시간이었다. 이 중, 열품타에 기록된 순공 시간은 6~7시간 정도였다.

 

아침에 일어나서 도서관 가서 공부하고, 운동하고 집에 돌아오면 22~23시일 정도로 하루를 알차게 보낸다고 생각했기 때문에 스스로 정말 열심히 살고 있고, 충분히 만족을 하고 있었다.

그리고, 이것이 내가 코딩을 할 수 있는 최대 시간이라고 생각했다.

그러나, 프리코스에서는 밤 10시에 도서관에 나오면서 해결하지 못한 고민들을 집에 가면서도, 집에 도착해서도 고민했다.

"생성자로 검증하면 어떤 이점이 있는거지?" , "WinningLotto의 상태를 어떻게 Lotto 로 만들지?", "일급 컬렉션이 뭔 말이지?" 이 고민들을 잠 자려고 누운 상태에서도 하다 보니 두뇌 회전이 되는 바람에 잠이 깨버려서 새벽에 코딩도 했다.

아침에는 어제 한 생각들을 빨리 적용하거나 해결되지 않은 고민들을 해결하고 싶은 마음에 아침 일찍 일어나 학교를 가서 코딩을 했다. 그러자, 코딩하러 밖에 나와 있는 시간은 12시간~14시간으로 평균 2~4시간이 늘었다.

평소와 달리 더 많이 코딩을 해도 지치지 않았다.

오히려, 노트북을 덮었는데도 코딩 생각을 하는 나를 보며 기존 학습방식말고 이 미션 중심의 공부를 진행해야 하는 필요성을 느꼈다.

 

미션 중심의 공부를 하며 내가 성장하고 있는 것을 느꼈다. 아래 3가지는 성장하고 있는 나의 느낌을 구체화 한 것이다.

 

1. 어떤 것을 학습하고, 바로 적용하는 것은 쉽지 않았지만, 적용하고 싶었기에 더 많은 시간을 코딩하게 되었다.

2. 어떤 것에 힘들어 하는지 알게 되어 보완하려고 노력했다.

    예를 들면, 나는 람다식 활용에 힘들어 하는 것을 알게 되어 모든 로직에 람다식을 적용하려고 노력했다.

3. 제일 중요한 것은 스스로 생각하는 시간이 월등하게 많아졌다는 점이다.

생각하는 시간이 많아질수록 궁금한 점들이 많이 생겼고, 이 궁금증들을 해결하고 싶다 보니 자연스레 공부에 더 집중하였다.

또, 궁금증을 해결하기 위해 검색하고 책을 들여다보는 과정에서 다양한 것들을 알게 되었다.(비록 미션에 바로 적용시키진 못하더라도 나중에 분명히 도움되는 것을 알기에 좋았다)
이렇게 궁금증이 해소되는데 걸리는 시간이 길어질수록 궁금증을 해소했을 때의 오는 쾌감, 깨달음을 얻을 때에 오는 그 소름과 희열감을 얻는 횟수가 독학할 때와는 비교할 수 없을 정도로 많아졌다.

 

기존 독학방식은 <책, 강의 시청 이해 기록> 이었는데,

앞으로 <책,강의 시청 미션 or 코드에 적용하며 이해 기록> 순으로 해야 겠다.

끝으로 MVC패턴, 객체지향 코드, <구현 → 테스트 코드 → 커밋> 순서, 여러 어노테이션을 사용한 테스트 코드, 올바른 커밋메시지, 살아있는 README 작성과 같이 3주 동안 학습한 것들을 코드에 적용하는 것에 아직도 익숙하지 않은 바람에 몇 개는 지키지 못했다. 그래서인지 항상 미션이 끝나더라도 아쉬움이 남는다.

 

4주차에는 어떤 아쉬움도 없도록 임해보자!!