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

[우아한 테크코스 3기] LEVEL 1 회고 - 로또 1단계 미션의 3차 피드백을 받아보다 (22일차)

제이온 (Jayon) 2021. 2. 23.

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

 

오늘은 오전 중으로 수업이 있었고, 이후에는 로또 1단계 미션 3차 피드백 및 2단계 미션을 진행하였습니다.

 

 

데일리 미팅

오전 10시에 데일리 미팅이 진행되었습니다. 오늘 진행자는 저였고, 루터 회관에 가면 무엇을 하고 싶은지, 혹은 자신이 생각하는 루터 회관의 삶이 무엇인지 이야기하는 것을 주제로 선정하였습니다. 사실 이 주제는 어제 로키가 댓글로 달아준 것이었고, 주제가 무겁지도 않고 딱 적합한 것 같아서 그대로 주워먹었습니다 ㅎㅎ

 

다들 여러 가지 대답이 나왔지만, 1순위는 역시 크루들과 오프라인으로 만나서 친해지는 것이었습니다. 저는 포비가 말한 한강 뷰에서 코딩하는 것에 로망이 있어서, 이것을 하고 싶다고 말했더니... 워니가 한강은 보이지도 않는다고 하였습니다.

 

그 외에 롯데월드나 석촌호수로 놀러가고 싶다는 의견도 있었고, 맛있는 것을 시켜서 먹자는 의견도 있었습니다.

 

저는 단순히 마지막에 발표한 사람을 내일 데일리 미팅 진행자로 선정하지 않았고, 네이버 룰렛 돌리기를 통하여 진행자는 '아론'으로 결정되었습니다. 내일 어떠한 주제를 가져올지 기대가 되는군요?

 

 

Java Exception 수업

오전 10시35분부터 약 1시간 30분간 제이슨의 Java Exception 수업이 진행되었습니다. 전반적으로 예외 처리에 대한 이야기를 들었고, 흥미로웠던 부분 위주로 적어보겠습니다.

 

첫 번째는 try ~ catch 구문에서 '|'을 통해 동시에 예외를 처리해 줄 수 있는데, 가능하지 않는 경우가 있습니다. 만약, Exception을 상속받은 A가 있다고 가정해 봅시다. 그렇다면, catch(Exception | A e)와 같이 쓸 수 있을 것 같은데 컴파일 에러가 발생합니다. 왜냐하면 A를 상속한 Exception이 먼저 예외 처리되기 때문이죠.

 

두 번째는 CheckedException과 UncheckedException입니다. 먼저 둘의 특징을 알아보겠습니다.

 

 

 

 

이 둘의 차이를 한 크루가 예시를 통해 설명해 주었는데 참 이해가 잘 갔습니다. Checked Exception은 가스가 누출될 수 있으니까 단단히 막아달라는 의미이고, Unchecked Exception은 가스가 누출되면 단단하게 막아달라는 의미로 이해하면 됩니다. 참고로, Checked Exception은 메소드 옆에 시그니쳐로 throws (익셉션명)을 반드시 붙여주어야 합니다.

 

세 번째는 사용자 정의 예외 처리를 만드는 것이 좋은지, 아니면 기존의 있는 예외 처리를 사용하는 것이 좋은지에 대한 의견입니다. 저는 커스텀 예외 처리를 만들다보면 비슷한 예외가 불필요하게 늘어날 것이고, 이미 잘 만들어져있는 예외가 많기때문에 굳이 새로 예외를 만들 필요 없다고 생각하는 입장이었습니다. 이와 관련하여 다양한 크루들의 의견이 있었는데, 이 링크를 참조하시면 좋을 것 같습니다. 결론적으로는 정답이 없고, 본인이 상황에 맞게 잘 쓰면 됩니다.

 

마지막으로는 예외 처리는 아니고 Integer.valueOf()에 대한 이야기입니다. 우선 아래 코드를 봅시다.

 

 

    Integer a = Integer.valueOf(1);
    Integer b = Integer.valueOf(1);
    assertThat(a).isSameAs(b);

 

 

위 테스트는 통과할까요? 네 통과를 합니다.

 

 

    Integer a = Integer.valueOf(1000);
    Integer b = Integer.valueOf(1000);
    assertThat(a).isSameAs(b);

 

 

그렇다면 위 테스트도 통과할까요? 아니요, 통과하지 않습니다.

 

 

단지 숫자만 달라진 것인데 왜 이러한 결과가 나온 걸까요?

 

 

 

 

그것은 바로 Integer 내에서 -128에서 127까지 캐시를 해 두기 때문입니다. 수업 이후 빠르게 답변해 주신 루트는 그저.. 갓... 자세한 내용은 이곳을 참고하시면 되겠습니다.

 

 

3차 피드백

이번 3차 피드백을 마지막으로 로또 1단계 미션은 merge가 되었습니다. 2단계로 넘어가기 전에 데이브가 남겨준 피드백을 몇 가지 살펴보고 가겠습니다.

 

 

 

 

저는 처음에 Rating 클래스에서 열거형 상수의 필드로 상금과 일치하는 번호의 개수를 설정하였습니다. 하지만, 일치하는 번호의 개수와 보너스 볼 체크 유무를 동시에 갖고 있으면 getRating() 메소드의 코드 길이를 줄일 수 있다는 사실을 알게 되었습니다. 그래서 저는 일치하는 번호의 개수와 보너스 볼 체크 유무를 저장하는 RatingResult 클래스를 정의하였습니다.

 

 

public class RatingResult {

    private static final int SECOND_MATCH_COUNT = 5;
    private final int matchCount;
    private boolean hasBonusBall = false;

    public RatingResult(final int matchCount, final boolean hasBonusBall) {
        this.matchCount = matchCount;

        if (matchCount == SECOND_MATCH_COUNT) {
            this.hasBonusBall = hasBonusBall;
        }
    }

    public int getMatchCount() {
        return matchCount;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        RatingResult that = (RatingResult) o;
        return matchCount == that.matchCount && hasBonusBall == that.hasBonusBall;
    }

    @Override
    public int hashCode() {
        return Objects.hash(matchCount, hasBonusBall);
    }
}

 

 

사실 보너스 볼 체크 유무는 2등과 3등에만 필요한 것인데, 2등이 true일 때이므로 나머지는 모두 false로 고정해 두면 됩니다. 그리고 Rating 클래스에서 이 RatingResult 객체를 equals()로 비교할 것입니다.

 

 

    public static Rating getRating(final int matchCount, final boolean hasBonusBall) {
        RatingResult ratingResult = new RatingResult(matchCount, hasBonusBall);
        return Arrays.stream(values())
            .filter(rating -> rating.ratingResult.equals(ratingResult))
            .findAny()
            .orElse(MISS);
    }

 

 

이런 방식으로 일치하는 번호 개수와 보너스 볼 체크 유무를 가지고 한결 깔끔하게 당첨 순위를 얻어낼 수 있게 되었습니다.

 

 

 

 

이것은 숫자에 언더바를 붙임으로써 가독성을 높여줄 수 있습니다. 숫자에 '_'를 붙일 수 있다는 것이 참 신기하였습니다.

 

 

 

 

다음으로, 정적 팩토리 메소드 네이밍 컨벤션을 지키라는 것입니다.

 

 

 

 

저는 한 개의 매개변수를 사용하고 있으므로 from으로 메소드명을 변경하였습니다.

 

 

 

 

그리고 이것은 Lotto의 있는 상수를 public으로 만든 뒤, 그것을 가져와서 사용하라는 것이었습니다. 사실 외부 클래스의 상수를 public으로 만드는 것이 좋은 방법같지는 않은데, 일단 수정한 후 따로 데이브에게 여쭈어 보기로 하였습니다.

 

 

 

 

마지막으로 LottoService의 로직을 LottoController로 옮겨도 되는지 질문하였는데, 옮기는 것은 선택이지만 RatingInfo와 LottoMachine가 꼭 필드에 존재해야하는지 고민하라고 해 주셨습니다.

 

생각해보니까 대부분 인자로 넘겨받아도 무방하기때문에 LottoService의 필드를 싹 지우되, 로직은 남겨두기로 하였습니다. 아무래도 LottoController의 코드가 너무 길어질 것 같다는 우려가 있었습니다.

 

 

로또 2단계 미션

 

 

2단계 미션은 기존의 자동 구매 기능에서 수동 구매 기능을 추가하라는 것이었습니다. 구현 자체는 어렵지 않았습니다만, 구현해 놓고 나서 리팩토링을 하려고 하니까 문제점이 발견되었습니다.

 

 

    public void start() {
        final LottoService lottoService = new LottoService();
        final LottoRepository lottoRepository = new LottoRepository();

        final Ticket totalTicket = buyTicket();
        final Ticket manualTicket = manualBuyTicket(totalTicket);
        generateManualLottoNumbers(manualTicket.getCount(), lottoRepository);
        printBuyLottoResult(manualTicket.getCount(), totalTicket.getCount(), lottoService, lottoRepository);

        RatingInfo ratingInfo = lottoService.scratchLotto(lottoRepository, buyWinningLotto());
        printWinningStats(ratingInfo, lottoService, totalTicket);
    }

 

 

위 코드는 LottoController의 start() 메소드입니다. 구매금액에 따라 최대 티켓 개수를 담고 있는 totalTicket 객체를 만들고, 수동 구매를 한 manualTicket 객체를 만들었습니다. 그리고 이 두 개를 이용하여 printBuyLottoResult에서 수동 구매한 로또 번호와 자동 구매한 로또 번호를 출력하고 있습니다.

 

만약 더 이상 기능이 추가되지 않는다면, 이정도로만 해도 프로그램 돌아가는 데는 지장이 없을 것입니다. 하지만, 자동과 수동 구매 기능이 아니라 또다른 제 3의 기능이 생긴다면, 저는 Ticket 객체를 또 하나 만들고 비슷한 구조의 코드를 작성해야하는 문제가 발생합니다. 또한, getter 메소드를 통해 데이터를 직접 가져와서 비교하는 절차지향적인 코드를 작성하고 있으므로 유지 보수 측면에서 나쁜 코드입니다.

 

그런데 막상 '구매' 기능을 담당하는 클래스를 만들자니, 현재 코드에서 책임을 어떻게 분배할지 생각하기가 너무 막막하였습니다. 그래서 데이브에게는 죄송하지만, 제가 노력하였지만 잘 되지 않았던 부분을 상세하게 적어서 코드 리뷰를 신청하였습니다.

 

내일 데이브의 명쾌한 조언을 듣고 더 나은 코드로 변화할 수 있지 않을까 기대합니다.

 

 

정리

오늘은 알찬 수업을 듣고, 오후에는 열심히 미션을 수행하였습니다. 비록, 만족스러운 미션 코드를 작성한 것은 아니지만 저의 문제를 확실하게 인지한 상태이므로 성장할 수 있는 기회라고 생각합니다. 내일도 열심히 공부해서 오늘보다 더 성장한 개발자가 되면 좋겠습니다.

 

위 미션의 코드가 궁금하신 분은 이곳을 참고하시길 바랍니다.

댓글

추천 글