각종 후기/우아한테크코스

[우아한 테크코스 3기] LEVEL 1 회고 (16일차)

제이온 (J.ON) 2021. 2. 17.

안녕하세요? 제이온입니다.

 

이틀 연속 밤 10시까지 빡코딩하느라 꽤 지치는군요.. 그래도 내일 미션을 마감하고 나면 저녁에는 한 숨 돌릴 수 있을 듯 합니다.

 

 

데일리 미팅

오전 10시에 로키가 데일리 미팅을 진행하였는데, 말하기 주제는 없었고 '라이어 게임'을 통해 내일 데일리 미팅을 진행할 사람을 정하기로 하였습니다. 저는 라이어 게임을 며칠 전에 술자리에서 친구들이랑 한 경험이 있어서, 게임 규칙은 쉽게 익힐 수 있었습니다.

 

첫 판 라이어는 워니, 두 번째 판 라이어는 우기가 걸렸는데, 두 판 모두 시민 팀이 라이어를 찾아냈습니다. 힘든 개발 전에 이런 힐링 타임이 있어서 힘차게 오전을 시작할 수 있었던 것 같습니다 ㅎㅎ 준비 열심히 한 로키 고마워요~

 

 

페어 프로그래밍

어제 처음부터 당첨 결과를 도출하는 로직을 어느 정도 구현해놔서 오늘은 전 날보다는 덜 빡세지않을까 생각을 하였습니다. 하지만.. 예상은 언제나 빗나가는 법이죠. 특히, enum을 짜는 부분에서 온갖 고생을 하였습니다.

 

 

 

 

이 부분에서 "%개 일치 (%원) - %개' 부분을 enum으로 표현하면 좋겠다고 의견 합의를 보았습니다. 그런데, 한 가지 문제가 있었죠. 바로, 로또 2등을 도출할 때는 보너스 볼을 고려해야한다는 것입니다. 마찬가지로 3등은 보너스 볼이 일치하지 않아야합니다.

 

따라서 위 내용을 만족하려면 2등의 경우 (5, true)와 같이 맞춘 개수와 보너스 볼 일치 여부를 동시에 저장해야합니다. 반대로 3등의 경우 (5, false)인 것이죠. 하지만, 이 방법에는 문제가 하나 있습니다. 만약, 1등의 경우 (6, true)와 (6, false)를 동시에 저장해야합니다. 4등과 5등도 보너스볼 일치 여부 2가지 경우의 수를 갖고 있어야 합니다.

 

그래서 맞춘 개수와 보너스 볼 여부를 저장하는 WinningResult를 생성하고, 그 안에 equals()와 hashCode()를 정의해서 비교하는 것은 어떨까 생각이 들었습니다. 하지만, 이 방법도 문제가 있는 것이 enum 객체를 초기화하는 코드가 쓸데 없이 길어집니다.

 

 

FIRST(2000000000, Arrays.asList(new WinningResult(6, true), new WinningResult(6, false))

 

 

그리고 여기다가 해당 등수가 몇 번이나 당첨되었는지까지도 저장한다고 가정하면 뒤에 0도 추가로 붙어야합니다.

 

 

이러한 고민을 완태와 함께 점심 먹기 전까지 고민하였고, 명쾌한 해답이 나오지는 않아서 밥을 먹고 오기로 결정하였습니다. 저는 밥을 먹으면서 굳이 2등때문에 다른 객체가 보너스 볼 여부를 판단해야하는건 불필요하다가 생각이 들었습니다. 약간의 하드 코딩을 하더라도 전체적으로 코드를 줄이는 것이 이득이기때문이죠.

 

그래서 최종적으로 enum은 아래와 같이 구현하였습니다.

 

 

package lotto.domain.rating;

import java.util.Arrays;
import java.util.NoSuchElementException;

public enum Rating {
    FIRST(6, 2000000000),
    SECOND(5, 30000000),
    THIRD(5, 1500000),
    FOURTH(4, 50000),
    FIFTH(3, 5000),
    MISS(0, 0);

    private int matchCount;
    private int reward;

    Rating(final int matchCount, final int reward) {
        this.matchCount = matchCount;
        this.reward = reward;
    }

    public static Rating getRating(final int matchCount, final boolean containBonusBall) {
        if (matchCount == THIRD.matchCount && !containBonusBall) {
            return THIRD;
        }

        if (matchCount < FIFTH.matchCount) {
            return MISS;
        }

        return Arrays.stream(values()).filter(rating -> rating.matchCount == matchCount).findAny()
            .orElseThrow(NoSuchElementException::new);
    }

    public int getMatchCount() {
        return matchCount;
    }

    public int getReward() {
        return reward;
    }

}

 

 

여기서 주목해야할 부분은 getRating()입니다. TWO.matchCount나 THREE.matchCount는 둘다 5인데, containBonusBall 여부에 따라 2등인지 3등인지 갈리게 됩니다. 첫 번째 조건문을 통해서 3등을 걸러내면, 나머지 조건은 보너스 볼 여부와는 관계가 없어집니다. 물론, 5등 미만은 의미가 없으므로 바로 MISS로 걸러낸 후, FIRST부터 MISS 외의 값이 도출되는 것을 방지하기 위하여 예외 처리도 하였습니다.

 

 

그렇다면, 각각의 등수가 몇 번 당첨되었는지 어떻게 세야 할까요? 그것은 따로 RatingInfo라는 클래스에서 EnumMap을 두어 관리하기로 하였습니다.

 

 

package lotto.domain.rating;

import java.util.EnumMap;
import java.util.Map;

public class RatingInfo {
    private Map<Rating, Integer> ratings;

    public RatingInfo() {
        ratings = new EnumMap<>(Rating.class);
        for (Rating rating : Rating.values()) {
            ratings.put(rating, 0);
        }
    }

    public void update(final Rating rating) {
        ratings.put(rating, ratings.get(rating) + 1);
    }

    public int get(final Rating rating) {
        return ratings.get(rating);
    }
}

 

 

이렇게 멤버 변수로 EnumMap을 정의하고, update()와 get()으로 map을 관리하였습니다. 물론, enum에서 등수를 세도록 할 수 있지만, 상수 처리 관련 클래스인 enum에서 너무 많은 일을 하는 것 같아서 따로 로직을 분리하기로 결정하였습니다. 물론.. 현재 이 클래스에서 ratings는 불변이 아니므로 여러 모로 불안한 감은 있어서 내일 리팩토링을 해 봐야겠습니다.

 

 

위 로직을 구현하는 데 굉장히 힘들었고, 완태가 많이 고생해 주었습니다. 다행히 서로 의견이 맞아서 이 문제를 잘 해결하였고, 이후는 구현하는 데 그렇게 어렵지는 않았습니다. 요구 사항인 원시 타입 포장과 일급 컬렉션을 모두 지켜서 프로그램을 동작하게 하였습니다.

 

그리고 예외 처리와 main에 Control 역할을 맡기던 중 완태에게 좋은 방법을 배웠습니다. 먼저, InputView에서 예외 처리를 할 때, 저는 Money라는 객체 내에서 validate()를 만들어서 throw를 하고 InputView 내에서 try ~ catch 내에 new Money()로 구현하는 편이었습니다.

 

 

    public static Money getMoney() {
        try {
            String input = scanner.nextLine().trim();
            return new Money(Integer.parseInt(input));
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException(INPUT_INTEGER_ERROR_MESSAGE);
        }
    }

 

 

위와 같이 try ~ catch 내에 객체를 생상하여 반환하면, Money 생성자 내에서 예외도 한 번에 처리할 수 있으므로 좋은 방법이라고 생각해 왔습니다. 하지만, 완태는 클라이언트 측에서 이 모델의 정보를 직접적으로 알면 안 된다고 알려주었고 예외 처리를 메인 메소드로 옮겼습니다.

 

 

다음으로, 메인 메소드의 역할을 Controller로 위임하는 것을 배웠습니다. 처음에는 위에서 말한 예외 처리와 더불어 전반적인 프로그램 동작을 모두 메인 메소드에서 진행하였는데, 메인 메소드치고 코드가 너무 길어지는 것 같아서 LottoController에 로직을 분리시켰습니다. 이렇게 함으로써 메인 메소드는 한결 단순한 코드가 되었죠.

 

 

정리

포비는 어제 크루들에게 '돌아가는 쓰레기 코드가 낫다'라는 말씀을 해 주셨습니다. 현업에서는 프로젝트 마감 기한이 존재하는데, 아무리 코드가 깔끔하더라도 프로그램이 동작하지 않으면 의미가 없다는 것이죠.

 

그런 의미에서 이번 미션부터는 3일도 안 되는 미션 기한이 주어졌고, 코드의 구조가 다소 마음에 들지 않더라도 기능을 최우선적으로 구현하려고 노력하였습니다. 그리고 이 마음에 안 들어서 필수적으로 리팩토링할 부분만 to-do 리스트에 기록해 놓고, 선 테스트 후 프로덕션 코드 기반을 개발하다보니 어느샌가 차근 차근 주어진 프로그램을 완성하는 제 모습을 볼 수 있었습니다.

 

물론, 내일 리팩토링을 또 해 봐야겠지만, 오늘 주어진 요구 사항을 지켜서 개발을 완료한 것에 만족합니다. 그 외에, 내일 테코톡 발표가 있는데 서둘러서 대본 정리하고 발표 연습을 좀 해야겠습니다..

추천 글