본문 바로가기
📝 회고/🤝 코드스쿼드 백엔드 프로젝트

[프로젝트 회고] : 로또 게임 (OOP, TDD, 1급 컬렉션, 페어 프로그래밍)

by kukim 2022. 3. 2.
  🔢 로또 게임 프로젝트 (전체 소스코드)
배운 것 페어 프로그래밍, OOP, TDD, 1급 컬렉션, eum, DCI 패턴 Unit Test ...
기간 22.02.21 ~ 22.02.25 (5일) 
팀원 @쿠킴 @땃쥐 
Step 1 기본 기능 구현 / 1단계 PR
Step 2 보너스 번호 추가 / 2단계 PR
Step 3 수동구매 기능 추가 / 3단계 PR
Reviewer @Honux, @Dion

로또 게임은 콘솔 프로그램  프로젝트이다. 특별한 점은 1주일 동안 모든 과정을 페어 프로그래밍을 했다. 1주일이 짧은 시간이지만 모든 코드를 팀원과 함께 구현하는 과정을 통해 극악의(?) 환경에서 협업을 작게나마 배울 수 있었다. 사다리 게임에서 배웠던 TDD, DCI 패턴의 Unit Test를 다듬을 수 있었고 도전 과제였던 1급 컬렉션, enum을 연습할 수 있었다. 부족함을 배울 수 있는 시간이었다.


프로젝트하며 배운 

커뮤니케이션 : ✅ '아이엠 그라운드 자기소개하기'도 좋지만 '그라운드 룰(Ground Rule)'도!

개인적으로 처음보는 사람과도 아이스 브레이킹, 잡담을 통해 쉽게 친해지는 편이다. 페어 프로그래밍을 한다면 보통 1일 1~2시간 정도만 경험해봤다. 하지만 이번 프로젝트는 1주일 동안 1일 거의 8시간씩 반강제적(?)으로 내비게이터, 드라이버 룰을 지키며 페어 프로그래밍을 했다. 첫날 둘이 금방 친해져 쉬는 시간 없이 계속 코딩했다. 밤 11시 넘어서까지 코딩을 하게 되었고 이건 무언가 잘못되었다고 느꼈다.

페어 프로그래밍, 협업을 하기 위해선 상호간의 지켜야 할 그라운드 룰(Ground Rule)이 필요하다고 느꼈다. 기술적인 룰 코딩, git 컨밴션, 네이밍 등도 필요하지만 집중할 시간은 언제인지? 휴식 시간은 언제인지? 프로젝트 시작과 끝에 스크럼 방법 등 상호 간에 룰이 필요하다는 것을 느꼈다. 이 룰은 둘 사이의 협업을 잘하기 위한 방법이지만 확장하자면 작은 4~5명 팀 단위, 크게는 팀 전체의 문화까지 영향을 미칠 수 있겠다는 생각을 했다.

또한 상대방과 원할한 커뮤니케이션을 하기 위해선 1. 상대방을 짐작하지 말고 2. 텍스트가 아니라 컨텍스트(문맥)을 주고받아야 한다는 것을 알았다. 예를 들어 페어 프로그래밍 중 생성자 대신 팩토리 메서드를 쓸 때 단순히 팩토리 메서드 쓰죠!라고 말하기보다 팩토리 메서드를 사용하면 이름의 의미를 통해 가독성을 높일 수 있으니 팩토리 메서드 쓰는 게 어떤가요? 라거나 테스트 코드가 필요하다면 테스트 코드 해야 해! 보다는 현재 구현체의 설계가 감이 오지 않으니 TDD를 활용해 테스트 코드를 통해 구현체 설계를 조금씩 발전시키는 방법은 어떨까요?라고 이야기한다는 것이다.


Java : ✅  큰 숫자에는 _ 구분자로 나누기

큰 숫자가 있을 때 Java 언어에서 제공하는 '_'를 사용하여 가독성을 높일 수 있다. 19999999 -> "19_999_999"

 

Java : ✅ 일급 컬렉션 적용하기

저번 사다리 프로젝트에서 일급 컬렉션 적용을 알게 되었고 이번 프로젝트에서 적용해보았다.

복수의 로또 티켓(lottoTickets)을 List<> 형태로 가지고 있는 것이 아닌 LottoTickets 객체를 일급 컬렉션으로 만들어 사용했다. 일급 컬렉션 안의 List<LottoTicket> 멤버 변수를 불변으로 사용하고 있다. 일급 컬렉션의 장점은 향로님의 글에서 볼 수 있듯이 4가지가 있다.

1. 비즈니스에 종속적인 자료구조이다. 2. Collection의 불변성을 보장한다. 3. 상태와 행위를 한 곳에서 관리한다. 4. 이름이 있는 컬렉션이다.  추가적으로 완전한 불변을 위해 getter에 unmodifiableList도 추가해주었다. (참고 글)

// 전체 소스 코드 : https://github.com/ku-kim/java-lotto/tree/ttasjwi/src/main/java/domain/lotto

// 적용 전
Set<LottoTicket> lottoTickets = LottoTickets.createRandomTickets(money);

// 적용 후 : 컬렉션(LottoTickets안의 List<LottoTicket>에 불변성 보장
LottoTickets lottoTickets = LottoTickets.createRandomTickets(money);

// 소스코드
public class LottoTicket {

    private static final int LIMIT_LOTTO_NUMBERS = 6;
    public static final int LOTTO_TICKET_PRICE = 1000;

    private final Set<LottoNumber> lottoNumbers;

    // 생략
}



Java : ✅ enum 사용

로또 당첨 순위(Rank) 표현하기 위해 enum을 사용했다. 이유는 다음과 같다.

1. Rank(순위) 관련 데이터를 한 번에 묶기 위해서 

하나의 데이터, 순위만 가지고 있는 것이 아닌 당첨 금액도 함께 가지고 있고 싶었다.

 

2. 순위 결정 로직이 enum 안에

Rank.of(당첨 개수, 보너스 당첨 번호 여부)로 넘겨주면 enum 스스로 계산하여 해당하는 Rank를 리턴할 수 있었다. 상태와 행동을 한 번에 묶을 수 있었다. 

 

3. toString 오버라이딩

오버 라이딩을 통해 해당 enum의 정보를 외부에 쉽게 표현할 수 있었다.

public enum Rank {
    FIRST(6, 2_000_000_000L),
    SECOND(5, 30_000_000L),
    THIRD(5, 1_500_000L),
    FOURTH(4, 50_000L),
    FIFTH(3, 5_000L),
    FAILED(0, 0L);

    private final int countOfMatch;
    private final long reward;

    Rank(int countOfMatch, long reward) {
        this.countOfMatch = countOfMatch;
        this.reward = reward;
    }

	// 생략
}

C언어의 습관이 남아 에러 값을 -1로 하는 버릇을 고쳐야겠다.

 

Java : ✅ for vs stream 가독성 싸움

생각 : 성능이 중요하다면 for문을 사용, 가독성은 stream vs for 중 더 좋은 걸로 선택

처음 case1(stream)으로 구현했다. entry.getKey().getReware()에 의미가 없어 보여 case2(for문)으로 변경했다가 최종적으로 case3(stream+가독성)으로 변경했다.

public class LottoGameResults {
    // case 1
    private long getTotalReward() {
        return rankCounts.entrySet().stream()
                .mapToLong(entry -> entry.getKey().getReward() * entry.getValue())
                .sum();
    }

    // case 2
    private long getTotalRewards() {
        long totalRewards = 0;
        for (Map.Entry<Rank, Integer> entry : rankCounts.entrySet()) {
            long reward = entry.getKey().getReward();
            int count = entry.getValue();
            long totalReward = reward * count;
            totalRewards += totalReward;
        }
        return totalRewards;
    }
    
    // case 3
    private long getTotalRewards() {
        return Stream.of(Rank.values())
                .mapToLong(this::getRankReward)
                .sum();
    }
}

리팩토링 : ✅  변수명의 가로 길이도 너무 길면 가독성이 떨어진다

가로 길이가 너무 길었다.

 

리팩토링 : ✅  early return문은 한 줄로 끝내보자

early return문 사용할 때 조건을 반대로 주면 한 줄에 끝낼 수 있어 보인다. 특별한 이유가 없다면 가독성이 더 높은 쪽을 고르자. 아래 예에서는 한 줄에 끝내는 것이 좋아 보인다.


TDD: ✅  TDD 해보실래요?

팀원은 테스트 코드 없이 설계를 고민하고 구현하는 스타일이었고 나는 TDD 방식을 선호했다. 팀원에게 TDD를 설득했고 몇 가지 로직에 시도할 수 있었다. 테스트 코드 중심으로 초기 설계를 바꾸고 리팩토링 하며 객체의 설계를 유연하게 바꾸었다. step이 진행될수록 하나의 객체에게 많은 역할이 부여되는 것을 확인했고 이를 위해 클래스를 나누려고 했다. 팀원에게 TDD가 어떤 맛(?)인지 전파할 수 있는 좋은 기회였다. 

 

테스트 코드&설계: ✅  테스트 코드 작성이 어렵다면 구현 코드의 설계가 잘못일 수 있다.

수동 LottoTicket 생성할 때 Set<LottoNumber>을 넘겨줘야 했다. 이 문제는 테스트 코드 작성할 때 반복적으로 발생하는 문제였다. 사실 테스트 코드를 작성하며 LottoTicket.of(1,2,3,4,5,6) 의 메서드가 있었으면 좋겠다는 생각을 했지만 이 신호를 무시한 게 잘못이었다. 테스트 코드 작성에서 발생하는 설계와 리팩토링 냄새를 잊지 말아야겠다.

귀찮은 테스트 코드의 반복은 설계, 리팩토링 변경의 냄새다

테스트 코드: ✅  테스트 코드도 리팩토링 대상이다

테스트 코드의 가독성도 중요하다.

가독성이 떨어지는 테스트 코드

테스트 코드: ✅ DCI 패턴의 Unit Test

DCI 패턴의 Unit Test를 적용했다. JUnit5의 @Nested 계층형을 활용해 구현했다. 

테스트 코드에 대한 좋은 리뷰를 받고 뿌듯해졌다. 하지만 @Nested 를 사용한 계층형 DCI 패턴의 테스트 코드의 문제는 구현체 코드가 바뀐다면 테스트 코드 수정하기가 힘들었고 작성 시간이 상당했다. TDD에 처음부터 계층형 코드를 적용하긴 어려워 리뷰어 @Dion에 질의응답을 받을 수 있었다.

객체의 행동이 복잡해지거나 테스트 코드의 리팩토링이 필요할 때 적용해볼 수 있지 않을까 생각이 든다.

 

 

테스트 코드 : ✅ 랜덤 테스트 방법

테스트 코드 작성 중에 랜덤 기능에 대해 테스트하는 경우가 있었다. 먼저 랜덤 기능을 테스트하기 어려운 경우는 설계가 잘못된 신호일 수 있다. 예를 들어 한 클래스에 랜덤 기능이 있을 때, 해당 클래스에 Random() 객체가 멤버 변수로 있다면 강한 Coupling 상태이다. 이를 위해서 Random을 주입받아 사용하면 된다. 테스트하는 방법으로  Random 객체를 주입할 때 Mock 객체를 만들어 사용자 지정 랜덤 값을 전달해 줄 수 있다. 현재 로또 게임 프로젝트에서 랜덤 한 로또 복권 구입할 때 Random 객체를 주입하는 방식으로 구현하고자 했으나 팀 프로젝트가 종료되어 추후에 의존 관계 주입을 고민하고 구현하고자 한다.

 

참고 1 : Random 값을 처리하는 코드는 어떻게 단위 테스트를 할 수 있을까? - 박재성

참고 2 : 랜덤 요소가 들어간 로직의 테스트 코드는 어떻게 짜야할까요? - 스포크님의 질문과 박재성님 등 다른 분들의 답변


👍 Keep

- 커뮤니케이션에 문맥을 더하자

- 테스트 코드 작성을 멈추지 말자

🔥Problem

- 테스트 코드의 bad smell을 놓치지 말자

🚒 Try

- bad smell을 무시하지 말자


⛓ Reference

[일일 회고] 22.02.21~22 페어 프로그래밍(아이엠 그라운드 자기소개하기도 좋지만 그라운드 룰(Ground Rule)!) & 일급 컬렉션

[일일 회고] 22.02.23 - 여전히 Java 연습중

[일일 회고] 22.02.24 - 랜덤 테스트 / 스프링 시작 / 김영한님의 Q&A

IntelliJ + Gradle + Junit5 환경에서 테스트 실행 시 @DisplayName 이 테스트 결과에 나오지 않는 경우, 해결 방법

[주간 회고] 22.02. 4주차

 

댓글