[우테코 6기] 프리코스 3주 차 회고

Hanjmo·2023년 11월 17일
1
post-thumbnail

바쁜 일정 속에서 미루고 또 미뤘던 3주 차 회고를 프리코스가 종료된 지금 드디어 작성해보려고 한다..!

더 미루면 휘발성 메모리와 쏙 빼닮은 내 머리에서 아예 날아갈 것 같음

🎰 3주 차 미션

3주 차 미션은 로또로, "구입 금액만큼 랜덤 값으로 되어 있는 로또를 발행하고, 당첨 번호와 비교하여 당첨 여부를 결정"하는 내용이었다.

🎯 3주 차 목표 달성 여부

  • ✅ 2주 차 공통 피드백 반영하기
    • 살아있는 문서를 만들기 위해 노력하기
    • 테스트를 작성하는 이유에 대해 나의 경험을 토대로 정리하기
  • ✅ 정적 메서드를 사용하는 나만의 기준 세우기
  • ✅ '책임-주도 설계'를 통해 객체 설계하기
  • ✅ 컨트롤러에서 비즈니스 로직을 분리하기
  • ✅ enum 학습

📖 학습 및 고민한 내용

살아 있는 문서를 만들자

2주 차 공통 피드백에는 아래와 같은 내용이 담겨 있었다.

나는 기존에도 기능 목록을 구체적으로 잘 작성했다고 생각했지만, 아쉬운 점이 있다면 내 코드에 대한 설명이 부족하고 최신화가 잘 이루어지지 않았다는 것이다.

그래서 이번 기회에 코드 리뷰할 때, 다른 지원자분들의 코드 뿐만 아니라 README도 함께 유심히 살펴봤다.

많은 분들이 각자 다른 방식으로 프로젝트에 대한 설명을 잘 나타내고 계셨고, 나는 어떤 방식으로 내 프로젝트에 대해 설명할 수 있을까 고민한 끝에 협력 관계를 그리기로 생각했다.

여기서 협력 관계는 각 객체들이 서로 어떤 메시지를 주고받으면서 협력하는지를 의미한다.
마침 이번 주차에 책임-주도 설계를 경험해보고자 했는데, 이를 바탕으로 객체들의 협력 관계를 짧은 글과 그림으로 표현해봤다.

그리고 살아있는 문서를 만들기 위해 기능 목록과 클래스 이름, 메서드 이름 등 계속해서 변하는 것들을 놓치지 않으려고 노력했다.
이전에는 문서만 작업한 것을 커밋하면 낭비가 아닐까 우려했지만, 이는 내 착각이었다.
문서 작성도 정말 중요한 작업이기 때문에 이번에는 문서에서 변경되는 부분이 있을 때마다 주기적으로 업데이트하기 위해 노력했던 것 같다.

책임-주도 설계. 찍먹해볼까?

'객체지향의 사실과 오해'를 읽으면서 책임-주도 설계를 처음 알게 되었다. 지금까지는 "아 이런 것도 있구나"라고 생각만 할 뿐 시도해보지는 않았는데, 이번 주차에서는 이를 실제 적용해보고 싶었다.

책에서는 책임-주도 설계에 대해 간단하게 설명해주었기 때문에 구글링을 통해 개념을 보충해봤다.
이 과정에서 책임을 할당할 때 지침으로 삼을 수 있는 원칙들을 패턴 형식으로 정리한 GRASP 패턴을 알게 되었는데, 이에 대해서는 나중에 깊이 있게 알아보려고 한다.

책임-주도 설계에 대한 기본적인 개념을 어느 정도 학습한 후, 본격적으로 설계를 시작했다.

가장 먼저 할 일은 시스템이 사용자에게 제공해야 하는 기능인 시스템의 책임을 파악하는 것이었다. 그리고나서 이 시스템의 책임을 더 작은 책임으로 분리했다.

각각의 분할된 책임을 수행할 수 있는 객체가 무엇인지 고민해보면서 여러 객체에게 적절한 책임을 할당해주었다.

이러한 과정을 통해 여러 객체들이 서로 메시지를 주고받으면서 협력하는 관계를 설계할 수 있었다.

컨트롤러에서 비즈니스 로직 분리하기

MVC 패턴에서 Controller는 그저 model과 view를 연결해주는 다리 역할만 수행해야 한다.

하지만 기존에 작성한 내 코드를 보면, 컨트롤러에서 자동차 경주, 우승자 결정 등 비즈니스 로직까지 처리하고 있었다.

public class GameController {

    ...

    private void playRacing(Cars cars, NumberOfAttempts numberOfAttempts) {
        OutputView.printRacingResultMessage();
        while (numberOfAttempts.hasRemainingAttempts()) {
            MovingResult movingResult = cars.handleCarMovement(numberGenerator);
            OutputView.printMoveResult(movingResult.carNames(), movingResult.forwardCounts());
            numberOfAttempts.decreaseNumberOfAttempts();
        }
    }

    private void decideWinners(Cars cars) {
        Referee referee = new Referee();
        List<String> winners = referee.decideWinners(cars)
                .stream()
                .map(Car::getName)
                .toList();
        OutputView.printWinners(winners);
    }
}

이를 어떻게 분리할 수 있을까 생각하다가 Service 계층이 떠올랐다.
나는 Spring을 학습할 때 처음으로 MVC 패턴을 배웠으며, 이때 Service 계층에서 비즈니스 로직을 수행한다는 것을 알고 있었다.
하지만 그저 기계적으로 컨트롤러에서 온 요청을 서비스에서 처리해주는 것일 뿐, 그 이유에 대해서는 궁금해했던 적이 없다.

그러나 이제는 Service 계층이 어쩌다 생기게 되었고, 왜 필요한지를 명확하게 이해할 수 있게 되었다.

MVC 패턴과 같은 개념은 역시 처음부터 직접 작성해보고 단계를 높여봐야, 왜 사용하는지 등을 알 수 있게 되는 것 같다.

Enum.. 가깝게 다가가기

난 Java를 처음 접한 뒤로 Enum을 사용한 기억이 손에 꼽을 수 있을 정도로 적다.
사용한 것도 아래처럼 단순히 상수로서 사용하기만 했지, Enum에 대해 깊이 있게 학습한 적은 없다.

public enum NameOfTime {

    MORNING, LUNCH, DINNER
}

그런데 이번 주차 요구 사항을 보니 Enum을 사용하라는 내용이 있었고, 지금이 드디어 억지로라도 Enum과 친해질 기회라고 생각했다.

그렇게 구글링을 시작했고, 기가 막히게 잘 설명해주는 블로그를 참고하면서 Enum을 학습했다.

과거에는 자체 클래스의 인스턴스화를 이용해 상수처럼 사용했다는 사실부터 Enum의 탄생까지, 생각보다 신기한 지식들을 알 수 있었다. 이를 통해 상수를 정의한 클래스가 아닌 Enum을 사용해야 하는지 깨달았다.

Enum도 결국 참조 방식의 특수한 클래스라는 사실을 알았을 때, 멀게만 느껴졌던 Enum이 내 동생같은 느낌을 받았다.

Enum에 대해 공부하다가, 상태와 행위를 한 곳에서 관리할 수 있으며, 람다식까지 사용할 수 있음을 알고 이를 적용해보기로 했다.

로또 번호와 당첨 번호가 일치하는 개수, 보너스 번호의 여부에 따른 Rank를 가져오는 로직의 경우 기존에는 조건문을 사용했다. 하지만 이렇게 사용할 경우 아래처럼 아주 해괴망측한(?) 코드가 나오게 된다😨

public static Rank getRank(long matchingCount, boolean containsBonusNumber) {
    if (matchingCount == 6) {
        return FIRST;
    }
    if (matchingCount == 5 && containsBonusNumber) {
        return SECOND;
    }
    if (matchingCount == 5) {
        return THIRD;
    }
    if (matchingCount == 4) {
         return FOURTH;
    }
    if (matchingCount == 3) {
        return FIFTH;
    }
    return NONE;
}

상태와 행위를 한 곳에서 관리함으로써 얼른 이 지저분한 코드를 씻겨보자.

public enum Rank {

    FIRST(6, false, 2_000_000_000L, (match, hasBonusNumber) -> match == 6),
    SECOND(5, true, 30_000_000L, (match, hasBonusNumber) -> match == 5 && hasBonusNumber),
    THIRD(5, false, 1_500_000L, (match, hasBonusNumber) -> match == 5 && !hasBonusNumber),
    FOURTH(4, false, 50_000L,(match, hasBonusNumber) -> match == 4),
    FIFTH(3, false, 5_000L, (match, hasBonusNumber) -> match == 3),
    MISS(0, false, 0L, (match, hasBonusNumber) -> match <= 2);

    ...

    public boolean matchesCondition(long match, boolean hasBonusNumber) {
        return condition.apply(match, hasBonusNumber);
    }
    
    ...

위와 같이 나름 깔끔해진 것을 보면서 새삼 Enum의 위대함을 느꼈다.

테스트 코드는 왜 작성하지?

테스트 코드는 왜 작성하는걸까?

이 이유에 대해서 깊게 생각해본 적은 없고, 그저 내가 작성한 코드를 말 그대로 테스트할 목적으로 작성하는 것으로 알고 있었다.

그야 테스트 코드를 본격적으로 작성하기 시작한게 프리코스를 시작하면서부터니까...

이런 나에게 2주 차 공통 피드백에서 학습 테스트라는 개념을 알려주면서 테스트를 작성하는 이유에 대해 생각해볼 기회를 주었다.

그래서 이에 대한 이유를 오래 고민해봤고, 정리한 내용은 다음과 같다.

  • 내가 작성한 코드에 대한 피드백을 빠른 시간 내로 받을 수 있다.
  • 코드 리팩토링 후 테스트를 실행해보면서 어떤 부분을 놓쳤는지 빠르게 알 수 있다.
  • 기능을 구현하기 전에 테스트 코드를 작성해보면서 내가 사용할 API에 대한 세세한 내용을 확인할 수 있다.

정적 메서드는 언제 사용할까?

1주 차 코드 리뷰에서 도메인 객체가 정적 메서드를 사용하는 것을 지양하는 것이 좋다는 피드백을 받았다.

2주 차에 해당 내용을 알아봤지만, 확실한 결과를 얻지는 못해 아쉬움이 남았었다.

그래서 이번 주차에 정적 메서드를 지양해야 하는 이유를 찾아봤으며, 프리코스 커뮤니티에 있는 다른 지원자분들의 의견도 봤지만 역시 각자 생각하는 관점이 달랐다.

그래도 일관성 있게 사용하는 것이 좋겠다 생각하여 이에 대해 깊게 고민한 끝에 나만의 정적 메서드 사용 기준을 설정할 수 있었다.

아직 확실한 기준은 아니지만, 더 좋은 대안을 찾기 전까지는 이번에 설정한 기준에 따라 정적 메서드를 적절하게 사용할 계획이다.

🌙 끝내며

확실히 3주 차가 되니 미션이 꽤 어려워지는 듯 했다. 심지어 프리코스 외의 할 일이 많이 겹쳐서 정말 빡빡했다..

아 그리고 나는 내가 작성한 코드에서 스스로 문제점을 찾아내는 능력이 부족하다고 생각한다. 그래서 4주 차는 내 코드에서 나는 이상한 냄새를 잘 맡아 리팩토링하는 것이 목표였다.

프리코스가 마지막을 향해 달려가고 있는만큼, 끝까지 최선을 다해 최대한 많은 것을 얻어가려고 한다.

0개의 댓글