Skip to content

Conversation

@iftype
Copy link

@iftype iftype commented Nov 2, 2025

로또

3주차 과제에서 작은 웹 환경을 경험해 보라는 의도를 느꼈습니다
그 이유는 아래의 조건 때문인데요

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

컨트롤러와 서비스 레이어에 상태를 저장하지 않고 입력이 실패했을 시에도
사용자의 데이터를 잃지 않기 위해 간단한 저장소를 추가해 데이터를 저장했습니다

레이어

image

모든 의존성은 프로그램 실행 시 App.js 의 생성자에서 주입됩니다.

기본 MVC패턴에서 상태 저장의 책임을 LottoRepository로 분리했습니다.

Service구매 후 로또 출력당첨 번호와 보너스 번호를 입력 받고 결과 출력의 두 기능으로 나눴습니다.

Controller에서 Service 레이어로 데이터를 전송할 때 DTO(Data Transfer Object)로 감싸서 웹 환경을 흉내냈습니다.

워크 플로우

서비스- 로또 구매 서비스
image
서비스- 당첨 결과 서비스
image

로또 기능 구현

image

1~45 까지의 LottoNumber 객체를 모듈이 로드될 때 단 한번 생성 되며
그 뒤의 로또 들은 미리 생성된 LottoNumber 들을 참조하게 하여 캐싱하였습니다

LottoNumber : 가장 작은 객체로, 1~45 까지의 범위를 검증하고, 그 중 한 값을 담당하며 불변 객체의 역할을 합니다

LottoNumberFactory: 모듈이 로드될 때, LottoNumber를 1부터 45까지 생성하고, 들어온 숫자를 미리 생성한 객체로 반환해주는 싱글톤 객체입니다

Lotto: 한 장의 로또라는 의미를 가진 객체입니다. LottoNumber과 마찬가지로 생성될 때 유효성 검사를 하여 유효성을 유지하게 했습니다. 6개LottoNumber 을 참조하며 주어진 값과 일치하는 번호의 갯수를 확인할 수 있습니다

LottoFactory: mission-utils라이브러리를 이용해 랜덤 숫자 배열을 생성하여 LottoNumberFactoryLotto 객체를 만들어냅니다

LottoWinningFactory: 당첨 번호와 보너스 번호를 미리 생성한 LottoNumber 에 참조 시키기 위해 LottoNumberFactory 만의 의존성을 주입 받은 객체입니다.

캐싱

image

큰 금액이 들어오면 어떡하지?

구매 금액에 제한이 없던 점을 고려해서, 불변 객체를 매번 생성하던 로직을 미리 생성해두고 참조하는 로직으로 변경하였습니다.
구매 금액에 얼마가 들어오든, 이미 생성된 인스턴스를 재사용하기 때문에 메모리 관리를 효율적으로 쓸 수 있습니다

모듈 싱글톤

// domains/LottoNumberFactory.js
class LottoNumberFactory {
  // ...
}
// 모듈 싱글톤 적용
const LottoNumberFactoryInstance = new LottoNumberFactory();
export default LottoNumberFactoryInstance;

LottoNumber 객체는 프로그램 실행 중 45개 만 존재할 수 있게
모듈 내에서 인스턴스를 생성해 그 인스턴스를 export함으로서 단 하나의 인스턴스를 갖게 했습니다

로또가 가질 번호의 갯수?

// domains/Lotto.js
class Lotto {
  static #MAX_QUANTITY = 6;
  #lottoNumbers;
  // ...

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

과제의 Lotto 클래스 조건에 위와 같은 조건이 있었습니다.
그래서 처음 설계에선 총 수량을 상수로 정의하고 외부에서 끌어썼습니다.

하지만 로또가 가질 로또 번호의 갯수는 로또 객체가 알아야 한다고 생각하여 로또 내부의 static 상수를 추가했습니다
필드를 추가하지 말라는 조건보다 인스턴스 변수에 주목하여 인스턴스들이 모두 공유하는 클래스 필드의 static 상수는 조건을 위반하지 않는다고 판단하였습니다.

유효성 검사

image

각 객체들은 생성될 때 본인의 유효성을 스스로 지켜 항상 사용할 수 있는 상태를 유지합니다

그 외로 입력, 데이터 전송, 다른 객체와 검증을 같이해야 하는 경우 (당첨번호와 보너스번호의 중복) 세 부분에서 유효성을 검사합니다.
도메인에 관련된 검사는 서비스 레이어에서만 검사하도록 수정하였습니다.

1. 입력 부에서 공백인지 확인

// view/LottoInputView.js
  async readBonusNumber() {
    const bonusNumber = await Console.readLineAsync(INFO_MEESAGE.INFO_BONUS_NUMBER);
    return LottoInputView.#checkBlank(bonusNumber);
  }
  static #checkBlank(input) {
    if (input.trim() === '') throw new Error(ERROR_MESSAGES.BLANK);
    return input;
  }

입력 단에서 빈 값이 많이 입력될 것이라 예상되어 빈 값은 우선적으로 검사해줬습니다

2. Controller 에서 Service로 입력 값을 전송해줄 때 DTO 안에서 간단한 검사(파싱, 타입변환)

Request
// dtos/requestDto/WinningNumbersDto.js
    const parseWinningNumbers = Parser.stringToNumberArray(winningNumbers);
    InputWinningNumberValidator.validate(parseWinningNumbers);
    this.#winningNumbers = parseWinningNumbers;

Controller단에서 데이터를 보낼 때 파싱과 변환을 수행해서 데이터 타입을 변경 후 타입관련 유효성 검사를 진행합니다
Service는 사용할 수 있는 데이터를 전달 받습니다

Response

// dtos/responseDto/PurchasedLottosDto.js
  toJSON() {
    const lottosToArray = [...this.#lottos].map((lotto) => lotto.getNumbers());
    return {
      lottos: lottosToArray,
   }

Service에서 Controller로 도메인 객체를 보내되, 클라이언트 단에서 사용할 때 값 배열로 변환하여 내부 구조를 숨겼습니다

3. Service에서 값을 받고 도메인 관련 검증

// services/WinningResultService.js
    const bonusLotto = this.#winningFactory.createBonusLotto(bonusNumber);
    BonusNumberValidator.validate(winningLotto, bonusLotto);

객체 끼리의 비교는 도메인 규칙이기 때문에 서비스 단에서 검사해줬습니다

세부 사항

Repository

image
// 저장시 services/LottoPurchaseService.js 
  getPurchasedLottos() {
    const insertLotto = lottos.map((lotto) => lotto.getNumbers());
    this.#lottoRepository.save('admin', { lottos: insertLotto });
// 사용시 services/WinningResultService.js 
    const db = this.#lottoRepository.findAll('admin');
    const lottos = db.lottos.map((lotto) => new Lotto(lotto));

작은 웹 환경이라 생각하고 데이터 베이스를 사용하는 식으로 구성하였습니다
실제 DB처럼 직렬화를 하여 데이터 값을 저장하고 새로 사용할 때 객체를 만들어 사용하였습니다

템플릿 에러 메세지

// constants/errorMessage.js
const PREFIX = '[ERROR]';
const format = (unit) => new Intl.NumberFormat().format(unit);
const ERROR_MESSAGES = Object.freeze({
  PURCHASE_UNIT: (PURCHASE_UNIT) =>
    `${PREFIX}구매 금액은 ${format(PURCHASE_UNIT)}원 단위로 입력해야됩니다`,
// ...
});

단위, 범위, 수량을 객체가 지니고 있기 때문에 에러 메세지 관리가 필요했습니다
템플릿 리터럴을 사용해 동적으로 값을 정해줬습니다.

DI

// domains/LottoFactory.js
  constructor(piker, lottoNumberFactory) {
    this.#piker = piker;  
    // ...
  createLotto(numbers) {
    const newArray = this.#piker.pick(numbers);
    //...

로또 팩토리는 숫자 배열을 생성자에서 주입받습니다
이를 통해 테스트 시 랜덤한 값이 아닌 고정된 숫자 배열을 사용해 테스트 할 수 있었습니다

Constants

// constans/
const LOTTO_SETTING = Object.freeze({
  PRIZES: Object.freeze({
    FIRST: 2000000000,
    SECOND: 30000000,
    THIRD: 1500000,
    FOURTH: 50000,
    FIFTH: 5000,
    OTHER: 0,
  }),
});

상금은 변경될 가능성이 높다고 생각하여 객체가 아닌 상수로 정의하였습니다

당첨 카운트

// domians/LottoWinningResult.js
  static #getWinningRank(winningStats) {
    return winningStats.map(({ winning, bonus }) => {
      if (winning === 6) return { rank: 'FIRST' };
      if (winning === 5 && bonus) return { rank: 'SECOND' };
      if (winning === 5) return { rank: 'THIRD' };
      if (winning === 4) return { rank: 'FOURTH' };
      if (winning === 3) return { rank: 'FIFTH' };
      return { rank: 'OTHER' };
    });
  }

  // #랭크로 총 순위 카운트
  static #getWinningCount(winningRank) {
    const count = { FIRST: 0, SECOND: 0, THIRD: 0, FOURTH: 0, FIFTH: 0, OTHER: 0 };
    winningRank.forEach(({ rank }) => {
      count[rank] += 1;
    });
    return count;
  }

로또 배열과 당첨 번호를 비교하여 [winning:일치 갯수, bonus: 보너스여부] 형태의 객체 배열로 변환했습니다
그 후 랭크를 상금 상수의 키와 일치 시켜 당첨 카운트를 계산했습니다

iftype added 30 commits October 29, 2025 15:15
- Prettier, ESlint: 컨벤션을 위해 스타일라이브러리 추가
- @woowacourse/mission-utils: 랜덤 함수를 사용하기 위한 유틸라이브러리 추가
- 구현할 기능 목록 추가
- 실행 흐름 추가
- 요구사항 추가
- 입력한 값에 대해서 숫자로 변환 가능한지 테스트
- 성공/실패 케이스 추가
- 입력한 값에 대해서 숫자로 변환 가능한지
- null 과 undefined 인지 체크
- 문자열로 변환하여 공백인지 체크
- 숫자로 변환해보고 체크
- 입력한 값이 숫자인지 테스트 성공/실패 케이스 추가
- 문자열로 들어온 "1"은 false
- 입력 값이 Number 타입인지
- "1"에 대해선 false
- coverage: jest coverage 파일 제외
- 값이 양수인지 확인함
- 문자열로 들어온 "1"까지 true
- 0.1도 true
- 입력 값이 숫자로 변환가능한지 검사한후 숫자로 변환한값이 0보다 큰지 검사
- 정적메서드를 가진 클래스로 바꿔 가독성 향상
- 입력한 값이 정수인지 테스트 성공/실패 케이스 추가
- 입력값이 숫자로 변할 수 있는지 검사하고 정수인지 확인
- 에러 메세지 상수파일 추가
- 숫자-정수-양수-단위 테스트를 함
- expect에 화살표 함수 추가
- 타입,정수,양수,단위 순서로 진행
- LOTTO_DUPLICATE: 중복검사
- LOTTO_QUANTITY: 수량검사
- LOTTO_RANGE: 범위검사
로또 숫자 전담하는 유효성검사 테스트 추가

 - isQuantity: 수량체크
 - isOutRange: 범위체크
 - isDuplicate: 중복체크
 - hasLottoList: 포함 여부 체크
- isQuantity: 수량체크
- isOutRange: 범위체크
- isDuplicate: 중복체크
- hasLottoList: 포함 여부 체크
- isNumber: 각 요소들이 숫자가 맞는지 테스트
- 모든 원소값이 숫자인지 판별
- isNumber의 검사부를 UtilValidator.isNum으로 사용
- 구현한 기능 목록 추가
- 구현할 기능 목록 추가
- 타입, 수량, 범위, 중복 검사를 함
- 타입, 수량, 범위, 중복 검사를 함
- 타입, 수량, 범위 검사를 추가
- 타입, 수량, 범위, 중복여부를 검사
iftype added 20 commits November 2, 2025 14:02
- 입력 받은 숫자로 같은 인스턴스를 참조하게 만들어 비교
- 전략이 하나이기 때문에 변수명 변경
- Dto내에서 타입 유효성 검사
- Domain 내에서 구매 단위 기준 검사
- 1등과 2등의 결과값 테스트
- 수익률 테스트 추가
- 특정 배열을 생성하는지 테스트
- 같은 인스턴스를 참조하는지 단위테스트
- Validator:  범위 검사를 제거, LottoNumber 을 신뢰
- errorMessage: 상수 메세지 템플릿으로 변경
- Validator: 범위 검사를 제거
- Lotto 생성에서 수량 신뢰
- errorMessage: 상수 메세지 템플릿으로 변경
- 구매금액과 금액단위를 가지는 LottoPrice를 생성
- WinningResult: 총합 계산은 불변객체인 LottoPrice에서 진행
- 구매금액과 금액단위를 가지는 LottoPrice를 생성
- WinningResult: 총합 계산은 불변객체인 LottoPrice에서 진행
- 테스트 추가
- utils-> domains 로 이동
- 상수를 객체의 프로퍼티로 가지게 함으로서 도메인 규칙을 가지게 됨
- 과제 진행 결과 추가
- LottoFacotory 에서 Lotto 안으로 이동
- LottoFactory는 Lotto를 랜덤 생성하는 팩토리
- 계층을 줄이기 위해 로또 스토어를 없애고 반복횟수를 서비스 로직에서 구함
-실제 DB를 다루듯이 영속화를 지킴
- 메서드를 다섯개로 나눔
Copy link

@manNomi manNomi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DTO 라는 개념을 어떻게 넣지 라는 고민을 많이했었는데 덕분에 갈피를 조금 잡은것 같네요

코드가 구조화가 되어있다보니 테스트하기도 쉬웠을것이고 대기업 코드를 보는것 같습니다 GOOD

저 같은 초보자가 읽기에는 다소 흐름이 힘들었던 부분이 있어서 컨벤션을 익히는것도 실력상승의 한가지 케이스가 될 수 있음도 느꼈고요
제가 만약 이 컨벤션을 잘 숙지하고 있었다면 코드흐름이 빨랐을테니까요

좋은 코드 덕분에 많이 배웠습니다!

return this.#bonusNumber;
}
}
export default BonusNumberDto;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 Dto개념을 적용하신것이 군요
DTO 에 넣을때 validate 해주면 테스트할때도 확실히 분리가 되겠네요

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하지만 제출하고 나서보니
DTO에서 validate 하는 것보다 view에서 controller로 넘어올 때 유효성 검사하는 게 더 웹환경 같더라구요
실제 controller는 서버 환경이었다는 걸 늦게 알았습니다😥


#isInteger(purchaseAmount) {
return Number.isInteger(purchaseAmount);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드가 줄어드는게 Number.isInteger -> isInteger 로 줄어드는데
꼭 분리 하신 이유가 있을까요 ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

가독성때문입니다 validate는 흐름을 제어하는 역할을 하는데
조건 속에 로직이 들어가면 어떤 역할을 하는지 조건문 속에서 읽어야 하기 때문에
메서드 이름만 봐도 뭘 검사하는지 알 수 있게 통일했습니다

this.#winningResultService = winningResultService;
this.#lottoInputView = lottoInputView;
this.#lottoOutputView = lottoOutputView;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 외부에서 의존성을 주입하는군요

의존성 주입이 저같은 초보 입장에서는 다소 코드를 읽기 힘든 경향이
있는것 같습니다

흐름이 한눈에 안읽힌다는 이유인데

내부에서 선언하는것과 외부에서 주입하는것의 차이가 크게 존재하나요 ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 의존성을 주입한 이유는 1.상태 관리 2. 결합도 3. 테스트 용이 때문입니다

  1. 상태관리
    저는 레포지토리의 인스턴스를 App의 생명 주기와 맞추고 싶었습니다
    하지만 컨트롤러나 서비스 내부에서 생성하게 된다면 저장된 데이터가 생성한 메서드가 끝날 때 같이 사라집니다.
    그래서 프로그램 상위 단계 App에서 인스턴스를 생성해 컨트롤러에 주어 생명 주기를 늘렸습니다

  2. 결합도

    const lottoRepository = new LottoRepository();
    const randomPiker = new RandomPicker();
    const randomLottoFactory = new LottoFactory(randomPiker);
    const lottoPurchaseService = new LottoPurchaseService(randomLottoFactory, lottoRepository);

직접 서비스를 import 해서 컨트롤러 내부에서 생성하는 로직을 작성했을 때,
해당 코드가 컨트롤러 내부로 들어가야 합니다
이처럼 강하게 결합되어 한 클래스를 변경 할 때마다 컨트롤러의 코드 수정을 필요하게 됩니다

  1. 테스트 용이
    제가 마지막까지 컨트롤러를 수정하느라 컨트롤러에 대한 테스트가 없어
    테스트의 용이성을 설명 못한 것 같아 아래의 코드를 첨부합니다
class LottoFactory {
  #piker;
  constructor(piker) {
    this.#piker = piker;
  }
  createLotto(numbers) {
    const newNumbers = this.#piker.pick(numbers);
    return new Lotto(newNumbers);
  }
}
const fixed = {
  pick: (arr) => arr,
};
   // 테스트 로직
      const factory = new LottoFactory(fixed);
      const result = [1, 2, 3, 4, 5, 6];
      const lotto = factory.createLotto(result);
      expect(lotto.getNumbers()).toEqual(result);

랜덤성을 가지는 로또 팩토리에 fixed 조건을 추가하여 테스트를 진행한다면
간결하게 테스트를 작성할 수 있습니다
마찬가지로 의존성을 주입 받은 모듈들은 위와 같이 mock 모듈을 주입해 간결한 단위 테스트가 가능해집니다

추가로 App만 봐도 클래스들의 관계를 알 수 있어서 목차 같은 느낌도 좋아 채택했습니다

this.#lottoOutputView.printError(err);
return false;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runLotto가 return true와 false를 반환하는데 큰이유가 있나요 ?
사용하는 부분에서는 true 또는 false가 사용되지 않아서요

지워도 크게 상관없거나
의도적으로 함수를 멈춰야한다면 return ; 으로 작성하는것과 차이가 있을까 ?
싶었습니다

함수동작만 보면 해당 true false값으로 조건을 분기할것 처럼 보여서요 !

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀하신 부분이 정확합니다 ..

Copy link

@iamodh iamodh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"ERROR 출력 후 다시 입력을 받는다."라는 요구사항에서 레퍼지토리,DTO, 캐싱 등등 실제 웹이 동작하는 방식을 미션에 적용하신 부분 존경합니다...

저는 어떻게 보면 건형님이 설계를 갈아 엎기 전의 구조를 가지고 진행을 계속했던 것 같네요.

저는 단순한 구조를 선택했음에도 설계에 어려움을 느꼈고, TDD 사이클을 지키기 위해 어떤 테스트부터 작성해야 할지 고민이 많았는데,

건형님께서는 설계를 마치고 테스트 작성에 어려움이 없으셨는지, 있었다면 어떤 방식으로 해결하셨는지 궁금합니다. (커밋 기록에 TDD의 흔적이 보여서 질문 드립니다 ㅎㅎ...)

제 PR 주소도 함께 첨부합니다!
#63

Comment on lines +5 to +6
static #MAX_QUANTITY = 6;
#lottoNumbers;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 PR에서 말씀하신

"로또가 가질 로또 번호의 갯수는 로또 객체가 알아야 한다고 생각하여 로또 내부의 static 상수를 추가했습니다"

이 부분이 Lotto.#MAX_QUNTITY처럼 변수의 네임스페이스가 명확해진다는 것을 의미할까요?
개인적으로는 LOTTO_SETTING.MAX_QUNTITY처럼 상수를 사용해도 그 의미가 충분히 전달 되고,
해당 변수를 외부에서 사용할때에도 인스턴스 호출과 관련된 코드를 작성하지 않아도 된다는 장점이 있는 것 같습니다!

Copy link
Author

@iftype iftype Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OOP적인 관점에서 클래스 변수로 관리했을 시 Lotto의 책임이 더 증가 된다고 생각했습니다
과제에서 객체지향적으로 생각하기 위해 Lotto 안에 넣어 캡슐화를 했습니다

하지만 실제 Lotto가 생성되기 전까지 에러를 뱉지 않기 때문에
실용적인 건 상수로 정의해서 컨트롤러의 유효성 검사에서 1차로 걸러내주는 게 좋다고 보긴 합니다..

Comment on lines +53 to +54
return this.#processWinningNumbers();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

입력 실패시 재입력을 위하여 catch의 리턴 값에서 메서드를 재귀 실행하는 방법이 있다는 걸 처음 알게되었습니다!
혹시 자주 사용하시는 패턴이신지 궁금합니다 ㅎㅎ (저는 모르고 이 요구사항을 빼먹었어요 ㅠ)

Copy link
Author

@iftype iftype Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안됩니다 JS에서 재귀호출시 호출스택이 쌓이면 오버플로우가 발생하기 때문에
re-try 모듈을 만들어 사용하거나 while(true)반복문을 만들고 catch 안에서 return 안하는 식으로 만들어야합니다
제 실수입니다 이거 ㅠㅠㅠㅠㅠ

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅋㅋㅋㅋㅋ 그렇군요... 그래도 덕분에 오버플로우에 대해서도 배울 수 있었네요 👍

Copy link

@itwillbeoptimal itwillbeoptimal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저번 미션부터 건형님 코드는 다양한 패턴이 등장해서 읽는 재미가 있는 것 같습니다 ㅎㅎ

한 가지 궁금한 점이 있다면, 예외 발생 시 이전 상태를 복구하기 위해 Repository를 도입하셨다고 하셨는데, 실제로는 검증에 실패한 데이터가 저장되지 않으니 복구할 상태 자체가 없지 않나 하는 생각이 들었습니다. 혹시 제가 의도를 제대로 이해하지 못한 걸까요?

아무쪼록 3주차 미션도 고생하셨고, 시간 되실 때 제 코드도 한 번 봐주시면 감사하겠습니다 🙇🏻‍♂️ (#48)

Comment on lines +10 to +23
save(id, data) {
const repoData = this.#lottoDB.get(id);
const insertData = { ...repoData, ...data };
this.#lottoDB.set(id, insertData);
}

update(id, data) {
if (!this.#lottoDB.has(id)) {
throw new Error(ERROR_MESSAGES.NOT_DATA);
}
const repoData = this.#lottoDB.get(id);
const insertData = { ...repoData, ...data };
this.#lottoDB.set(id, insertData);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

saveupdate가 기능상의 차이는 없네요. update 안에서 save를 재활용했어도 괜찮았을 것 같아요~

Copy link
Author

@iftype iftype Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀하신 부분이 정확합니다 한 번씩 입력되니 update를 쓸 일이 없어졌더라구요..

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DTO는 일반적으로 계층 간 데이터 전송이나 여러 엔티티 정보를 묶는 용도로 사용되는 것으로 알고 있습니다. 그런데 현재 구현된 DTO는 단일 값 래핑 + 검증 + getter만 제공하고 있어서, 이 경우 DTO라는 이름이 적절한지 조금 의문이 들기도 하네요 🤔

Copy link
Author

@iftype iftype Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DTO이름이 적절한지?

이 당시 제가 컨트롤러까지 클라이언트의 영역이라 생각해버렸습니다.
그래서 클라이언트에서 서버로 보낼 때의 동작과 컨트롤러와 서비스의 동작을 같다고 생각하며 구현해버렸습니다..

지금와서 드는 생각은 DTO는 데이터타입 규약만 하고
파싱과 변환은 DTO가 아닌 컨트롤러에서 처리해야 한다고 생각합니다

단일값 래핑에 대해서

DTO는 주고 받는 데이터의 규약이기 때문에 보내는 타입이 하나여도 래핑을 해야한다고 생각합니다.
responseDtoWinningResultDto처럼 한번에 여러 값을 보내는 상황이 아니면 묶을 필요가 없다고 생각했습니다

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로또 추첨은 도메인상 항상 무작위로 6개의 숫자를 뽑는다는 규칙이 고정된 영역이라고 생각하고, 전략이 바뀌거나 정책이 추가될 가능성은 거의 없다고 생각해서 저는 의존성 주입을 사용하지 않았었는데, RandomPicker의 존재 이유가 테스트 용이성 외에 또 있는지 궁금합니다! (실제 테스트에서는 Random.pickUniqueNumbersInRange를 모킹하는 편이 더 직접적인 것 같다는 생각도 듭니다)

Copy link
Author

@iftype iftype Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 로또 상금을 제외한 상수들은 변하지 않을 것이라 생각하여 클래스 변수로 뺏듯이
무작위로 6개 뽑는 규칙도 변하지 않을 규칙이라 봅니다!

정책의 추가 가능성보다는 테스트 용이성과 Random이 외부 라이브러리인 이유 때문에 밖으로 빼고 싶었습니다!

Comment on lines +13 to +29
static #isConvertNumber(purchaseAmount) {
if (purchaseAmount === null || typeof purchaseAmount === 'undefined') {
return false;
}
if (String(purchaseAmount).trim() === '') {
return false;
}
if (Number.isNaN(Number(purchaseAmount))) {
return false;
}
return true;
}

static #isInteger(purchaseAmount) {
return Number.isInteger(Number(purchaseAmount));
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#isInteger#isConvertNumber가 서로 다른 3개의 Validator 클래스에서 사용되고 있는데, 재사용성을 고려했을 때는 하나의 클래스로 통합하는 것도 나쁘지 않은 것 같습니다

Copy link
Author

@iftype iftype Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞습니다 저번 과제에서는 Validator을 묶어서 진행했었는데,
이번엔 코드를 읽을 때 파일 이동없이도 편하게 읽을 수 있게 만들어보려고 고의적으로 분리를 안했었습니다.
막상 다시 보니 InputValidator 에서는 묶는 게 좋아보이네요😥

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flyweight 패턴을 적용하신 이유가 궁금합니다! 성능 최적화를 위한 선택이셨을까요, 아니면 도메인 무결성을 보장하기 위한 의도이셨을까요? (만약 성능상의 이득을 기대하셨다면 실제로 메모리 절약을 측정해 보셨는지도 궁금합니다)

Copy link
Author

@iftype iftype Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flyweight 패턴이라 하는군요 저는 그냥 캐싱인 줄 알았습니다😅
처음 적용할 때에는 로또 번호 하나하나를 클래스로 만들기 위해 결정했을 때여서
인스턴스를 계속 만들면 위험하지 않을까? 하고 적용했었습니다.
(+ 성능적인 이점은 캐싱이 아닌 로또의 Set에서 이루어 진다고 생각합니다)

하지만 그 뒤에 DB를 흉내내면서 실질적인 메모리 절약 이점은 못 본 것 같네요
만약 실제 DB를 쓰는 상황이었으면 성능 측정까지 고려했을 것 같습니다

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+Repository에 대해서
말씀하신 게 맞습니다. 컨트롤러 안에서 처리할 수 있었는데 생각이 한 쪽으로 매몰됐었네요
굳이 Repository 안 만들어도 지역변수로 처리할 수 있었습니다 아..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants