자동차 경주 풀이

이프·2025년 10월 27일

woowacourse

목록 보기
5/9

이번에는 이렇게 접근했습니다!

이번 과제(자동차 경주)은 이전 과제(문자열 계산기)와 다른 관점에서 바라보고 개발했습니다.

문자열 계산기에서의 접근

링크 - 이프 (문자열 계산기)

이전 과제는 최대한 객체지향 설계원칙(SOLID)를 생각하며 구현했습니다.
이 때의 관점은 문자열 계산기라는 것은 하나의 모듈(라이브러리)였는데요!
라이브러리의 특징은 무엇이 있을까요? 제가 생각하기에는

  • Convention over Configuration(COC)가 적용되어 관례를 따릅니다!
  • 관례는 관례일뿐, 필요에 따라 커스터마이징을 할 수 있습니다!

그래서 Application에서 Controller로 어떻게 라이브러리를 활용하는지,
기본 관례부터 확장해서 커스터마이징 방법에 목적을 뒀습니다!

자동차 경주에서의 접근

자동차 경주는 Library가 아닌 Application 관점에서 바라봤습니다.

둘의 차이점은 무엇일까요?

Library는 조금 자유로운 반면 Application은 명확한 규약으로, 도메인 규칙을 강제하는 프로그램이라고 생각합니다.

물론 문자열 계산기 또한 도메인 규칙이 있었지만 단일 정보만 다루었죠!
반면, 자동차 경주는 여러 정보를 다루기 때문에 도메인 규칙이 더욱 강력하다고 생각했습니다!
그래서 저는 Application 관점에서 바라보고 자동차 경주를 개발하기 시작했습니다.

이어지는 내용부터는 Application 관점에서 어떤 고민들을 했는지 담아내도록 하겠습니다!


처음에는 구현부터!

문자열 계산기와 마찬가지로 레이어 분리, 요구사항 분석, 구현 순서로 진행했습니다.
레이어 분리는 문자열 계산기와 같으므로 생략하겠습니다.

요구사항 분석

  • 자동차는 고유한 이름을 가진다.
    • 자동차 이름은 쉼표를 기준으로 구분한다.
      • 자동차 1대가 여러 이름을 스스로 구분할 수는 없다.
    • 자동차 이름은 5자 이하이다.
    • 전진 및 정지 기능이 있다.
      • 무작위 값이 4 이상 일 경우 전진 그 외 정지이다.
  • 입력받은 이동 횟수동안 자동차를 움직이고 결과를 출력한다.
  • 경주의 최종 우승자를 한명 이상 발표한다. (여러명일 경우 쉼표를 이용해 구분한다.)
  • 잘못된 입력의 경우 예외가 발생한다.

기능 구현

Controller

  • 컨트롤러는 모든 흐름을 제어하는 역할로 구현했습니다.
  • 처음 View 입력/출력 흐름만 추가하고 각 흐름 사이에 기능을 구현했습니다.

View

Domain

  • 도메인은 자동차의 규칙인 이름을 가진다, 이동을 판단한다, 이름 길이를 지키며 구현했습니다.

  • 그 다음 RacingGame은 자동차를 생성하기 전, 이름을 분리하고 추가하는 기능
  • 그리고 자동차를 통한 경주를 진행하는 기능을 구현했습니다.

생각보다 이번 과제의 요구사항이 굉장히 간단해서 짧은 시간안에 구현할 수 있었습니다.


코드를 개선하자!

이번에도 처음에는 단순히 기능만 구현했기 때문에, 리팩토링은 필수였습니다.

이번 리팩토링 단계에서는 문자열 계산기와 다르게 확장성을 고려하지 않았기 때문에, 페르소나를 따로 설정하지 않았습니다.

Application은 고객의 요구사항이 정말 자주 바뀔 수 있습니다.
특히 B2C와 같은 대규모 사용자를 가진 엔터프라이즈급 Application 일 수록 더욱 빨리 요구사항이 변경되겠죠?

그래서 지금 요구사항에 충실하고, 미래 확장성에 대한 고민은 과감히 버려야하는 단계입니다!

불필요한 확장성 고려하지 않는 설계

SW 설계 원칙은 정말 많은데요. 앞서 소개했던 SOLID, 객체지향 원칙(캡슐화, 다형성, 상속, 추상화), Clean Code, …

이번에는 SW 3대 설계 원칙과 추가적인 원칙 하나에 대해 학습하고 적용해보는 경험을 했습니다.

YAGNI(You Aren’t Gonna Need It)

정의: 프로그래머가 필요하다고 간주할 때까지 기능을 추가하지 않는 것이 좋다는 익스트림 프로그래밍(XP)
출처: 위키피디아 - 링크

XP 공동 창시자인 구루의 경험으로 소개되는 원칙인데요!
현재 레이싱 게임에서 명확하지 않은 요구사항들을 전부 다 확장성을 열어둘 이유가 있을까요?
정말 고려한 확장성을 100% 적용할 수 있을까요?
즉 YAGNI는 너무 먼 미래를 생각하며, 생산성을 저하시키고 복잡도를 높이지 말라는 경험입니다!

KISS(Keep it Simple, Stupid)

정의: `KISS 원칙이란 디자인에서 간단하고 알기 쉽게 만드는 편이 좋다는 원리`를 말한다.
출처: 위키피디아 - 링크

시작은 미해군으로 현재는 프로그래머에게 통용된다고 합니다.
1주차 피드백에서 나온 내용이 있죠? Java에서 제공하는 메서드를 적극 활용하라!
자신만의 알고리즘이나 구현 방식도 좋지만, 이미 좋은 방법이 제공되고 있으면 그것을 활용하는게 베스트입니다!

DRY(Don’t Repeat Yourself)

정의: DRY는 변경될 가능성이 있는 정보 의 반복을 줄이고, 변경 가능성이 적은 추상화 로 대체하거나, 처음부터 중복을 피하는 데이터 정규화를 사용하는 것을 목표로 하는 소프트웨어 개발 원칙입니다 .

출처: 위키피디아 - 링크

실용주의 프로그래머에서 처음 언급된 소재로 동일한 정보를 최소한으로 관리해야한다는 것입니다!
간단히 유지보수를 한다고 생각할 때, 2개보다는 1개를 수정하는게 낫겠죠?!

Worse is better

정의: 실용성과 사용성 측면에서 기능성이 낮은 것("더 나쁨")이 더 바람직한 옵션("더 좋음")이 되는 지점이 있다는 것입니다.
출처: https://en.wikipedia.org/wiki/Worse_is_better

구현의 단순성이 정확성이나 완전성과 같은 다른 품질보다 우선해야 한다는 주장입니다.
The Right Thing과 상반되는 개념으로, MIT에서는 올바른 것을 추구하는데요.
현대 Agile을 추구하는 공학적 관점에서 보면, 미리 프로그램을 출시하고 사용자 요구사항에 빠르게 대응하는 방식을 채택합니다.
이는 최종 코딩테스트와 연관이 있을 수 있고, 원장님(?) 우테코 박재성님께서도 같은 이론을 항상 언급하시죠!

리팩토링 적용 (1)

그럼 불필요한 확장을 하지 않고, 앞서 언급된 설계 원칙을 지키며 리팩토링 지점을 찾아보겠습니다.
솔직히 가장 처음에 개발한 구조가 이미 위 네가지를 대부분 지키고 있는 것 같다고 느꼈습니다…
그래서 그나마 찾아보니 DRY를 적용할 수 있겠다고 느꼈습니다.

  • 기존 검증하는 로직을 매직 넘버에서 상수화 후, 예외에서는 포맷팅을 적용합니다.
  • 그럼, 상수 값만 변경하면 분기 처리와 상수 출력이 동시에 수정이 되겠죠?!

지식이 짧은건지, 운이 좋게 잘 구현된건지… 사실 상수화를 통해 예외 처리를 통합하는 것 외에는 따로 적용할게 안보였습니다 ㅠㅠ

그래서 리팩토링을 위해 다른 포인트를 또 알아봤습니다.

커맨드와 쿼리를 분리하라!

Command Query Separation(CQS) 패턴에 대해서 알아보겠습니다. 이것은 생각보다 되게 간단했습니다.

  • Command: 객체의 상태를 변화 O, 값을 반환하지 않음의 특성을 가지고 있습니다.
  • Query: 객체의 상태를 변화 X, 값을 반환 함의 특성을 가지고 있습니다.

즉, 완전 상반된 개념인데요!
제 코드를 보면 Domain-RacingGame 객체에서 start 메서드가 상대변화와 값 반환 책임을 동시에 수행하고 있습니다.

CQS 개념에 따르면 SoC(Separation of Concerns)가 필요한 시점입니다.

리팩토링 적용(2)

  • 커맨드와 쿼리를 분리해, CQS를 적용했습니다.
  • 이 과정에서 Domain 정보는 OOP(Object Oriented Programming)의 중심 원리 중 하나인 은닉성을 지켰습니다.
    • View는 핵심 도메인 정보대신 VO(값 객체)를 의존합니다.

명확한 구조 체계를 가져라!

현재 MVC 패턴의 큰 문제점이 있습니다.

Application의 Presentation 의존

Application 영역에서 Controller가 View를 직접적으로 의존하고 있는 것이 큰 문제입니다.

그럼 DIP를 적용하면 되지 않나요?

이런 질문이 나온다면 당신은 OOP 고수라고 생각됩니다.
하지만 앞서 소개된 YAGNI나 DRY를 생각해보면 불필요한 확장성을 설계한 꼴이 됩니다.

간단히 입력 예시만 봐도 Console의 경우 2가지 입력을 받고 있지만, 이게 웹으로 바뀐다면 4번의 Http 요청이 발생합니다.

하지만 정상 시나리오는 간단히 1번의 Http 요청으로 해결할 수 있습니다.

콘솔: 자동차 이름 입력, 시도 횟수 입력, 경주 실행, 경주 결과 반환
웹: 웹에서 입력 다 받고 경주 실행 요청 1번만 하면 됨

도메인이 외부 라이브러리를 직접 의존

Randoms 유틸은 우테코에서 제공하는 라이브러리로 도메인이 직접의존하면 안됩니다! 도메인은 순수성을 띄고 있어야 합니다!

Controller와 외부 라이브러리 의존 문제를 어떻게 해결 할 수 있을까요?

저는 앞서 소개된 문제들을 토대로 클린아키텍처를 도입하게 됐습니다.
이는 크게 외부 영역, 애플리케이션 영역, 도메인 영역으로 나뉘어집니다.
그림과 같이 동심원 구조로 외부에서 내부로만 의존할 수 있는 구조입니다.

리팩토링 적용 (3)

클린 아키텍처 개념을 적용하면 Presentation 계층에 위치한 Controller만 Web 전용으로 바꿔주면, 간단히 웹 서비스로도 구현할 수 있습니다.

리팩토링 적용 (4)

기존 자동차 내부에서 외부 라이브러리를 직접 의존하면서 결합도가 굉장히 높아집니다.

이를 DIP를 적용해 결합도를 감소할 수 있는데요, 그럼 interface의 구현체는 domain과 같은 패키지에 위치해야될까요?

라이브러리라서 같은 패키지에 위치하는 것은 부적절 할 수 있습니다.
클린아키텍처 동심원 그림을 보면, infrastructure 영역이 외부 라이브러리들을 관리하는 영역입니다.

그 말은 우테코 유틸은 외부라이브러리로 infrastructure에서 관리하면 됩니다!

패키지명을 보면 클린아키텍처 이미지와 조금 다르게 작성한 것을 볼 수 있습니다.

클린아키텍처는 헥사고날에서 파생된 개념으로 presentation(ui), infrastructure(external)은 모두 Adapter 개념으로 봅니다.

즉 외부 요소들을 연결해주는 개념으로 adapter라고 네이밍을 작성했습니다!

리팩토링 완료 패키지 구조

src/main/java/racingcar/
├── Application.java                  ← 진입점
├── application/
│   ├── RacingCarUseCase.java
│   ├── RacingCommand.java
│   └── RacingResult.java
├── domain/
│   ├── Car.java
│   ├── RacingGame.java
│   ├── ForwardCondition.java
│   ├── Attempts.java
│   └── vo/
│       └── RaceResult.java
└── adapter/
    ├── in/
    │   ├── ConsoleRacingController.java
    │   ├── InputView.java
    │   └── OutputView.java
    └── external/
        └── RandomForwardCondition.java

마치며

이번 키워드가 도전인 만큼 다양한 설계 원칙, CQS 패턴, 클린 아키텍처에 대해 학습하고 도전해봤습니다.

특히 필요 없는 것을 버리는 과정과 적당한 기준을 구분하는 것이 정말 어려웠는데요…

숨은 진실

사실 여기 다 담지 못했지만, 가장 힘들었던 건 "이게 정답인가?"라는 의문이었습니다.

  • "참가자 1명도 허용해야 하나?"
  • "ForwardCondition을 필드로 가져야 하나, 파라미터로 받아야 하나?"
  • "이 추상화는 필요한가, 과한가?"

명확한 정답이 없는 질문들이었지만, 고민의 과정 자체가 학습이었습니다.

소프트웨어 설계 원칙

그리고 YAGNI, KISS, DRY를 배우며 깨달은 건, 원칙은 맹신하는 게 아니라 상황에 맞게 적용하는 것이라는 점입니다.

"항상 DIP를 적용해야 하나?" → "Console과 Web은 근본적으로 달라서 DIP가 어색할 수 있다"

이렇게 원칙을 비판적으로 바라보는 시각을 갖게 됐습니다.

아키텍처의 한계

클린 아키텍처를 적용하며 "완벽한 설계"는 없다는 걸 배웠습니다.

지금의 구조도 완벽하지 않고, 앞으로 더 나은 방법을 배울 것입니다.

하지만 "왜 이렇게 설계했는가?"를 설명할 수 있다면, 그것으로 충분하다고 생각합니다.

이번 미션은 정답을 찾는 과정이 아니라, 질문하는 법을 배우는 과정이었습니다. 🚀

특히 필요없는 것을 버리는 과정과 적당한 기준을 구분하는 것이 정말 어려웠네요 ㅠㅠ…

profile
if (이런 시나리오는 어떨까?) then(테스트로 검증하고 해결) else(다음 시나리오 고민)

2개의 댓글

comment-user-thumbnail
2025년 10월 28일

잘 읽었습니다..!
이론적인 구조 고민을 되게 많이 하신 것 같아요👍
제 협소했던 관점이 넓어지는 기분이었어요 ㅎㅎ
저는 단순히 프로그래밍 요구사항에 주어진 규칙 지키기에 급급했는데 많이 배우고 가요!!

1개의 답글