0320

회고

[우테코] 5편 : 3주차 프리코스 회고

1. 서론

안녕하세요! 팡일입니다.

2주차 미션을 마무리 지은지가 얼마 전 같은데, 벌써 3주차도 끝을 향해 가게 되었네요!

이번 3주차 프리코스는 어떤 미션이 출제되었고, 미션을 수행하면서 마주했던 새로운 스토리들을 기록하고 공유해보려고 합니다.

| 로또 - 문제 설명

이번 과제에서도 안내 사항은 기능 요구 사항, 입출력 요구 사항, 프로그래밍 요구 사항 크게 3가지로 분류되었습니다.

1) 기능 요구 사항

(1) 문제 설명

: 간단한 로또 발매기를 구현한다.

  • 로또 번호의 숫자 범위 : 1 ~ 45

  • 1개의 로또를 발행할 때, 중복되지 않는 6개의 숫자를 뽑는다.

  • 당첨 번호 추첨 시, 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑는다.

  • 당첨은 1등부터 5등까지 있다.

    • 1등 : 6개 번호 일치 / 2,000,000,000원

    • 2등 : 5개 번호 + 보너스 번호 일치 / 30,000,000원

    • 3등 : 5개 번호 일치 / 1,500,000원

    • 4등 : 4개 번호 일치 / 50,000원

    • 5등 : 3개 번호 일치

  • 로또 구입 금액을 입력하면, 구입 금액에 해당하는 만큼 로또를 발행해야 한다.

    • 로또 1장의 가격은 1,000원

  • 사용자가 구매한 로또 번호와 당첨 번호를 비교하여, 당첨 내역 및 수익률을 출력하고 로또 게임을 종료한다.

  • 사용자가 잘못된 값을 입력할 경우, '[ERROR]'로 시작하는 메세지와 함께, Error를 발생시키고, 해당 메세지를 출력한 다음, 해당 지점부터 '다시 입력을 받는다'

2) 입출력 요구 사항

(1) 입력

JavaScript
1. 로또 구입 금액을 입력 받는다.
- 구입 금액은 1,000원 단위로 받는다.
- 1,000원으로 나누어 떨어지지 않는 경우 예외 처리한다.
```
14000
```
2. 당첨 번호를 입력 받는다.
- 번호는 쉼표(,)를 기준으로 구분한다.
```
1,2,3,4,5,6
```
3. 보너스 번호를 입력 받는다.
```
7
```

(2) 출력

JavaScript
1. 발행한 로또 수량 및 번호를 출력한다.
- 로또 번호는 오름차순으로 정렬하여 보여준다.
```
8개를 구매했습니다.
[8, 21, 23, 41, 42, 43] 
[3, 5, 11, 16, 32, 38] 
[7, 11, 16, 35, 36, 44] 
[1, 8, 11, 31, 41, 42] 
[13, 14, 16, 38, 42, 45] 
[7, 11, 30, 40, 42, 43] 
[2, 13, 22, 32, 38, 45] 
[1, 3, 5, 14, 22, 45]
```

2. 당첨 내역을 출력한다.
```
3개 일치 (5,000원) - 1개
4개 일치 (50,000원) - 0개
5개 일치 (1,500,000원) - 0개
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개
6개 일치 (2,000,000,000원) - 0개
```

3. 수익률을 출력한다.
- 소수점 둘째 자리에서 반올림한다.
- ex. 100.0%, 51.5%, 1,000,000.0%
```
총 수익률은 62.5%입니다.
```

4. 예외 상황 시, 에러 문구를 출력해야 한다.
- 단, 에러 문구는 '[Error]'로 시작해야 한다.
```
[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.
```

(3) 실행 결과 예시

JavaScript
```
구입금액을 입력해 주세요.
8000

8개를 구매했습니다.
[8, 21, 23, 41, 42, 43] 
[3, 5, 11, 16, 32, 38] 
[7, 11, 16, 35, 36, 44] 
[1, 8, 11, 31, 41, 42] 
[13, 14, 16, 38, 42, 45] 
[7, 11, 30, 40, 42, 43] 
[2, 13, 22, 32, 38, 45] 
[1, 3, 5, 14, 22, 45]

당첨 번호를 입력해 주세요.
1,2,3,4,5,6

보너스 번호를 입력해 주세요.
7

당첨 통계
---
3개 일치 (5,000원) - 1개
4개 일치 (50,000원) - 0개
5개 일치 (1,500,000원) - 0개
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개
6개 일치 (2,000,000,000원) - 0개
총 수익률은 62.5%입니다.
```

3) 프로그래밍 요구 사항

(1) 기본

  • Node.js 22.19.0 버전에서 실행 가능해야 한다.

  • 프로그램 실행의 시작점은 App.js의 run()이다.

  • package.json 파일은 변경할 수 없으며, 제공된 라이브러리와 스타일 라이브러리 이외의 외부 라이브러리는 사용하지 않는다.

  • 프로그램 종료 시 process.exit()를 호출하지 않는다.

  • 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 등의 이름을 바꾸거나 이동하지 않는다.

  • 자바스크립트 코드 컨벤션을 지키면서 프로그래밍한다.

  • 기본적으로 JavaScript Style Guide를 원칙으로 한다.

(2) 추가

  • indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현하며, 2까지만 허용한다.

    • 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.

    • 힌트 : indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.

  • 3항 연산자를 사용하지 않는다.

  • 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.

  • Jest를 이용하여 정리한 기능 목록이 정상적으로 작동하는지 테스트 코트로 확인한다.

    • 테스트 도구 사용법이 익숙하지 않다면 아래 문서를 참고하여 학습한 후 테스트를 구현한다.

      • Using Matchers

      • Testing Asynchronous Code

      • Jest로 파라미터화 테스트하기: test.each(), describe.each()

  • 함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다.

    • 함수(또는 메서드)가 한 가지 일만 잘 하도록 구현한다.

  • else를 지양한다.

    • 때로는 if/else, when문을 사용하는 것이 더 깔끔해 보일 수 있다. 어느 경우에 쓰는 것이 적절할지 스스로 고민해 본다.

    • 힌트 : if 조건절에서 값을 return하는 방식으로 구현하면, else를 사용하지 않아도 된다.

  • 구현한 기능에 대한 단위 테스트를 작성한다. 단, UI(System.out, System.in, Scanner) 조직은 제외한다.

    • 단위 테스트 작성이 익숙하지 않다면, LottoTest를 참고하여 학습한 후 테스트를 작성한다.

(3) 라이브러리

  • woowacourse/mission-utils에서 제공하는 Random 및 Console API를 사용하여 구현해야 한다.

    • Random 값 추출은 Random. pickUniqueNumbersInRange()를 활용한다.

    • 사용자의 값을 입력 및 출력하려면 Console.readLineAsync()와 Console.print()를 활용한다.

  • Lotto 클래스

    • 제공된 Lotto 클래스를 사용하여 구현해야 한다.

    • Lotto에 numbers 이외의 필드(인스턴스 변수)를 추가할 수 없다.

    • numbers의 접근 제어자인 #은 변경할 수 없다.

    • Lotto의 패키지를 변경할 수 있다.

| 2주차를 통해 배운 점 Review

아래의 내용은 지난 2주차 미션 회고를 통해서 개선하고자 했던 목록입니다.

1) Commit 방식 개선

: 단순한 커밋 절차 방식이 아닌, 구체적인 커밋 구조와 메세지 가이드라인 적용하기

2) 에러 검증의 우선순위

: 입력값 유효성이 먼저이며, 그 이후 유효성에 어긋난 값은 비교할 필요가 없다.

3) 단일 책임 원칙 적용

: 단일 책임 원칙에 따라 하나의 함수 또는 메섣는 하나의 작업만 하도록 적용하기

4) MVC 패턴 적용

: Model, View, Controller로 분리하여, 각각의 역할에 맞춰 구조 만들기

실제로 3주차 미션을 진행하면서, 이 점들을 유즤하면서 진행하였고, 그 결과 전보다는 클린 코드를 더욱 잘 작성할 수 있게 되었습니다.

2. 고민의 순간 / 개선의 순간 모음

이번 3주차 미션을 수행하면서부터는, 1/2주차 미션보다는 난이도가 있었습니다. 입력도 한 가지가 아닌, 세 가지의 입력 값이 존재하다보니, 고려해야 할 것들도 많았고, 계산 과정도 결코 가볍지 않았습니다.

그 과정 속에서 마주했던 고민의 순간과 개선의 순간들을 몇 가지 공유해보려고 합니다.

| 험난했던 '단위 테스트' 과정

2주차 미션과 가장 크게 달랐던 점은 바로 '단위 테스트'를 도입해보는 것이었는데요?

테스트 코드 작성 -> MVP 기능 구현 -> 리팩토링

시작하기에 앞서, 제공되었던 LottoTest.js를 확인해보면서 구현 과정에 대해 고민한 결과,

이렇게 구현 과정을 밟아가는 것이 TDD를 잘 반영할 수 있을 것 같았습니다.

하지만, 실제로 테스트 코드를 작성하려고 하니 막막한 감정이 들었습니다.

그래서 어떤 것부터 어떻게 시작하면 좋을지 고민해본 결과,

구현해야 할 기능들과 흐름들에 대해서 분석했던 기능 명세서를 참고해서 시작해보자는 결론을 내렸습니다.

1) 실제로 진행한 단위 테스트의 예시

예를 들면, 아래와도 같습니다.

(1) 목표

: 구입 금액을 공백으로 입력했을 경우의 Validate 기능을 구현하기

(2) 작성한 테스트 코드

JavaScript
import { ERROR_MESSAGES_INPUT } from "../src/constants/Error_Messages.js";
import InputValidators from "../src/validators/InputValidators.js";

describe("로또 구입 금액 검증 테스트", () => {
  test("정상적으로 입력한 경우", () => {
    expect(() => InputValidators.validatePurchasePrice("8000")).not.toThrow();
  });

  test("값을 입력하지 않은 경우", () => {
    expect(() => InputValidators.validatePurchasePrice("")).toThrow(ERROR_MESSAGES_INPUT.EMPTY_PURCHASE_PRICE);
  });
});

수행하는 내용이 입력을 검증하는 부분이고, 구입금액과 관련된 검증이었기에,

InputValidators라는 클래스에 validatePurchasePrice라는 정적 메소드를 사용해보고자 했습니다.

(3) 작성한 기능 코드

JavaScript
import { ERROR_MESSAGES_INPUT } from '../constants/index.js';

class InputValidators {
  static validatePurchasePrice(purchasePrice) {
    InputValidators.#isEmpty(purchasePrice);
  }

  static #isEmpty(purchasePrice) {
    if (purchasePrice === '') {
      throw new Error(ERROR_MESSAGES_INPUT.EMPTY_PURCHASE_PRICE);
    }
  }
}

export default InputValidators;

#isEmpty라는 메서드를 만들어서, validatePurchasePrice 메서드에서 호출한 뒤에 '공백으로 입력한 경우'에 대한 기능을 구현했습니다.

(4) 테스트 진행

JavaScript
npm runt test __test__/InputValidatorsTest.js

터미널에서 해당 명령어를 입력하여, 작성한 테스트 코드를 검증한 결과를 보면 아래와 같이

무사히 테스트 케이스가 통과하는 것을 볼 수 있었습니다.

2) 하지만 마주했던 문제점 <1>

아직은 테스트 코드 작성 -> 기능 구현 -> 리팩토링 이 흐름이 익숙하지 않은 탓인지,

종종 기능 구현에 몰두한 나머지 테스트 케이스를 고민하지 않고 바로 메소드를 만들거나, 파일을 생성해서 로직을 고민하는 모습을 보이곤 했습니다.

무엇보다 테스트 코드에 사용되어야 할 메소드나 클래스를 뼈대를 잡아놓더라도,

이후에 "파일을 직접 생성하고 코드와 로직을 구현"해야 했기에 어떤 식으로 흐름을 잡는 것이 맞을지 의문이 들었습니다.

그 결과, 제가 내렸던 해결책은 이렇습니다.

1단계 : 그림을 그릴 때 형태만 있는 선을 연결하자.- 실제 파일을 생성하고, 실제 메소드를 만들어서 import만 시켜놓자.2단계 : 선 안에 색을 칠하고, 그림을 그리자.- 생성한 파일, 메소드 안에 로직을 추가하자.3단계 : 해당 그림의 디테일을 신경쓰자.- 기존 코드에서 개선할 점을 발견하고, 발생할 수 있는 예외상황을 찾아보자.

이렇게 저만의 기준을 세워놓고 진행하니, 기능을 구현할 때 명확하게 할 수 있는 장점이 있었습니다.

3) 하지만 마주했던 문제점 <2>

그러나 또 다른 문제점이 있었습니다.

하나의 기능을 만들 때에 하나의 테스트 케이스를 추가해서 각각의 테스트 코드를 실행하고 검증하는 것이 '단위 테스트'의 모습이라고 이해했던 저는 전체 기능을 구현한 뒤에 전체 기능 테스트를 한 번에 진행한 결과 아래와 같은 상황을 마주해야 했습니다.

각각의 단위 테스트를 진행한 파일들의 테스트 결과는 passed였지만,

전체 기능 테스트를 한 번에 처리하는 ApplicationTest.js에서 failed를 마주해야 했습니다.

'npm run start' 명령어를 입력하여 기능을 수없이 검토했음에도 불구하고, 문제점은 찾을 수가 없었습니다.

발견된 에러는 "출력되어야 할 문장이 출력이 되지 않았다는 log의 탐색 매칭의 실패"였습니다.

이를 두고 수시간을 투자하게 되면서 점차 마감 시간은 다가오고, 과제를 끝낼 수 있을지에 대한 불안감이 커져가곤 했습니다,,

하지만 희망은 결코 저를 떠나지 않았습니다.

코드를 실행하는 첫 단계부터 파악을 해보고자, index.js -> App.js를 검토해본 결과,,

왼쪽이 수정 전, 오른쪽이 수정 후

비동기로 처리되어야 하는 입력을 위해 async/await로 모든 메서드를 구성했지만,

이를 불러오는 LottoController의 run 메소드 앞에 await가 빠져있음을 발견했습니다.

이를 수정한 결과 전체 테스트 코드가 통과된 것을 볼 수 있었습니다.

어쩌면 정말 사소했지만, 이 부분으로 인해서 테스트에 실패하는, 어쩌면 단위 테스트의 방법을 잘못 사용한 점에 대해 교훈을 얻을 수 있었습니다.

이를 통해 앞으로 저는 테스트 파일 하나만을 테스트하는 것이 아니라,

전체 테스트 파일을 한 번에 테스팅하는 'npm run test' 명령어를 꼭 사용하고, 에러가 없는 것을 발생한 뒤에 다음 기능을 구현할 것을 목표로 잡게 되었습니다.

| Model, View, Controller 그리고 Services까지?

이번 프로젝트에서도 MVC 패턴을 기반으로 폴더 구조를 나누고, 각 역할에 맞게 파일을 구성했습니다.


MVC의 핵심은 ‘관심사 분리(Separation of Concerns)’에 있다고 생각했기에, 각 구성 요소가 명확한 책임을 가지고 서로의 영역을 침범하지 않도록 하는 데 집중했죠.

하지만 구현을 진행하다 보니, 한 가지 아쉬운 점이 생겼습니다.

바로, Model 중 하나인 LottoMachine 파일이 지나치게 커졌다는 점입니다.

1) 거대해진 LottoMachine

LottoMachine은 로또의 전체 흐름을 담당하는 핵심 모델로,
로또 번호 생성, 당첨 여부 계산, 통계 및 수익률 계산 등 다양한 비즈니스 로직을 담고 있었습니다.

그러다 보니 하나의 모델 안에 10~20개가 넘는 메서드가 생겼고,
결과적으로 모델의 구조를 한눈에 파악하기 어려워지고 유지보수성도 떨어지기 시작했습니다.

이 문제를 해결하기 위해, “Model의 본질이 무엇일까?”라는 근본적인 질문으로 돌아가 보게 되었습니다.

2) Model의 본질

MVC에서 Model은 단순한 데이터 저장소가 아니라,

“도메인 로직(비즈니스 로직)과 상태를 관리하는 계층” 이라고 정의할 수 있습니다.

LottoMachine 역시 ‘로또를 진행하는 데 필요한 상태를 보관하고, 그 상태를 이용해 로직을 수행하는 주체’로 볼 수 있습니다.

예전에는 Model 내부에서 다른 로직을 호출하는 게 MVC 원칙에 어긋난다고 생각했지만, 지금은 다르게 생각합니다.

Model이 View나 Controller에 직접 의존하는 것은 금지되지만, 도메인 로직을 완성하기 위해 내부적으로 다른 로직을 호출하는 것은 자연스러운 구조라는 점을 깨달았습니다.

3) 파일 분리

이제 본격적으로 관심사 분리를 실천해보기로 했습니다.

  • LottoMachine: 로또 전체를 관리하는 중심 모델

  • LottoGenerator: 로또 티켓 생성 로직

  • LottoMatcher: 당첨 번호 매칭 로직

  • LottoStatistics: 통계 및 수익 계산 로직

이 네 가지는 모두 로또 시스템의 비즈니스 로직을 담당하므로, 여전히 Model 계층에 속한다고 볼 수 있습니다.
즉, 하나의 거대한 모델을 작은 단위로 세분화했을 뿐, MVC의 원칙을 어긴 것은 아닙니다.

만약 파일을 완전히 분리한다면, 다음과 같은 구조가 됩니다:

JavaScript
src/
 ├── model/
 │    ├── Lotto.js
 │    ├── LottoMachine.js
 │    ├── LottoGenerator.js
 │    ├── LottoMatcher.js
 │    └── LottoStatistics.js

이렇게 하면 Model 폴더 내부에서도 도메인 역할이 명확히 나뉘고,
Controller나 View에서는 여전히 LottoMachine 하나만 import하여 사용할 수 있습니다.

4) Service 도입

그런데 LottoGenerator, LottoMatcher, LottoStatistics를 다시 들여다보니,
이들은 상태를 가지지 않고 연산만 수행하는 순수한 유틸리티 클래스에 가까웠습니다.

그래서 다음과 같이 Service 계층으로 분리하기로 했습니다

JavaScript
src/
 ├── model/
 │    ├── Lotto.js
 │    ├── LottoMachine.js
 ├── service/
 │    ├── LottoGenerator.js
 │    ├── LottoMatcher.js
 │    └── LottoStatistics.js

이제 구조적으로는 다음과 같게 되었습니다.

JavaScript
Controller → LottoMachine(Model)
               ├─ LottoGenerator(Service)
               ├─ LottoMatcher(Service)
               └─ LottoStatistics(Service)

LottoMachine은 상태를 가진 핵심 도메인 객체,

세 개의 Service는 상태가 없는 순수 기능 로직으로서 Model을 보조합니다.

즉, “비즈니스 로직을 책임지는 모델이 하위 도메인 서비스를 조합해 사용하는 구조” 라고 정리할 수 있습니다.

Controller는 오직 LottoMachine이라는 도메인 진입점만 알고,
LottoMachine 내부에서는 필요한 Service들을 호출해 로직을 완성합니다.

결과적으로 Controller는 내부 구현을 알 필요가 없고,
MVC의 핵심 원칙인 캡슐화와 관심사 분리가 한층 더 명확해졌습니다.

3. '2주차' 전반적인 회고

| 회고록

이번 3주차 미션 제출 시에 첨부했던 실제 회고록을 이번 포스팅에서도 공유해보려고 합니다.

“첫인상과 목표 설정”

이번 과제를 처음 봤을 때, 저는 이전 주차들보다 확실히 난이도가 높아졌다는 인상을 받았습니다. 기능 요구사항이 구체적으로 제시되어 있어서 고려해야 할 요소가 많았고, 단순히 입력과 출력만 처리하는 수준이 아니라 프로그램 전체의 흐름을 설계해야 하는 과제였습니다. 구입 금액, 당첨 번호, 보너스 번호라는 세 가지 입력을 받아야 했고, 각 입력마다 검증이 필요했습니다. 또한 구입 금액에 따라 로또를 발행하고, 당첨 번호와의 매칭을 계산하며, 전체 결과와 수익률까지 계산하는 복합적인 흐름을 구현해야 했습니다.

이러한 요구사항을 보며 단순히 기능을 구현하는 것을 넘어, 구조적으로 프로그램을 어떻게 설계할 것인가를 고민하게 되었습니다. 특히 예외 처리나 유효성 검증, 상수 관리, 클래스 간의 역할 분리 등 이전보다 더 많은 점들을 고려해야 했습니다. 그래서 본격적인 구현에 들어가기 전에 문제를 충분히 분석하고, 단계별 흐름을 문서로 정리하며 계획을 세웠습니다. 이런 과정 덕분에 이번 과제는 단순한 코딩 과제라기보다 설계와 사고력을 키우는 미션처럼 느껴졌습니다.

”요구사항에서 리팩토링까지의 흐름”

이번 과제는 요구사항 분석부터 구현, 그리고 리팩토링까지 전체 흐름이 매우 체계적으로 진행되었습니다. 우선 프로그램의 주요 단계를 정리하고, 그에 따라 어떤 데이터가 필요하며 어떤 처리가 일어나야 하는지를 파악했습니다. 구입 금액 입력 → 검증 → 로또 생성 → 로또 번호 출력 → 당첨 번호 입력 및 검증 → 보너스 번호 입력 및 검증 → 당첨 개수 계산 → 수익률 계산 → 결과 출력의 순서로 프로그램의 전반적인 흐름을 명확히 설계했습니다.

이 과정에서 발생할 수 있는 다양한 예외 상황을 함께 고려했습니다. 공백 입력, 타입 오류, 범위 초과, 중복 값 등의 검증 절차를 구체적으로 정의했고, 이러한 절차를 함수 단위로 명확히 구분하려고 했습니다. 그 결과 구현 단계에서는 흐름이 훨씬 자연스럽게 이어졌고, 구조적인 혼란 없이 기능을 완성할 수 있었습니다.

테스트 케이스를 먼저 작성하고 그에 맞춰 기능을 구현하는 과정은 여전히 쉽지 않았지만, 테스트 주도 개발의 개념을 조금씩 체득할 수 있었습니다. 특히 npm run test 명령어를 통해 반복적으로 검증하면서 기능이 정확히 작동하는지를 확인하는 과정이 인상 깊었습니다. 리팩토링 단계에서는 상수 분리, 메서드 분리, 단일 책임 원칙 등을 중심으로 코드를 개선했습니다. 함수의 길이를 15라인 이내로 유지하려고 노력했고, 각 함수가 한 가지 역할에만 집중하도록 구조를 단순화했습니다. 그 과정은 쉽지 않았지만 코드가 점점 단정해지고 깔끔해지는 것을 보며 큰 보람을 느꼈습니다.

특히 리팩토링 단계에서는 기존의 LottoMachine이 가지고 있던 여러 책임을 **세부 클래스 단위로 분리**했습니다. 로또 번호를 생성하는 LottoGenerator, 당첨 번호를 비교하는 LottoMatcher, 그리고 수익률을 계산하는 LottoStatistics를 따로 구성하면서, 각 클래스가 하나의 역할에만 집중할 수 있도록 구조를 단순화했습니다. 이를 통해 단일 책임 원칙(SRP)을 보다 명확히 적용할 수 있었고, 각 기능이 독립적으로 동작하면서 코드의 가독성과 유지보수성이 크게 향상되었습니다.

테스트 케이스를 먼저 작성하고 그에 맞춰 기능을 구현하는 과정은 여전히 쉽지 않았지만, 테스트 주도 개발의 개념을 조금씩 체득할 수 있었습니다. 특히 npm run test 명령어를 통해 반복적으로 검증하면서 기능이 정확히 작동하는지를 확인하는 과정이 인상 깊었습니다. 리팩토링 후에는 각 클래스 단위의 역할이 명확해지면서 테스트 코드의 구조도 이전보다 훨씬 직관적으로 다듬어졌습니다.

“기억에 남는 순간들”

가장 인상 깊었던 순간은 서비스 계층을 도입했을 때였습니다. PurchaseService, WinningService, ResultService를 분리하면서 애플리케이션의 비즈니스 로직이 명확히 구분되기 시작했습니다. LottoController는 이제 전체적인 흐름만 조율하고, 실제 데이터 처리와 계산은 서비스 객체가 담당하도록 구조를 변경했습니다. 로또 구매, 당첨 번호 입력, 결과 계산 등 서로 다른 책임을 가진 로직들이 각각의 파일로 분리되었고, App.js를 통해 하나로 연결될 때 구조적으로 완성된 느낌을 받았습니다. 특히 최종 결과가 의도한 대로 정확히 출력되었을 때, 잘 짜인 설계 구조가 주는 안정감과 성취감을 크게 느꼈습니다. 그 순간, “이게 진짜 객체지향적인 구조구나”라는 깨달음이 들었습니다.

또한, LottoMachine 내부에서도 중요한 전환점이 있었습니다. 하나의 클래스가 모든 로직을 처리하던 구조에서 벗어나, 로또 생성, 비교, 통계 계산의 역할을 각 LottoGenerator, LottoMatcher, LottoStatistics로 나누었을 때 프로그램의 복잡도가 눈에 띄게 줄었습니다. 각 클래스가 담당하는 책임이 명확해지자 전체 흐름을 추적하기 쉬워졌고, 이후 App.js를 통해 모든 컴포넌트를 연결했을 때 구조적으로 완성된 느낌을 받았습니다. 특히 최종 결과가 의도한 대로 정확히 출력되었을 때, 잘 짜인 설계 구조가 주는 안정감과 성취감을 크게 느꼈습니다.

“가장 도전적이었던 부분”

가장 도전적이었던 부분은 단위 테스트를 작성하고 이를 기반으로 기능을 구현하는 과정이었습니다. 처음에는 테스트 코드를 작성하는 것이 익숙하지 않았지만, 기능을 구현하기 전에 테스트 케이스를 작성해보며 예외 상황을 예측하려고 노력했습니다. 이렇게 먼저 테스트를 설계하고 나면, 기능 구현 단계에서 오히려 방향을 잡기 쉬워졌습니다.


물론 익숙하지 않은 탓에 중간에 테스트 없이 기능을 먼저 구현한 뒤, 나중에 테스트를 작성하는 실수를 하기도 했습니다. 하지만 이 방식이 비효율적이라는 것을 금세 깨닫고, 원칙대로 테스트를 먼저 작성한 뒤 기능을 구현하는 방식으로 돌아갔습니다. 이 과정에서 테스트 주도 개발의 중요성을 몸소 느꼈습니다. 아직 완벽하다고는 할 수 없지만, 점차 테스트 중심의 사고방식으로 전환하고 있다는 점에서 의미 있는 도전이었습니다.

“좋았던 점과 아쉬웠던 점”

좋았던 점은 여러 가지가 있었습니다. 먼저 README.md에 기능 요구사항, 입출력 명세, 기능 목록, 폴더 구조, 아키텍처 설계 등을 체계적으로 기록한 덕분에 프로젝트 전체를 명확하게 파악할 수 있었습니다. 또 else 문을 지양하고, 들여쓰기 깊이를 2단계로 유지하며, 함수 길이를 15라인 이하로 제한하는 등 클린 코드 원칙을 철저히 지키려 노력했습니다. 이런 습관이 코드의 가독성과 유지보수성을 높이는 데 큰 도움이 되었습니다.

리팩토링 과정에서는 코드가 단순히 동작하는 수준을 넘어서 구조적으로 얼마나 견고한지 고민할 수 있었습니다. 점진적으로 개선을 반복하면서 코드를 다듬는 과정이 즐거웠고, 개발자로서 한 단계 성장했다는 확신을 얻을 수 있었습니다. 특히 LottoMachine 내부의 리팩토링은 이번 과제의 가장 큰 성과 중 하나였습니다. 핵심 로직을 LottoGenerator, LottoMatcher, LottoStatistics로 분리함으로써, 단일 클래스에 집중되어 있던 복잡한 책임을 해소하고 구조적 일관성을 높일 수 있었습니다. 다만, 이 세 클래스 간의 의존 관계를 더 느슨하게 관리하거나, 개별 단위 테스트로 검증하는 부분은 여전히 개선 여지가 있었습니다.

반면 아쉬운 점도 있었습니다. 서비스와 컨트롤러의 역할을 처음부터 명확히 구분하지 못한 탓에, 중간에 설계를 수정하면서 리팩토링 비용이 발생했습니다. 또 개발 과정에서 README.md의 설계 문서와 실제 코드 간의 불일치가 생기는 경우도 있었습니다. 코드 변경 시 문서도 함께 관리해야 한다는 기본적인 원칙의 중요성을 다시 한번 깨닫는 계기가 되었습니다.

무엇보다 단위 테스트에 대해 더욱 익숙해질 필요성을 느꼈습니다. 이전보다 Jest와 테스트 환경 자체에는 확실히 가까워졌지만, 여전히 테스트를 더 작은 단위로 나누어 검증하는 연습이 부족하다고 느꼈습니다. 기능 단위를 세분화하고, 각 기능이 독립적으로 올바르게 동작하는지를 확인하는 과정이야말로 프로그램의 안정성을 높이는 핵심이라는 점을 다시금 깨달았습니다.

앞으로는 단순히 테스트를 통과시키는 것에 그치지 않고, 테스트가 “설계의 일부”로 작동하도록 만들고 싶습니다. 이를 위해 기능 명세서를 기반으로 먼저 테스트 시나리오를 설계하고, 그에 맞춰 기능을 구현하는 과정을 꾸준히 실천해볼 계획입니다.

“정리하며”

이번 과제는 단순한 기능 구현을 넘어, 설계와 구조의 중요성을 깊이 체감한 경험이었습니다. 테스트 주도 개발, 단일 책임 원칙, 계층 분리 등 다양한 소프트웨어 설계 개념을 직접 적용해보며, 더 나은 코드를 향한 고민이 습관이 되기 시작했습니다.

또한 이번 미션을 통해 “왜 이렇게 설계해야 하는가”라는 질문을 끊임없이 던지며 개발할 수 있었습니다. 기능이 정상적으로 작동하는 것에서 만족하지 않고, 더 깔끔하고 더 유지보수하기 좋은 코드를 만들기 위해 고민하는 과정이 즐거웠습니다. 앞으로도 이번 경험을 바탕으로 문제 해결 능력뿐만 아니라 설계적인 사고력을 함께 성장시켜 나가고자 합니다.

| 결론

어느덧 1, 2, 3번째 미션 총 3주라는 시간이 흘러갔는데요? 주마다 성장하는 스스로를 보면서 "프리코스 참여하길 잘했다"라는 생각이 들곤 했습니다.

이전보다는 난이도 있는 과제, 그리고 더 탄탄한 아키텍처에 대한 고민 등 저를 성장할 수 있도록 해준 한 주라고 생각되어 남은 기간도 화이팅해보려고 합니다.

오픈미션 기대가 많이 되는데요? 그 과정을 앞으로 꾸준히 공유해보겠습니다.

3주차 프리코스 예제테스트도 무사히 통과했습니다!