우아한테크코스 2주차 회고 '의미 있는 도전을 해보자'

2025. 10. 28. 03:30·외부 활동/우아한테크코스 8기

여러분 안녕하세요ㅎㅎ 회고록으로 찾아뵙는 건 처음인 것 같아요.
사실 1주차는 이미 지나갔습니다..ㅋㅋ 여러모로 일거리가 한꺼번에 오는 바람에 타이밍을 놓쳤지만! 2주차부터 착실하게 회고를 진행해보려고 해요😅

1주차 문자열 덧셈 계산기 미션이 '객체의 책임'이라는 추상적인 개념을 붙들고 씨름하는 시간이었다면, 2주차 자동차 경주 미션은 그 책임을 '어떻게 테스트 가능한 코드로 분리할 것인가'에 대한 구체적인 답을 찾는 과정이었어요.

1주차에 "선 설계, 후 구현"의 힘을 어렴풋이 느꼈다면, 2주차에는 '테스트 용이성'이라는 명확한 잣대를 가지고 설계를 진행했어요. 이번에도 README.md에 기능 목록을 먼저 정의했지만, 목록을 작성하는 동시에 '이 기능을 어떻게 테스트할 것인가?'를 함께 고민했습니다.

MVVM: '나의 방식'으로 도전하기

이번 미션에서 가장 오래 고민한 부분은 아키텍처였어요. "indent depth 2 이하", "작은 함수" 요구사항을 지키면서 테스트 가능한 코드를 만들 방법이 필요했기 때문이에요.

1주차에는 DelimiterAnalyser와 NumberConverter라는 도메인 객체를 분리하는 수준에서 책임을 나누었어요. 2주차에는 이 생각을 확장해서, '안드로이드 개발자인 만큼'이라는 생각으로 MVVM 아키텍처 개념을 도입해 봤어요. 제가 안드로이드 코스를 선택한 만큼, 이 패턴을 콘솔 환경에 적용해 보는 것 자체가 의미 있는 도전이라고 생각했어요 :)

  • View: InputView, OutputView로 구성했고, 테스트가 불가능한 camp.nextstep.edu.missionutils.Console API를 사용하는 유일한 계층으로 격리시켰어요. View는 오직 입출력만 담당해요.
  • Model: Car, RacingGame, Validator 등 순수한 Kotlin 코드로 작성된 비즈니스 로직의 집합이에요.
  • ViewModel: RacingGameViewModel은 View와 Model을 중재하며 게임의 흐름을 제어해요.
  • Application: main 함수는 모든 객체를 생성하고 의존성을 주입하는 '조립자' 역할만 수행하도록 했어요.

Application이 모든 로직을 들고 있는 대신, 각 계층에 책임을 위임해서 자연스럽게 indent depth가 줄었고, 무엇보다 View를 제외한 Model과 ViewModel의 100% 단위 테스트가 가능해졌어요.

모든 결정의 중심: "이 코드는 테스트 가능한가?"

이번 미션에서는 '테스트 용이성'을 모든 기술적 결정의 최우선 기준으로 삼았어요.

1. 불변 객체를 선택한 이유 (그리고 의미 있던 시도)

Car의 위치 position을 어떻게 관리할지 고민했어요. 처음엔 익숙한 private var와 '백킹 프로퍼티'를 시도해 봤어요. 하지만 테스트 코드를 짜려니, 객체 내부의 상태가 변했는지 검증하는 과정이 깔끔하지 않았어요.

이때 data class의 불변성을 활용하는 방식이 떠올랐어요. position을 val로 갖는 data class를 선택하고 전진할 때 copy()로 새 객체를 반환하게 했어요.

move() 함수 실행 후 객체의 내부 상태를 검증하는 것보다, move() 함수가 반환한 새로운 객체의 값이 기대값과 같은지 검증하는 것이 훨씬 명확하고 단순했어요. 이건 1주차에는 미처 생각지 못했던 '함수형 프로그래밍'의 이점을 Compose이외에서 제대로 느껴본 경험이었어요.

2. 전략 패턴으로 외부 의존성 분리

자동차의 전진 조건인 Randoms.pickNumberInRange()는 테스트를 불가능하게 만드는 주범이었어요. Car.move()가 이 코드를 직접 호출했다면, move 로직을 테스트할 방법이 없었다고 생각해요.

이걸 해결하기 위해 MoveStrategy 인터페이스를 정의하고 외부에서 주입받도록 했어요.

  • 실제 실행할 땐: RandomStrategy 주입
  • 테스트할 땐: FakeStrategy(true)나 FakeStrategy(false) 주입

덕분에 Car의 move 로직을 100% 확신을 가지고 검증할 수 있었어요.

3. 의존성 역전 원칙으로 ViewModel 테스트 확보

처음에는 ViewModel이 RacingGameValidator라는 구체적인 클래스에 의존했어요. 하지만 이건 ViewModel을 테스트할 때 Validator까지 함께 테스트해야 하는 강한 결합을 만들었어요.

이걸 GameValidator라는 인터페이스에 의존하도록 의존성 역전 원칙을 적용했어요. 덕분에 ViewModel 테스트 시에는 가짜 Validator를 주입해서, ViewModel의 상태 관리 및 흐름 제어 로직에만 집중해 테스트할 수 있었어요.

1주차 원칙의 진화: 응집도와 실용성

1주차에 세웠던 원칙들도 이번 미션을 통해 더 견고해졌어요.

1. 응집도 높은 Model (과거의 나에 대한 반성)

1주차에 "SRP는 무조건 작게 나누는 것이 아니라 응집도를 높이는 것"이라고 결론을 내렸어요. 2주차에도 비슷한 고민이 있었는데요. '우승자를 판별하는 로직'은 Model과 ViewModel 중 어디에 있어야 할까?에 시간을 많이 쏟았어요.

'우승자를 찾는 것'은 명백한 게임의 핵심 비즈니스 규칙이라고 판단했어요. 따라서 이 책임은 RacingGame이 갖는 것이 가장 응집도가 높다고 보았어요. ViewModel은 그저 RacingGame으로부터 우승자 목록을 받아 View가 이해할 수 있는 형태로 가공해주는 역할에만 충실하도록 했어요.

솔직히 여태까지 뷰모델에 작은 규모의 비즈니스 규칙을 편의상 허용했었는데, 이번 계기로 '과거의 내 코드'를 리팩토링하고 싶다는 욕구가 넘쳐났습니다…ㅎㅎ

2. 실용적인 개발 (의미 있었던 1주 차의 실패)

1주차에 "확장성이라는 이름으로 불필요한 복잡성을 추가하지 말자"는 원칙을 세웠었어요. 이번 미션의 예외 처리 요구사항인 "IllegalArgumentException 발생 시 애플리케이션 종료"가 이 원칙을 적용할 좋은 기회였어요.

1주차에는 코틀린스럽게 해보자고 생각하면서 runCatching을 사용했었는데, 사실 그건 요구사항을 만족하는 데 아무 의미가 없었어요🥲🥲🥲

이번에는 그 경험을 바탕으로, 가장 실용적인 해결책인 'View에서 예외를 catch하지 않는 것'을 선택했어요. 예외를 잡지 않으면 처리되지 않은 예외로 인해 프로그램이 자연스럽게 종료되면서 요구사항을 완벽하게 만족시켰어요. 불필요한 예외 처리 로직을 추가하지 않고 요구사항에 가장 충실하게 구현했어요.

2주차를 마치며

1주차가 설계의 '왜?'를 고민하는 시간이었다면, 2주차는 '어떻게?'를 구현하는 시간이었어요. MVVM, 불변성, 전략 패턴, 의존성 역전 같은 패턴들을 '테스트 용이성'이라는 하나의 목표를 위해 적용해 본 경험 덕에, 테스트 코드에 대해 막연하게 가졌던 무서움이 사라졌어요.

아직 배울 것이 많지만, '왜 이 코드를 이렇게 작성했는가?'에 대해 스스로 답할 수 있는 코드가 늘어나고 있다는 점이 다행이네요. 3주차 미션에서도 더 나은 설계를 위해 치열하게 고민해보겠습니다ㅎㅎ

'외부 활동 > 우아한테크코스 8기' 카테고리의 다른 글

data class랑 class랑 뭐가 그렇게 다른데?  (0) 2025.10.31
'외부 활동/우아한테크코스 8기' 카테고리의 다른 글
  • data class랑 class랑 뭐가 그렇게 다른데?
한민돌
한민돌
Android 개발자가 되기까지.
  • 한민돌
    미래 가젯 연구소
    한민돌
  • 전체
    오늘
    어제
    • 분류 전체보기 (20) N
      • Android (4)
        • Compose (10)
        • Jetpack (2)
      • Kotlin (2)
        • Kotlin In Action (0)
      • 외부 활동 (2) N
        • 우아한테크코스 8기 (2) N
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

    • GitHub
  • 인기 글

  • 태그

    compose
    Multi-module
    coroutine
    AboutLibraries
    derivedstateof
    Google Recommend Architecture
    producestate
    runcatching
    LaunchedEffect
    custom plugin
    OssLicensePlugin
    rememberupdatedstate
    sideeffect
    API35
    우테코
    backing property
    Android
    build-logic
    Baseline Profile
    jetpack
  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
한민돌
우아한테크코스 2주차 회고 '의미 있는 도전을 해보자'
상단으로

티스토리툴바