[우아한테크코스] 프리코스 2주차 문제 회고 - 자동차 경주

2025. 10. 28. 11:54·Various Dev/우아한테크코스

들어가며

이번 2주차 과제는 1주차 때 받았던 피드백을 다시 곱씹으며 반영할 부분은 적극적으로 적용하고, 그렇지 않은 부분은 이유를 나만의 확신으로 만들며 진행했다.

새로 추가된 요구사항은 여러가지가 있었지만 가장 크게 신경 쓴 것은 함수의 depth가 3이 넘어가지 않도록 구현하는 것이다. 해당 요구사항을 지키려 하다보면 어쩔 수 없이 생각을 깊게 하게 되고, 그 생각이 곧 메서드는 하나의 기능을 수행해야한다 원칙과 일치하게 된다고 느꼈다. 그리고 테스트 코드를 비롯해 여려 방면에서 나만의 생각이 담긴 최선을 코드로 옮기다 보니, 어느 한쪽에 취우쳐져 있는 느낌이 난다. (곧 많은 리뷰로 매를 맞을 예정.. 설렌다..) 그럼 내가 어떤 선택을 코드로 옮겼는지 천천히 2주차를 돌아보자~!

 

2주차 프리코스 PR 저장소이다.

https://github.com/woowacourse-precourse/java-racingcar-8/pull/507

 

[자동차 경주] 노창준 미션 제출합니다. by geniusjun · Pull Request #507 · woowacourse-precourse/java-racingcar-8

🛠️ 시스템 흐름도 🧠 기능 구현 목록 자동차 이름을 입력받는 문구 출력 기능 '경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)' 메시지를 출력한다. 자동차 이름 입력 기능

github.com


본론으로

⚠️ 문제 개요

2주차 자동차 경주 문제는 각 자동차가 랜덤값에 따른 전진을 하여 최종 경주가 끝났을 시 우승자를 가려내는 문제였다. 프로그램의 요구 진행흐름은 다음과 같다.

  • 게임에 참여하는 자동차 이름을 입력한다("pobi,jun,noeul")
  • 자동차가 전진을 시도하는 총 횟수를 입력한다. (ex. 5)
  • 총 횟수만큼 자동차가 전진을 시도한다.
    • 자동차의 무작위 숫자가 4 이상일 경우에 전진한다.
  • 게임이 끝나면 전진 횟수가 가장 많은 자동차를 우승자로 출력한다.

🛠️ 기능 구현 목록

# 기능 구현 목록

### 자동차 이름을 입력받는 문구 출력 기능

- [x] '경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)' 메시지를 출력한다.

### 자동차 이름 입력 기능

- [x] 자동차 이름을 입력하여 자동차를 구성한다.
    - [x] 뷰 레이어의 입력값 검증
        - [x] 빈 값인지 검증한다.
        - [x] 원본 입력 문자열 그대로 컨트롤러에 반환한다.
    - [x] 뷰 -> 컨트롤러 레이어로 입력값 전달
        - [x] Cars 객체를 생성한다.
    - [x] 도메인 레이어 유효성 검증(Cars)
        - [x] 콤마를 구분자로 이름을 나눈다.
        - [x] 구분된 이름으로 Car 객체를 생성하여 List<Car>로 저장한다.
            - [x] Car 도메인의 유효성 검증
                - [x] 이름이 빈 값인지 검증한다.
                - [x] 이름이 5글자 이하인지 검증한다.
        - [x] 중복된 자동차의 이름이 없는지 검증한다.
        - [x] 2대 이상의 자동차가 존재하는지 검증한다.
        - [x] 검증된 Cars 객체를 반환한다.

### 전진 시도할 횟수 물어보는 문구 출력 기능

- [x] '시도할 횟수는 몇 회인가요?' 메시지를 출력한다.

### 전진 시도할 수 입력 기능

- [x] 전진 시도할 횟수를 입력 받는다.
    - [x] 뷰 레이어의 입력값 검증
        - [x] 빈 입력인지 검증한다.
        - [x] 숫자인지 검증한다.
        - [x] 양수인지 검증한다.
        - [x] 검증된 횟수를 정수로 변환하여 반환한다.

### 실행 결과 문구 출력 기능

- [x] '실행 결과' 메시지를 출력한다.

### 전진 시도하는 기능

- [x] 0~9 사이의 무작위 값을 구한다.
    - [x] 무작위 값이 4 이상일 경우 전진(Car의 advance++)한다.

### 전진 시도 후 결과 출력 기능

- [x] 한번 전진이 끝날때마다 각 자동차들의 이름과 전진결과를 출력한다.

### 우승자를 출력 기능

- [x] 모든 전진이 끝난 후 우승자를 출력한다.
    - [x] Cars 객체에서 자신의 List<Car> 중 advance가 가장 큰 Car의 이름을 반환한다.(중복 가능)
    - [x] View 계층에서 String.join(", ")로 형식에 알맞는 우승 결과를 만든다.

🧠 시스템 구현도


🧐 고민한 점들 & 지난 주차 피드백 적용 & 나의 선택

1. 컨트롤러가 뷰를 의존성 주입받는 방식. 왜 그랬을까?

아래 코드는 1주차 코드이다. 1주차부터 나는 컨트롤러가 View를 의존성 주입(DI) 방식으로 받아 입출력 메서드를 호출하도록 설계했다.

public CalculatorController(OutputView outputView, InputView inputView) {
        this.outputView = outputView;
        this.inputView = inputView;
}

❗❗그런데 ❗❗ 아래와 같은 피드백이 들어왔고 난 다음과 같이 답했다...

이때는 솔직히 컨트롤러는 당연하게 뷰를 알아야하고, 그 방식으로 의존성 주입 방식이 가장 안전하고 좋다 라고 알고 있어서 무의식적로 적용했기에 그냥 인정하겠습니다!! 식의 답변을 한 것 같다.

하지만 정적 메서드는 보기엔 단순하고 깔끔하지만, 그만큼 한계도 분명하다고 생각한다.

  • 테스트하기 어렵다. 정적 메서드는 가짜 객체(Mock)를 넣을 수 없어, 입출력을 제외한 로직만 따로 테스트하기가 힘들다.
  • 교체가 어렵다. 코드가 컴파일될 때 이미 ‘이 클래스의 이 메서드’로 딱 고정된다. 즉, 나중에 콘솔 대신 파일, 웹, GUI 등으로 바꾸고 싶어도 유연하게 바꾸기 힘들다.
  • 확장성이 떨어진다. 다형성이 막히기 때문에, 새로운 방식으로 확장하려면 기존 코드를 고쳐야 한다.

반면, 의존성 주입(DI) 으로 View를 외부에서 주입받으면 훨씬 유연해진다.
테스트할 땐 가짜 View를 넣어 실제 입출력 없이도 로직을 검증할 수 있고, 나중에 콘솔 대신 다른 출력 수단으로 바꾸는 것도 훨씬 쉽다.

그래서 나는 이번에도 똑같이 View를 주입받는 구조를 유지했다. 단순히 깔끔해 보이기 위함이 아니라, 테스트성과 확장성 면에서 훨씬 유리하기 때문이다.

이렇듯 이후 확장성을 고려한건가요? 라는 질문에, 위와같이 답변을 다시 드리고 싶다!

 


2.  역할과 책임에 알맞는 View를 어떻게 구성할까?

과거 코드들을 보던 중 직접 입출력을 담당하는 클래스(console)와 이를 호출하는 클래스(view)를 나눠서 view 계층을 구성하는 코드를 봤었는데 너무 깔끔했던 느낌을 받았다. 그래서인지 그 흐름을 1주차 코드에 적용했었다. 아래 사진을 참고하자! console쪽에서 사용자의 터무니 없는 빈값을 검증 중이다.

1주차 코드의 view->controller 흐름입니다.

❗❗그런데 ❗❗ 아래와 같은 피드백이 왔고 그럴싸하게 답변을 하였다..

하지만 답변을 냉철하게 다시 말하자면 그냥 미래에 있을 일을 대비했다 라는 표현으로 정리된다. 이제는 "역할과 책임" 측면에서 이 코드는 굳이 불필요한 코드라고 말할 수 있을 것 같다. 2주차 문제도 역할과 책임 측면에서 굳이 view를 쪼갤 이유를 전혀 찾지 못하였다! 그래서 아래와 같이 입력과 출력만 나눠주었다.

2주차 코드의 view -> controller 흐름입니다.


3.  getter 메서드를 지양하라 - 언제?

2주차 초반에 디스코드 지식 공유 채널의 아래의 글이 올라왔다. 처음 읽었을 때 너무 재밌게 읽었다. getter를 쓰면 캡슐화 깨지고, 객체지향성에서 멀어지고, 유지보수성 떨어지고 등등 너무 공감이 잘 되었다. 그래서 이번엔 getter가 없는 도메인을 만들어보자! 라고 무작정 마음다짐을 하고 코드작성에 들어갔다.

https://velog.io/@backfox/getter-%EC%93%B0%EC%A7%80-%EB%A7%90%EB%9D%BC%EA%B3%A0%EB%A7%8C-%ED%95%98%EA%B3%A0-%EA%B0%80%EB%B2%84%EB%A6%AC%EB%A9%B4-%EC%96%B4%EB%96%A1%ED%95%B4%EC%9A%94

 

getter 쓰지 말라고만 하고 가버리면 어떡해요

설명 좀 해주고 가요

velog.io

그래서 처음엔 최대한 메서드를 만들어서 결과만 도메인 외부로 반환해주는 식으로 많이 구성하였고, 아래처럼 toString() 오버라이드로 출력형식을 표현해보기도 하였다.(이러면 뷰가 도메인을 알아버리게 된다는 것을 매우 나중에 알았습니다...)

@Override
    public String toString() {
        return name + " : " + ("-").repeat(advance);
    }

결국 원하는 결과를 특정 메서드로만 접근하게끔 만들어 캡슐화는 지켰지만, 막상 구현해보니 프로그램이 너무 꽉 막혀 있고 확장하기 어렵다는 느낌이 들었다.

더군다나 화면에 객체의 결과를 출력해야하는데, 굳이 해당 내용을 은닉해야할까? 라는 의문점이 들었다. 결론적으로 내 제출 코드의 Car 도메인에서는 과감히 getter를 사용했다. 그러다보니, getter를 지양하는 시점은 언제일까? 라는 의문점이 들었다.

내가 내린 결론은 ‘객체가 스스로 행동할 수 있을 땐 getter를 피하되, 단순히 상태를 보여주는 역할이라면 굳이 숨길 필요는 없다’는 것이다. Car들의 상태를 보여주는 역할이기에 숨기지 않았다.

PR에 위와 같이 리뷰 포인트를 적어보았다! 많은 리뷰가 와서 나의 궁금증을 해소해줬으면..!


4. 내가 하고 있는 계층별 검증 방식이 옳은 것일까?

검증도 계층별로 하는 것이 맞다고 생각되어 아래 코드처럼 정적 중첩 클래스를 활용하여 검증 로직을 구현했다. 1주차 코드도 그렇고 2주차 코드도 이와 같은 방식으로 작성했다.

    private static class Validator {
        public static List<Car> validate(List<Car> cars) {
            validateSize(cars);
            validateDuplicateNames(cars);
            return cars;
        }

        private static void validateSize(List<Car> cars) {
            if (cars.size() < MIN_CAR_SIZE.getValue()) {
                throw CustomArgumentException.from(ErrorMessage.CAR_SIZE_ERROR);
            }
        }
        ...

1주차때 "왜 이런방법을 선택하셨어요?" "특이하네요!" 라는 리뷰에 "검증에도 캡슐화와 확장성을 챙기고 싶어서 이렇게 구현해봤어요!"라고 답변드리면 대부분 납득하는 분위기였다. 하지만 다른 분들 코드를 보면 대부분 검증을 글로벌하게 관리하거나, 정적 메서드로 관리하는 듯 하였다. 검증 로직에 대한 방법론적인 부분은 1주차부터 계속 이어져온 고민이기에 PR설명에 질문을 던졌다!

이 질문으로 검증에 대한 확신할 수 있는 개념을 얻을 수 있기를 기대하며 다음주차에 적용해 볼 예정이다.


5. 매직넘버 관리하기 - 한 곳으로 몰아넣자!

하드코딩된 숫자들을 깔끔하게 관리하고 싶었다. 처음에는 불변 상수로 클래스 윗단에 지정하는 방법으로 쭉 진행했었는데, 하다보니 멤버변수가 있어야 할 것 같은 곳에 상수가 있는 구조가 마음에 들지 않았다. 특히 일급 컬렉션에 하나의 컬렉션 자료구조만 가져야 한다 라는 규칙을 위반하는 것도 크게 거슬렸다. 그래서 아래와 같이 ENUM 클래스를 만들어서 한곳에 몰아넣기로 결정하였다.

public enum NumberType {
    NAME_MAX_LENGTH(5),
    MIN_CAR_SIZE(2),
    MIN_FORWARD_FLAG(4),
    ADVANCE_SIZE(1),
    MIN_RANDOM_NUMBER(0),
    MAX_RANDOM_NUMBER(9),
    ZERO(0),
    MINUS_ONE(-1);
    
    private final Integer value;

    NumberType(Integer value) {
        this.value = value;
    }

    public Integer getValue() {
        return value;
    }
}

사실 이게 가독성이 좋은 코드인지는 모르겠다. 처음보는 사람들이라면 결국 ENUM 클래스를 들어가봐야 할 것 같기도 하다. 그래서인지 ENUM 이름 설정을 최대한 직관적으로 느껴지게끔 하였다. 상수 관리에 관한 부분도 1주차부터 이어지는 고민 포인트인데, 나는 구조상 어지렵혀지는 것보단 이 방법이 낫다고 생각하여 이 방법으로 강행했다. 팀원들은 내 코드들을 보고 어떤 생각을 할까? 얼른 코드리뷰를 참고하며 나만의 주관을 얻고 싶다.


마무리하며

작은 부분까지 이야기하면 더 적을 부분도 많지만 이렇게 크게 다섯 가지 부분에 대해서 고민하며 프로그램을 설계하였다. 특히 이번 주차부터 추가된 함수의 depth를 3이상을 넘지말라는 요구사항을 지키기 위해 노력했다. 메서드를 나누려고 노력하다보니 하나의 메서드는 하나의 기능을 수행해야 한다는 원칙에 가까워진다는 느낌을 받았다. 또한 내가 생각하는 더 좋은 코드를 만들기 위해 enum클래스 적용, 내부 중첩 클래스 적용하는 등 주관을 설계에 녹아내어 보았다.

getter를 지양하는 도메인으로 돌아가는 프로그램을 만들어보고자 하였으나, 규칙에 맞추어 요구사항을 구현하려다 보니 코드가 오리혀 더 지저분하게 되는 경험을 하였다. 어떤 규칙이든, 디자인, 아키텍처 패턴이든 역시 수단이지 목적이 되서는 안된다는 말을 한번 더 실감하였고 요구사항에 맞게 적절한 패턴을 선택하고 클래스 간의 유기적인 흐름을 파악하는 것을 우선시해야겠다는 생각을 하였다.

'Various Dev > 우아한테크코스' 카테고리의 다른 글

[우아한테크코스] 프리코스 3주차 미션 회고 - 로또  (0) 2025.11.05
[우아한테크코스] 테스트 코드 작성 연습해보기(우테코 제공 라이브러리 적극 활용! NsTest, assertSimpleTest)  (0) 2025.10.30
[우아한테크코스] 프리코스 1주차 문제 회고 - 문자열 덧셈 계산기  (0) 2025.10.19
[미리보는 프리코스] 시작 하루 전..! 가장 hot한 논제 "각 계층의 필요시점은 어떻게 될까요?" by me (근데 이제 회고와 다짐을 곁들인..)  (2) 2025.10.13
[미리보는 프리코스] 6기 프리코스 1주차 숫자 야구게임 미리 풀어보기  (0) 2025.10.03
'Various Dev/우아한테크코스' 카테고리의 다른 글
  • [우아한테크코스] 프리코스 3주차 미션 회고 - 로또
  • [우아한테크코스] 테스트 코드 작성 연습해보기(우테코 제공 라이브러리 적극 활용! NsTest, assertSimpleTest)
  • [우아한테크코스] 프리코스 1주차 문제 회고 - 문자열 덧셈 계산기
  • [미리보는 프리코스] 시작 하루 전..! 가장 hot한 논제 "각 계층의 필요시점은 어떻게 될까요?" by me (근데 이제 회고와 다짐을 곁들인..)
노을을
노을을
진인사대천명
  • 노을을
    노을의 개발일기장
    노을을
  • 전체
    오늘
    어제
    • All (61) N
      • Java & Kotlin (16)
      • Spring (3) N
      • Problem Solve (13) N
      • Computer Science (0)
      • Infra (1)
      • DB (2)
      • Various Dev (23)
        • 우아한테크코스 (9)
        • Git&Github (2)
        • Unity (12)
      • Book (1)
      • Writing (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    오픈미션
    개발자
    티스토리챌린지
    알고리즘
    개발
    우아한테크코스
    코테
    스프링
    백준
    자바
    코딩테스트
    게임개발
    합격
    유니티
    github
    코딩
    8기
    java
    우테코
    프리코스
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
노을을
[우아한테크코스] 프리코스 2주차 문제 회고 - 자동차 경주
상단으로

티스토리툴바