프리코스 2-3주차 회고

개발새발log·2022년 11월 16일
0

프리코스

목록 보기
1/3

2주차 요약과 느낀 점

2주차 미션 과정을 요약하자면: 고민만 며칠 하다가 본격적인 구현은 부랴부랴 얼레벌레 했다.

커뮤니티를 보니까 테스트코드를 열심히 짠 사람들도 많이 보이고, 몇몇 풀리퀘에서는 생각지 못한 MVC 패턴으로 구현한 사람들도 많이 보여서, 솔직히 비교되고 우울감이 들었다. 2주차 미션을 처음 시작할 땐, 객체지향적으로 잘 짜인 코드를 짜고 싶다는 꿈에 부풀어 있었기에 그만큼 실망감도 컸다.

2주차 코수타에서 포비님께서 OOP, TDD고 나발이고 기본적인 요구사항을 잘 충족 시키라는 말씀이 실질적인 도움이 됐다😂 왜냐하면 2주차 때 가장 크게 느낀 건 꿈만 크고, 생각만 많아서는 죽도 밥도 안된다는 것이기 때문이다.

물론 그 모든 과정이 헛되다는 건 아니다. 머리를 쥐어 짜내며 고민하는 그 2-3일 간, 상태 전환에 따른 아키텍처를 고민했다. 사용할 수 없다는 결론을 내리기까지, State Design Pattern과 enum을 학습해보며 고민했다.


(고민 과정의 흔적)

2주차의 가장 큰 수확은 테스트코드 작성의 패러다임이 180도 전환됐다는 걸 꼽을 수 있다.

이전에는 테스트코드를 내가 구현한 코드의 보조수단 정도로만 생각했다. 그저 로직이 잘 돌아가는 검증하기 위한 도구 정도? 이전에 프로젝트 서비스 코드의 테스트를 작성하다가 너무 까다로워서 때려친 기억도 있어서, '테스트코드는 어렵다'는 막연한 두려움도 안고 있었다.

그런데 2주차 미션을 구현하는 일련의 과정을 겪으며 문득, '애초에 테스트하기 용이한, 결합도가 낮은 구현 코드를 작성해야 테스트 코드를 작성할 수 있는 게 아닐까..?' 라는 무서운 생각이 들었다 (이 생각이 갑자기 왜 들었는지는 자세히 생각이 안 난다.. 긁적)

https://jojoldu.tistory.com/674
그리고 위 글을 읽으며 테스트코드의 패러다임이 완전히 전환됐다. (강추!!✨)

"테스트를 위해 구현 설계가 변경될 수 있다.
테스트 코드는 구현의 보조적인 수단이 아니며, 같은 레벨로 봐야한다.
좋은 디자인으로 구현된 코드는 대부분 테스트 하기가 쉽다.
테스트 하기 어렵게 구현 되었다면, 코드 확장성 / 의존성 등 코드 디자인, 설계가 잘못되었을 확률이 굉장히 높다."

전에 프로젝트 코드의 테스트 코드를 작성하다가 너무 복잡하고 어려워서 때려친 기억을 되살려보자면.. 그건 사실 그 코드의 설계가 다소 거지같아서 어려웠던 게 아닐까 싶다💁‍♀️

테스트코드를 짜는 기술이 부족한 게 아니라, 기존 코드의 설계가 부실할 확률이 더 높다.

그래서 3주차의 마음가짐과 목표는?

완벽에 대한 내 집착과 강박을 인정하고, 내려놓자. 그리고 주어진 요구사항이나 제대로 충족하자!

  • 메소드, 클래스 분리
  • 단위 테스트 수행
  • 기능별 커밋

크게 위에 3가지를 가장 염두에 두고 미션에 임했다.

처음 두가지 목표는 2주차 때 테스트코드에 대한 패러다임이 전환되면서 중요성을 실감했다.

마지막 기능별 커밋은, 1주차 때부터 요구사항에 포함되어 있었다. 그러나 2주차 때 부랴부랴 구현하면서 느낀 건, 간단해 보이는 기능별 커밋이 사실 어렵다는 것이었다. 본래 나는 커밋은 "실행 가능한 단위"라고 생각했기 때문에, 큰 단위의 기능을 우다다다 짜는 개발 습관을 가졌다. 2주차 숫자야구 미션을 하면서도 커밋의 단위를 대체 어디서 끊어야 하지..?하는 혼란이 있었기 때문에, 3주차 미션에서는 기능을 확실히 나누고, 조급해하지 말고 차근차근 구현하자고 다짐했다. 돌이켜보면 내가 가진 악습을 끊어내는 과정이였던 것 같다.

3주차의 과정

3주차 미션을 수행하면서 개인적으로 신기했던 점은, 정말 "어쩌다가" MVC 패턴으로 구현하고, 도메인 중심적으로 개발했다는 것이다. 테스트코드를 짤 때, 역할과 책임이 분리되지 않고 복잡하게 얽혀있는 경우에는 짜기가 힘들어서 분리하다보니 자연스럽게 클래스가 분리되는 경험을 할 수 있었다.

Controller와 Service를 분리하기까지

  • 처음 사용자로부터 로또 가격을 입력 받아서 저장하기까지의 코드

  • 초기 구조: Controller(주요 로직), Validator(입력 검증), UserLottoInfo(입력 받은 로또 가격 저장), LottoConsole(입출력 메소드 있음), ConsoleMessage(콘솔 메세지 enum 클래스)

  • 초기 Controller

public class LottoController {
    private final LottoConsole lottoConsole;

    private UserLottoInfo userLottoInfo;

    public LottoController() {
        lottoConsole = new LottoConsole();
        userLottoInfo = new UserLottoInfo();
    }

    public void executeGame(){
        // 입력 받아서 검증 로직
        String lottoPrice = lottoConsole.inputLottoPrice();
        validateLottoPrice(lottoPrice);

        // userLottoInfo에 저장
        userLottoInfo.setLottoPrice(Integer.parseInt(lottoPrice));
    }
}

사용자로부터 입력을 받아서 잘 저장됐는지 확인하는 로직을 테스트 해보자고 마음먹고, 로직을 작성하려는데.. '어라? 이걸 어떻게 작성하지' 싶었다. 문득 2주차 때 https://jojoldu.tistory.com/674 시리즈를 읽으며, "외부에서 주입받는 형식으로 바꾸면, 테스트할 때 독립성을 가져갈 수 있다"는 게 기억나서, 그러면 controller에서 따로 다른 처리 로직을 담당할 클래스를 호출해서 파라메터로 넘기는 형식으로 갈까..? 라고 사고 흐름이 자연스레 이어졌다. 그렇게 처음 Service 클래스가 등장했고, executeGame의 코드는 아래와 같이 바뀌었다.

String lottoPrice = lottoConsole.inputLottoPrice();
lottoService.storeLottoPrice(lottoPrice);

이렇게 바뀌니 Service의 테스트코드를 작성하기 한결 편했다.
input 변수를 하나 선언해서 storeLottoPrice 메소드에 주입하면 끝이기 때문이다. 테스트하기 어려운 코드는 무언가가 강하게 결합되어 있는 구조로, 모듈의 독립성이 보장되지 않는다는 생각으로 분리했는데, 그러다보니 최상단에서 명령하는 Controller와, 핵심 로직을 담당할 Service으로 분리하게 됐다.

핵심 비즈니스와 도메인의 분리

일급 컬렉션을 처음 접하고 적용해 보는 과정에 자연스럽게 도메인 중심적으로 개발하기 시작했다.

참고) https://jojoldu.tistory.com/412
ㄴ그러고보니 동욱님의 블로그가 자주 등장하네요👀 (늘 감사합니다 굽신굽신)

기본적으로 주어진 Lotto 코드를 보면, 이렇게 주어져있다.

private final List<Integer> numbers;

public Lotto(List<Integer> numbers) {
        validate(numbers);
        this.numbers = numbers;
}

private void validate(List<Integer> numbers) {
    if (numbers.size() != 6) {
        throw new IllegalArgumentException();
}

이걸 보니 일급 콜렉션을 적용할 것은 유도한 거 같다는 생각이 들었다. 기존에는 입력 확인용 클래스 Validator를 따로 둬서, service에서 검증하는 식이였는데 도메인 단에 validation을 하도록 관련 메소드를 다 몰았다. 해당 도메인에서 중요한 필드도 다 몰아넣을 수 있어서 훨씬 깔끔해졌다.

Lotto의 경우,

private static final int LOTTO_NUMBERS_SIZE = 6;
private static final int START_NUMBER = 1;
private static final int END_NUMBER = 45;

validation에 필요한 중요한 상수들을 미리 선언해둬서 validation 로직에 활용했다.

또한 이렇게 하면 validation을 통과하고 나서 생성자에서 필드를 세팅하기 때문에, 불변으로 필드를 설정할 수 있다는 장점이 있다. 꼭 필요한 경우가 아니라면, 도메인에 setter를 따로 안 둬서 변경될 수 없게끔 했다.

enum 활용기

결과를 발표하는 부분을 구현하는데 명백히 떠오르는 해결 방식이 없어서 쉽지 않았다. 확실한 건, 1 ~ 5등은 enum을 활용해야 할 것 같았다.

3주차 미션에 대놓고 enum을 활용하라는 요구사항이 있었다. 바로 위에는 switch나 else를 사용하지 말라는 요구사항이 었었다. 결과 발표하는 로직을 작성한다고 할 때, 당장 떠오르는 가장 직관적인 방식은 switch나 if ~ else였지만 다른 방식으로 풀어내자고 다짐했다.

1등 ~ 5등을 enum으로 관리한다는 건 확실한데, 여기서 중요한 건 어떤 정보들과 일대일로 매칭되는가를 판단하는 것 같다. 정리해보니 매칭되는 번호 개수, 수익금, 그리고 출력 메세지가 모두 등수와 일대일로 매칭된다는 걸 확인하고, 다음과 같은 LottoMatch enum을 만들었다.

SIX_MATCHES(MatchCount.SIX_MATCH, MatchProfit.SIX_MATCH_PROFIT, MatchMessage.SIX_MATCHES_MSG),
FIVE_MATCHES_PLUS_BONUS(MatchCount.FIVE_MATCH_PLUS_BONUS, MatchProfit.FIVE_MATCH_PLUS_BONUS_PROFIT,
            MatchMessage.FIVE_MATCHES_PLUS_BONUS_MSG),
FIVE_MATCHES(MatchCount.FIVE_MATCH, MatchProfit.FIVE_MATCH_PROFIT, MatchMessage.FIVE_MATCHES_MSG),
FOUR_MATCHES(MatchCount.FOUR_MATCH, MatchProfit.FOUR_MATCH_PROFIT, MatchMessage.FOUR_MATCHES_MSG),
THREE_MATCHES(MatchCount.THREE_MATCH, MatchProfit.THREE_MATCH_PROFIT, MatchMessage.THREE_MATCHES_MSG),
NULL_RESULT(-1, -1, null);

아마 이번 미션을 하면서 다른 분들도 많이 접했을 것 같은데, 역시 이동욱 님의 글에서 힌트를 얻었다😃

참고) https://techblog.woowahan.com/2527/

사용자 로또를 돌면서 각각의 매칭 개수, 보너스 번호 유무를 저장해둔 CalculatedLotto라는 클래스가 있는데, CalculatedLotto의 결과와 매칭되는 enum을 찾기 위해 enum의 매칭되는 수를 순회하면서 찾는 방식으로 구현했다.

하지만 CalculatedLotto로 등수를 확인하는 건 개별적인 결과들이고, 이를 모두 모아서 처리하기 위해서 어떻게 해야할까 싶었다. 방법을 찾다가, enum map이라는 걸 알게 되서 key로 LottoMatch enum을 가지고 value로 개수값 Integer를 가지는 EnumMap을 만들어서, 연산 결과를 취합했다.

public void getTotalResult() {
    EnumMap<LottoMatch, Integer> statistics = initLottoMatchMap();
    long profit = 0;

    for (CalculatedLotto lotto : calculatedLottos) {
        profit = getProfit(statistics, profit, lotto);
    }

	printTotalResult(statistics, calculateProfitRate(profit));
}

이외에 신경 쓴 부분은?

  • 하드코딩 X, 상수 필드로 관리
  • validation 신경 썼다 ("x,x,x,x,x,x"는 엄격한 입력형식이였기에, regex로 처리했다)
  • 메소드는 가능한 쪼갰다
  • 주요 도메인 테스트 완료

여전히 고민되는 부분?

  • 핵심 비즈니스 로직과 UI 로직의 분리.. 잘..된걸까..?

3주차 느낀 점

앞으로도 테스트코드 열심히 짜야겠다 싶다🥹 이번에는 사실 처음부터 객체지향적으로 완벽한 설계의 코드를 짜고 들어간 게 아닌데도 불구하고, 테스트코드 작성하기 용이하도록 기존 코드의 설계를 개선시키면서 많은 부분이 분리됐다. 알고보니 더 나은 설계로 나아가는 ✨빛✨이였다.

본래 객체지향적인 디자인도 처음부터 완성된 형태로 나온다고 생각했다. 그래서 완!벽!한 설계도가 있어야지만 가능하다고 막연하게 생각했는데 그게 아니라는 걸 이번 기회에 확실히 알게 됐다. 처음부터 완벽한 코드를 짠다는 건 말도 안되는 환상이였구나 싶다.

기능별로 커밋하려고 노력하면서, 전처럼 한번에 큰 단위의 코드가 나오는 게 아니다보니, 작은 기능별로 부분적으로 검증해가면서 코딩할 수 있다는 장점도 느꼈다. 아마 우테코가 아니였으면 기존에 하던대로 큰 단위로 코딩하는 게 안 좋은 습관이라는 것도 모른 채 코딩했을 거 같다.

막연하게 가지고 있던 테스트코드에 대한 두려움도 많이 사라졌다. 단위 테스트를 작성하면서, 작성법 자체는 어렵지 않다는 걸 느꼈다. 만약 작성하기 곤란하다면 내 코드부터 의심해보자!😃

전에는 코드 리팩토링을 모르고 살았다가, 이제 첫걸음을 뗐다는 기분 좋은 느낌이 든다. 앞으로도 계속 고민해보고 개선시켜나가고 싶다

✅ 소스코드는 아래에!

https://github.com/dldbdud314/java-lotto/tree/dldbdud314

profile
⚠️ 주인장의 머릿속을 닮아 두서 없음 주의 ⚠️

1개의 댓글

comment-user-thumbnail
2022년 11월 16일

잘보고갑니다!

답글 달기