Coderoad

프리코스 3주 차를 돌아보며

2023-12-21 at 우아한테크코스 category

프리코스 3주 차 미션 - 로또 게임

다소 늦은 감이 없지 않아 있지만 프리코스 3주 차 미션의 회고를 작성해보고자 합니다. 11월과 12월, 기말고사 준비, 프로젝트 과제와 프리코스를 병행하려니 눈코 뜰 새 없이 지나간 것 같습니다.

3주 차 미션은 로또 게임을 구현하는 것이었습니다. 이번에는 '간단한'이라는 말은 없는 걸로 보아 확실히 1주나 2주차 과제보다 훨씬 까다롭겠다는 생각이 들었습니다. 특히, 프로그래밍 요구 사항에서 추가된 요구 사항은 조금 당황스러웠습니다. 절 당황시켰던 요구 사항은 바로, Java Enum을 적용한다.도메인 로직에 단위 테스트를 구현해야 한다. 라는 요구 사항이었습니다. 이 두 요구 사항을 적용하려면 제가 앞선 프리코스 미션을 수행하면서 자주 활용했던 문제 해결 전략을 수정해야 했습니다.

added_requirement
3주 차 미션이 막막해졌던 2개의 요구 사항들

열거형? 그냥 상수 모음 아닌가요?

추가된 요구 사항들을 준수하기 위해서는 먼저, 도메인 설계 과정부터 고쳐야 했습니다. 그동안 저는 Java의 Enum, 열거형을 단순히 코드에서 자주 사용되는 매직 넘버(Magic Number)나 문자열 리터럴(String Literal)값들을 상수로 정의해 하나의 클래스에서 관리하기 위한 수단으로만 생각했습니다. 그래서 열거형에는 단순히 특정 값을 대신하는 상수와 상수에 정의되어 있는 값을 얻기 위한 메소드만 두었습니다. 즉, 열거형에 무엇인가 도메인 로직을 둔다는 것은 미처 생각하지 못한 것입니다.

저는 매직 넘버(Magic Number)와 문자열 리터럴(String Literal)을 그대로 사용하는 것이 대표적인 안티 패턴이라고 생각합니다. 우아한테크코스에서도 숫자나 문자열 등의 값을 하드 코딩하지 말고 상수로 정의할 것을 권장하고 있습니다.

이번 미션 전까지 열거형에 대해 오해하고 있었기 때문에 저는 상수를 모아두기만 하는 열거형이 굳이 필요한가에 대한 의문이 들었습니다. 어차피 상수는 해당 상수를 활용하는 클래스에서 바로 정의할 수 있는데 따로 '열거형'의 형태로 정의할 필요를 느끼지 못했습니다.

필요성을 느끼지 못했으니 자연스럽게 다른 프리코스 미션이나 참여했던 여러 프로젝트에서 열거형을 잘 사용하지 않았고 효율적인 사용법도 당연히 알지 못했습니다. 그런 상황에서 좋든 실든 요구 사항을 준수하기 위해 열거형을 무조건 활용해야 했습니다. 그래서 저는 열거형에 대한 오해를 풀고자 처음부터 다시 열거형을 학습하기로 결정했습니다.

열거형의 진가를 찾아서

지금까지는 앞에서 계속 말했던 것처럼 열거형을 단순히 상수를 나열하는데 목적을 두고 도메인을 설계했습니다. 즉, 핵심 모델들에서 사용된 매직 넘버와 문자열 리터럴을 상수로 대체하기 위해서 열거형을 사용했습니다. 물론, Java에서 열거형이 서로 관련 깊은 여러 상수들의 집합인 것은 맞지만, 프리코스에서 요구하는 열거형의 사용이 단순히 상수 집합으로서의 사용이 아닐 것 같았습니다.

그래서 개발자들의 영원한 동반자, 구글링을 통해 왜 열거형을 사용해야 하는지, 어떻게 해야 잘 사용할 수 있는지 검색하던 중, 우아한 기술 블로그의 포스트를 찾게 되었습니다. 바로 Java Enum 활용기입니다. 이 포스트에는 무려 6년 전인 2017년에, 지금의 저와 같은 고민을 하셨던 선배 개발자께서 스스로 고민하고 학습하신 내용이 담겨있었습니다. 포스트를 모두 읽고 나서, 제가 정말 Java Enum의 진정한 장점을 단 하나도 제대로 활용하지 못하고 있구나라는 생각이 들었습니다.

6년 전부터 선배 개발자분들은 좋은 이정표를 제시해주셨지만 저는 어느 하나 제대로 알고 있지 못했습니다. 특히, 열거형은 잘 사용하지 않겠지라는 안일하고 위험한 생각을 가지고 제대로 학습하지 않았던 제 태도가 부끄러워졌습니다.

이 회고 포스트는 기술의 설명 보다는 제가 프리코스 미션을 해결하며 느꼈던 점을 위주로 작성하고자 합니다. 열거형을 사용해야 하는 자세한 이유는 위의 우아한 기술 블로그의 포스트를 직접 읽어보시는 것을 추천드립니다!

위 블로그 포스트에서 읽은 Java의 Enum은 완전한 기능을 갖춘 클래스라는 구절에 제 뒤통수가 얼얼했습니다. 열거형도 Java 클래스라면 당연히 상태(상수)를 가지고 있으며 이를 활용하는 여러 행동도 수행할 수 있겠다는 생각이 들었습니다. 저는 어쩌면 가장 기본 중의 기본인 객체지향 프로그래밍을 등한시하고 있었던 것 같습니다.

이때부터 저는 열거형도 하나의 도메인으로 생각하고 상수값들과 관련된 도메인 로직을 열거형 내부에 구현하기 시작했습니다. 예를들어, 이번 미션인 로또 게임에서는 정수형 당첨금 정보를 상수로 정의한 Prize 열거형에서 일치한 번호 개수와 2등 여부를 전달받아 그에 맞는 당첨금을 반환하는 도메인 로직을 구현했습니다.

public static int getPrize(int winCount, boolean isSecond) {
    return Arrays.stream(values())
            .filter(prize -> prize.winCount == winCount)
            .filter(prize -> prize.isSecond == isSecond)
            .findAny()
            .orElse(NONE)
            .getPrize();
}

그렇게 깔끔한 코드라고는 할 수 없지만 저의 첫 열거형 내부의 도메인 로직 코드입니다. Prize 열거형 객체는 로또 당첨금에 대한 정보를 저장하고 있기 때문에, 당첨 등수에 따른 당첨금 정보를 다른 객체에 제공해야 하는 책임이 있습니다. 따라서, 다른 객체와의 협력을 통해 로또 당첨금 정보를 제공하는 역할을 가지도록 설계했습니다. 드디어 역할, 책임, 협력 관계가 적절하게 갖춰진, 제가 공부한 객체지향 프로그래밍에 알맞은 도메인 설계가 이뤄졌습니다.

Java Enum을 적용한다. 라는 추가 요구 사항 덕분에 Java 도메인 설계에서 더 유연한 사고가 가능해진 것 같습니다. 개인적으로 3주 차 미션에서 가장 크게 얻어가는 점입니다.

단위 테스트는 또 무엇인가...

열거형을 어떻게 사용해야 할지 감이 잡히면서 추가된 요구 사항 중 하나는 그래도 잘 지켜가며 개발할 수 있었습니다. 이대로만 하면 무난하게 미션을 완수하고 제출할 수 있을 것 같았습니다. 그러나, 도메인 로직에 단위 테스트를 구현해야 한다. 라는 하나 남은 요구 사항은 제 생각보다 더 복잡했습니다.

요구 사항을 준수하기 위해 가장 먼저, 단위 테스트에 대해서 조금 더 학습했습니다. 다음은 AWS의 문서 유닛 테스트란?에서 발췌한 단위 테스트에 대한 설명입니다.

단위 테스트는 코드의 가장 작은 기능적 단위를 테스트하는 프로세스입니다.

즉, 단위 테스트는 각 객체가 제공하는 최소 단위의 기능을 테스트하는 일련의 과정입니다. 예를 들어, 두 정수를 더하는 기능(메소드)가 있다면, 두 양의 정수, 두 음의 정수, 양과 음의 정수끼리 더하는 모든 경우를 테스트하는 것이 단위 테스트입니다. 다음은 앞서 설명한 단위 테스트의 예제 코드입니다.

@Test
void 두_양의_정수를_더한다() {
    // given
    int a = 1;
    int b = 2;

    // when
    int result = Calculator.sum(a, b);

    // then
    assertThat(result).isEqualTo(3);
}

@Test
void 두_음의_정수를_더한다() {
    // given
    int a = -1;
    int b = -2;

    // when
    int result = Calculator.sum(a, b);

    // then
    assertThat(result).isEqualTo(-3);
}

@Test
void 양의_정수와_음의_정수를_더한다() {
    // given
    int a = 1;
    int b = -2;

    // when
    int result = Calculator.sum(a, b);

    // then
    assertThat(result).isEqualTo(-1);
}

단위 테스트가 어떤 것인지는 대략적으로 파악했고, 테스트 코드도 앞선 미션에서 작성해봤기 때문에 쉽게 작성할 수 있을 줄 알았습니다. 그러나 이번에 주어진 요구 사항은 조금 달랐습니다. 일단, UI 로직에서 사용한 System.in, System.out과 같은 입출력 클래스는 테스트에서 활용할 수 없었습니다. 2주 차 미션에서는 사용자 입력에 따라 동작하는 로직에 대한 검증을 System.in을 통해 직접 입력값을 설정해주는 방식으로 테스트를 작성했습니다.

@Test
void 사용자는_쉼표를_기준으로_구분되는_자동차_이름들을_입력한다() {
    // given
    System.setIn(createInputStream("pobi,woni,jun"));

    // when, then
    assertThat(raceView.inputNames()).isEqualTo("pobi,woni,jun");
}

그런데, 이번 미션에서는 이 방식이 금지되어버린 것입니다. 지금까지 구현해 온 도메인 로직에서 제공하는 기능은 단위 테스트 작성이 불가능했습니다. 하지만 사용자 입력에 대한 검증도 필수이기 때문에 단위 테스트를 작성하지 않을 수도 없었습니다. 그래서 저는 지금까지 작성한 핵심 도메인들의 코드를 몽땅 뜯어고치기로 결정했습니다.

2주 차 미션에서는 View 계층에서부터 입력값에 대한 검증을 처리했습니다. 저는 UI를 담당하는 객체에서 입력값을 검증하면 프로그램 전반에서 자동으로 검증된다고 생각했습니다. 그런데, 막상 핵심 도메인 로직에서 잘못된 값을 통한 예외 발생 단위 테스트를 작성하려고보니 예외를 발생시키고 이를 검증할 방법이 없었습니다. 실제로 자동차 경주 테스트 코드를 보면, assertThatThrownBy().isInstanceOf();과 같이 예외 발생을 테스트하는 코드가 없습니다.

이 문제를 해결하기 위해 저는 핵심 도메인을 더 세분화하고, 입력값에 대한 검증을 UI가 아니라 해당 입력값을 사용하는 도메인에 위임했습니다. 해당 리팩토링의 커밋 내역입니다. 이렇게 코드를 리팩토링하고나니 단위 테스트를 작성하는 것이 훨씬 수월해졌습니다. UI는 오로지 입출력만 담당하게 되면서 요구 사항대로 UI에 대한 테스트도 작성하지 않을 수 있게 됐고, 각 핵심 도메인 객체들이 스스로를 검증하도록 하는 더 객체지향다운 설계를 할 수 있게 됐습니다.

더 나은 방법은 무조건 있다!

이번 미션을 수행하면서 가장 절실하게 느낀 점은 '내가 생각한 것보다 더 나은 방법은 무조건 있다.' 입니다. 제출하신 코치님들께서는 간단하다고 주장하시는 프리코스 미션마저 여러 시행착오를 겪는 것을 보면 제 실력은 아직 주니어 개발자 근처도 가지 못한 햇병아리입니다. 그렇기 때문에 저는 제가 설계한 도메인 로직들은 크던 작던 분명히 개선할 점이 존재한다고 생각합니다. 더 나은 방법을 찾고, 이를 적용하는 과정이 저를 더 나은 개발자로 만들어줄 것이라고 믿습니다.

실제로 이번 미션에서 더 나은 Java Enum 사용법, 단위 테스트 작성을 위한 도메인 설계 개선점 등을 배울 수 있었고, 제 코드를 개선하는 경험을 했습니다. 더 나은 방법은 무조건 있습니다. 이를 찾아내고 내 것으로 만들면 성장하지 않을 수 없을 것입니다. 결국 우아한테크코스에서 강조하는 몰입의 경험이 이렇게 내가 쓴 코드를 개선하기 위해 그 너머를 바라보는 것을 말하는게 아닐까하는 생각도 들었습니다. 프로그래밍에 몰입하지 않고서는 한번 작성한 코드를 다시 읽기란 쉽지 않은 일이기 때문이니까요. :)

Repository

로또 게임 - hangillee

hangillee

Personal blog by hangillee.

Road to good developer.