본문 바로가기

회고

우아한테크코스 2주차 프리코스 회고록(레이싱 게임)

728x90

개요

2주 차 미션에서도 1주 차와 마찬가지로 객체 지향적 설계에 대한 고민이 가장 많았다.

또한 상호 코드 리뷰를 통해 dto와 정적 팩토리 메서드에 대해 좀 더 깊게 고민해 볼 수 있었다.

 

추가로 1주 차 피드백 강의를 통해 테스트 코드에서 어노테이션을 이용해 다양한 값을 한 번에 전달할 수 있는 방법을 알게 되었다.

아래는 2주 차 미션을 통해 내가 고민하고, 배운 것들이다.

구현

위 다이어그램은 Intellij에서 제공하는 각 클래스의 의존성을 보여주는 다이어그램이다.

역할

주요 객체에 대한 역할 아래와 같다.

Controller

RacingGameController

전체적인 게임의 진행을 담당한다.

  • 유저에게 보여줄 메세지를 출력하거나 입력을 받을 때는 View에게 요청
  • 게임 진행에 필요한 작업은 Model에게 요청

View

입출력을 담당한다.

  • InputView는 입력을, OutputView는 출력을 담당한다.

Model

TryCount

레이스 시도 횟수 역할을 담당한다.

  • 사용자의 입력을 시도 횟수로 사용할 수 있는지 유효성 검사
  • 기회를 소진
  • 남은 기회가 더 있는지 판단

GameManager

자동차의 전진 가능 여부를 심판이 판단하게 하고, 각 자동차에 전진 허가를 하는 역할을 한다.

  • 심판에게 전진 가능 여부를 요청
  • Cars에게 전진 가능한 자동차를 이동시키도록 요청

Referee

자동차의 전진 가능 여부를 판단

 

Cars

Car객체들을 관리, 우승자 Car들을 선정

 

Car

이름을 가지고, 움직일 수 있는 Car 객체 역할

 

Count

비교 가능하고, 자율적으로 수를 올릴 수 있는 Count 객체 역할

 

Name

문자열 값을 가지고, 각 이름에 맞는 유효성 검사를 하며 동등성 비교가 가능한 VO

배운 점

아래는 2주 차가 끝나고 고민했던 기록들이다.

 

[JAVA] equals(), hashCode() 재정의(동등성과 동일성)

개요 각 메서드를 보기 앞서 개념을 먼저 살펴보자. 동등성 동등하다는 것은 논리적인 지위가 같다는 것을 뜻한다. 그리고 이 논리적인 지위는 개발자가 결정한다. 아래 Person 클래스로 예를 들

choi-records.tistory.com

 

 

[JAVA] VO(Value Object), 원시값 포장

개요 VO와 원시값 포장 객체는 유사한 점이 있지만, 다른 맥락에서 생긴 개념이다. 먼저 VO는 객체를 '값'으로 보기 위해서 만들어진 개념이다. 특정 객체 또는 값에게 타입을 부여해 해당 객체가

choi-records.tistory.com

 

 

 

[OOP] 디미터의 법칙

개요 최근 계속해서 객체 지향에 대한 공부를 하던 중 디미터의 법칙을 접하게 되었다. 객체 지향의 핵심을 꿰고 있는 개념이라고 생각이 되어 이에 대한 내 생각을 기록으로 남기려고 한다. 디

choi-records.tistory.com

 

일급 컬렉션

Collection을 랩핑한 객체 외 다른 멤버 변수가 없다면 해당 객체를 일급 컬렉션이라고 한다.

이번 어플리케이션에서는 Cars 객체가 일급 컬렉션이다.

 

Cars 객체는 개별적인 Car 객체가 아닌 Collection으로 묶였을 때 할 수 있는 아래와 같은 작업을 담당한다.

  • Car 중 중복된 이름의 Car가 있는지 판별
  • Car 중 가장 많이 이동한 횟수를 알아낸다.
  • Car 중 우승자를 선정한다.

여기서 중요한 것은 Car의 캡슐화를 깨지 않고 위 작업을 수행한다는 것이다.

Car의 이름을 가져오기보다는 같은 이름을 묻고, Count 객체를 Comparable 하게 만들면

각 Car의 이름과 횟수의 값을 받지 않아도 위 작업을 수행할 수 있다.

 

예를 들어 Cars 일급 컬렉션에서 최대 이동 횟수를 구했고, 같은 횟수를 가진 차들을 우승자로 뽑고 싶다고 해보자.

private List<Car> findSameMoveCountCars(final Count moveCount) {
        return cars.stream()
            .filter(car -> car.hasSameMoveCount(moveCount))
            .toList();
    }

위처럼 같은 횟수를 가진 Car 리스트를 만드는 작업을 Count 값을 가져오지 않고도 구현할 수 있다.

 

ParameterizedTest 어노테이션

1주 차 미션이 끝나고 우아한테크코스에서 1주 차 피드백 강의를 제공해 주셨다.

코드에서 처음 본 ParameterizedTest라는 어노테이션을 보게 되었는데

테스트 메서드에 인자로 여러 값을 전달해서 같은 메서드로 여러 테스트 케이스를 실행할 수 있게 해주는 기능이었다.

 

아래는 ParameterizedTest를 사용하기 이전 코드이다.

    @Test
    void 이름의_길이가_빈_문자열일_때_예외처리() {
        // when & then
        assertAll(() -> {
            IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
                () -> {
                    Name.from("");
                });
            assertEquals(INVALID_NAME_LENGTH.getMessage(), exception.getMessage());
        });
    }

    @Test
    void 이름의_길이가_최대_길이보다_길_때_예외처리() {
        // when & then
        assertAll(() -> {
            IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
                () -> {
                    Name.from("EXCEEDED_NAME");
                });
            assertEquals(INVALID_NAME_LENGTH.getMessage(), exception.getMessage());
        });
    }

 

Name을 생성할 때 전달되는 문자열 값은 다르지만, 같은 코드가 중복되고 있다.

@ParameterizedTest를 이용하면, 인자로 값을 전달해서 두 문자열 값에 대해 한 번에 테스트를 수행할 수 있다.

    @ParameterizedTest
    @ValueSource(strings = {"", "EXCEEDED_NAME"})
    void 이름의_길이가_유효하지_않을_때_예외처리(final String name) {
        // when & then
        assertAll(() -> {
            IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
                () -> {
                    Name.from(name);
                });
            assertEquals(INVALID_NAME_LENGTH.getMessage(), exception.getMessage());
        });
    }

 

정수, Enum 등 다양한 타입을 인자로 전달할 수 있어서 유용하게 쓸 것 같다.

좀 더 자세하게 공부한 후 따로 기록을 남길 것이다.

 

record

코드 리뷰를 하면서 많은 분들이 dto를 record로 사용하는 것을 봤다.

그래서 record가 무엇인지 알아봤다.

 

간단하게 말하면, 동등성과 불변성을 보장하는 값 객체를 간결하게 생성하는 기능이라고 할 수 있을 것 같다.

아래는 dto로 record를 활용한 코드이다.

public record MoveResult(String carName, int moveCount) {

    public static MoveResult from(final Car car) {
        return new MoveResult(car.getNameValue(), car.getMoveCountValue());
    }
}

한 가지 아쉬운 점은 생성자의 접근 제한자를 반드시 public으로 해야 한다는 점이다.

따라서 생성자를 private으로 제한하고, 정적 팩토리 메서드로 의도대로만 생성할 수 있도록 하는 방법이 제한된다.

 

물론 다른 방법이 있겠지만, 위 방법을 많이 사용하기 때문에 아쉬웠다.

다른 좋은 방법을 찾아 record에 대해서도 기록을 남기려고 한다. 

고민한 점

정적 팩토리 메서드

위에서도 언급했듯 정적 팩토리 메서드는 이름과 로직을 통해 의도대로 객체를 생성하도록 할 수 있다.

그리고 개인적인 생각이지만, 이 기능이 가장 큰 장점이라고 생각한다.

따라서 당연히 해당 객체에 대한 유효성 검사는 정적 팩토리 메서드에서 진행해야 한다고 생각했다.

 

상호 코드 리뷰 때 유효성 검사는 생성자에서 수행하는 것이 효율적일 것 같다는 피드백을 받았고,

고민은 이 리뷰로부터 시작됐다.

 

객체는 협력 관계에서 분명한 역할을 가진다. 때문에 해당 객체의 유효성 검사가 다양하지 않을 가능성이 비교적 높다.

물론 해당 객체가 수행해야 하는 유효성 검사가 정해져 있다면, 정적 팩토리 메서드가 늘어날수록 중복 코드가 많아지기 때문에

유효성 검사를 생성자에서 하는 것이 효율적일 것이다.

 

하지만 해당 객체가 많은 곳에서 재사용되는 객체라면, 요구되는 유효성 검사 조건도 협력 관계에 따라 다양해질 것이다.

따라서 그런 경우에는 정적 팩토리 메서드에서 협력 관계에 맞춰서 적절한 유효성 검사를 할 수 있도록 하는 것이 효율적일 것이다.

 

결론

내가 내린 결론은 아래와 같다.

타입 검사와 같이 반드시 해야 하는 유효성 검사는 생성자에서 수행하고, 범위 제한과 같이 협력 관계에 따라 다양해질 수 있는 유효성 검사는 정적 팩토리 메서드에서 수행하는 것이 좋을 것 같다.

dto에 대한 고민

상호 코드 리뷰 과정에서 Model-dto 변환 로직을 어디서 수행해야 하는가에 대해서도 의견이 갈렸다.

 

내 의견은 Model이 만약 View에서 다양한 형태로 보여져야 한다면, 하나의 Model 객체에 대한 dto도 다양 해질 테니 각 dto에서 수행하는 것이 더 나을 것 같다. 였고,

 

다른 분들의 의견은 dto에서 변환 로직이 수행되면, Model이 dto에 의존하게 될 수도 있고, Model의 캡슐화가 깨지니 Model에서 수행하는 것이 더 나을 것 같다. 였다.

 

결론

내가 내린 결론은 아래와 같다.

만약 다양한 형태의 dto가 필요하다면 각 dto에서 Model을 받아 변환시키고, 그렇지 않다면 Model의 캡슐화를 지키기 위해 Model 객체에서 변환 로직을 수행하자.

 

상호 코드 리뷰를 통해 당연하다고 생각했던 것들에서도 다양한 시각을 얻을 수 있어 유익했다!

아쉬웠던 점 

1주 차와 마찬가지로 입력에 대한 사용자 경험을 고려하지 않은 것이 가장 아쉽다..

더 배우고 싶은 점

테스트 코드를 작성하는 것에 아직 미숙해서 테스트에 대해 깊게 공부해보고 싶다.

아직 유닛 테스트를 허술하게 작성하는 정도라서 종강하고 나면 유닛 테스트도 더 깊게 공부하고, 통합 테스트에 대해서도 배우고 싶다.

 

추가로 위에서 언급했던 ParameterizedTest와 Record에 대해서도 제대로 공부하고 기록을 남길 예정이다.

코드

 

GitHub - ChoiWonYu/java-racingcar-6

Contribute to ChoiWonYu/java-racingcar-6 development by creating an account on GitHub.

github.com

마무리

이번 기록은 주관적인 의견이 많습니다.

혹시나 제가 틀렸다고 생각하시거나 다른 의견이 있으시다면 꼭 피드백 부탁드립니다..!🙏

728x90