📖 우테코 7기 프리코스 3주차 회고
🍀 3주차 후기
1,2 주차와 비교했을 때 3주차에서 전체적인 객체 지향 설계에 대해서 깨달은 것 같습니다.
특히 MVC 패턴의 의존성 규칙을 더 깊이 이해하고 적용하기 위해 많은 노력을 기울였습니다. Model이 Controller와 View에 의존하지 않아야 한다는 규칙을 지키기 위해 Lotto 클래스는 완전히 독립적으로 설계했습니다. Lotto 클래스는 오직 자신의 상태(로또 번호)만을 관리하고, 외부 의존성 없이 자체적으로 유효성 검사를 수행합니다. 예를 들어, Lotto 클래스는 숫자 배열을 받아 검증하고 저장하는 것 외에는 다른 클래스의 존재를 전혀 알지 못하도록 설계했습니다.
View가 Model에만 의존해야 한다는 규칙도 중요하게 고려했습니다. OutputView 클래스는 단순히 받은 데이터를 출력하는 역할만 수행하며, 직접적으로 Controller의 메서드를 호출하지 않습니다. 예를 들어, 로또 결과를 출력할 때 OutputView는 Controller로부터 결과 데이터를 전달받아 단순히 출력만 담당합니다.
2주차 과제에서 사용하였던 MVC 패턴과 비교했을 때, Service Layer의 추가는 이번 미션의 가장 큰 구조적 변화였습니다. 이에 대해서 공부하면서 객체 지향 설계에 대한 이해도가 크게 성장했습니다. 2주차에서는 Controller가 모든 비즈니스 로직을 담당했지만, 이는 단일 책임 원칙에 위배되며 유지보수를 어렵게 만든다는 것을 깨달았습니다. 그리하여, 이후 LottoService를 도입하여 로또 게임의 핵심 로직(당첨 확인, 수익률 계산 등)을 분리했습니다. 이를 통해 단일 책임 원칙을 더 잘 준수할 수 있었고, 각 로직을 독립적으로 테스트하기도 쉬워졌습니다.
LottoService에서는 비즈니스 로직의 응집도를 높이는 데 주력했습니다. 당첨 번호 확인 로직을 여러 작은 메서드로 분리하여, 각 메서드가 한 가지 책임만 가지도록 개선했습니다. 예를 들어, 기존에 하나의 메서드에서 처리하던 당첨 확인 로직을 다음과 같이 분리했습니다. 1: 일치하는 번호 개수 확인, 2: 보너스 번호 확인, 3: 당첨 금액 계산, 4: 수익률 계산 - 이렇게 분리함으로써 각 로직을 독립적으로 테스트할 수 있었고, 코드의 재사용성도 높아졌습니다.
또한, JSDoc을 도입하여 각 메서드와 클래스의 목적, 매개변수, 반환값을 명확히 문서화했습니다. 예를 들어, Service 클래스의 메서드들은 모두 입력 타입과 반환 타입, 메서드의 목적을 JSDoc으로 명시하여 다른 개발자가 쉽게 이해할 수 있도록 했습니다.
테스트 전략도 크게 개선했습니다. 2주차에서는 주로 통합 테스트 위주로 진행했다면, 이번에는 각 클래스와 메서드별로 세분화된 단위 테스트를 작성했습니다. 특히 Jest의 test.each를 활용하여 다양한 입력 케이스에 대한 테스트를 효율적으로 구현했습니다. 예를 들어, 로또 번호 검증 테스트의 경우 여러 가지 잘못된 입력 케이스(범위 초과, 중복 숫자, 잘못된 개수 등)를 한 번에 테스트할 수 있었습니다.
메서드의 단일 책임도 더욱 철저히 지키려 노력했습니다. 예를 들어, 당첨 결과 계산 로직을 '번호 매칭 확인', '보너스 번호 확인', '당첨금 계산' 등 작은 단위로 분리했습니다. 이는 코드의 재사용성을 높이고 테스트를 쉽게 만들었습니다.
2주차 PR 리뷰에서 피드백을 통해 네이밍의 중요성에 대해서 크게 깨달은 적이 있습니다. 네이밍만으로도 메소드의 전반적인 역할을 나타낼 수 있었고, 3주차 과제에 적용하였습니다. 그리고 코드 품질 향상을 위해 명확한 네이밍과 일관된 코딩 스타일도 중요하게 생각했습니다. 메서드 이름은 해당 메서드의 역할을 명확히 표현하도록 지었으며, 변수명도 그 용도를 즉시 알 수 있도록 설정했습니다. 또한 일관된 들여쓰기와 주석 스타일을 유지하여 코드의 가독성을 높였습니다. 또한, 상수를 적극적으로 활용하여 매직 넘버를 제거했습니다. 로또 관련 상수들을 LOTTO_CONFIG 객체로 모아서 관리함으로써, 향후 변경 사항이 발생했을 때 한 곳에서만 수정하면 되도록 개선했습니다.
마지막으로 가장 많이 고민했던 것 중 하나는 static 방식과 인스턴스 생성 방식입니다. InputView의 코드를 짤 때, static 방식과 인스턴스 생성 방식 중 고민을 하였고, 효율적인 코드를 짜기 위해선 각 특징을 잘 알아야 한다고 생각했기 때문에 구글링을 통하여 깊게 공부를 하였고 사람마다 다양하게 택한다는 것을 알게되었습니다. 저는 InputView의 경우 인스턴스 생성 방법을 택하였는데, InputView를 인스턴스로 생성하도록 설계한 이유는, 테스트와 확장성 측면에서 더 유리하기 때문입니다. static 메서드를 사용할 경우, 인스턴스를 따로 생성하지 않아도 되어 편리하지만, 이러한 구조는 의존성 주입을 어렵게 만들고, 테스트나 유지보수에 제약을 줄 수 있습니다.
반면, 인스턴스를 생성하는 방식을 채택하면 의존성 주입이 가능해지며, 이를 통해 클래스 간의 결합도를 낮추고 더 유연한 설계를 할 수 있습니다. 이를 통해 InputView 클래스는 필요한 다른 객체를 주입받아 사용할 수 있어 재사용성과 확장성이 높아집니다.
또한, InputView를 클래스로 설계한 이유는 단순한 객체로 구현할 때보다 캡슐화, 추상화, 재사용성 측면에서 더 많은 장점을 제공하기 때문입니다. 이를 통해 InputView는 SOLID 원칙 중 의존성 역전 원칙(DIP)을 지키며, 향후 변화에도 유연하게 대응할 수 있는 구조를 갖추게 되었습니다.
이번 미션을 통해 단순히 기능을 구현하는 것을 넘어서, 좋은 코드란 무엇인지, 어떻게 하면 더 나은 설계를 할 수 있는지에 대해 깊이 고민할 수 있었습니다. 매주차 과제를 통해서 새로운 지식을 배우고 많은 다른 참가자분들과 피드백을 주고받으면서 다양한 방면에서 스스로 발전하고 더욱 훌륭한 개발자로 성장한다는 느낌을 많이 받아 프리코스를 즐겁게 수행할 수 있는 것 같습니다. 특히 객체 지향 설계와 테스트 주도 개발의 중요성을 체감했으며, 이를 통해 더 나은 개발자로 성장할 수 있었습니다.
3주차에서 새롭게 배운점
App에서 constroller 인스턴스 생성
class App {
async run() {
const lottoController = new LottoController();
await lottoController.run();
}
}
export default App;
보통 controller 의 인스턴스를 생성할땐 App에서 생성을 한다.
하지만 왜? 라는 이유를 묻지 않고 그저 쓰기만 하였다.
차이는 이렇다.
클래스의 constructor에서 필요한 인스턴스를 생성하면, 클래스가 생성될 때 초기화가 이루어진다.
즉, constructor에서 인스턴스를 생성하는 것은 클래스의 상태를 생성 즉시 고정시키고, 이후에는 재설정되지 않는 상태로 만드는 것과 같다.
하지만,
run 메서드 내부에서 인스턴스를 생성하면,
메서드가 호출될 때 인스턴스를 생성하게 됩니다. 즉, 실제 사용되는 시점에서 인스턴스를 초기화한다.
따라서, 인스턴스를 클래스 전반에서 재사용해야 하거나, 초기화가 반드시 필요하다면 constructor에서 인스턴스를 생성하는 것이 좋다.
반면, 인스턴스를 매번 새로 생성하고, 지연된 초기화가 필요할 경우 run 내부에서 생성하는 것이 더 유리하다.
deepFreeze
export const PROMPT_MESSAGES = Object.freeze({
INPUT: {
PURCHACE_PRICE: '구입금액을 입력해 주세요.\n',
WINNING_NUMBER: '\n당첨 번호를 입력해 주세요.\n',
BONUS_NUMBER: '\n보너스 번호를 입력해 주세요.\n',
},
OUTPUT: {
PURCHACE_QUANTITY: '개를 구매했습니다.',
WINNING_RESULT: '당첨 통계',
},
});
이 코드에서는 INPUT 과 OUTPUT 의 하위객체들은 freeze를 받지 못하게 된다.
그래서 각각 freeze를 또 해주던가 아니면 deepFreeze를 이용하여 하위 객체도 불변하게 만들어 줄 수 있다.
jsdoc
class LottoService {
#lottos;
#winningNumbers;
#bonusNumber;
/**
* @type {object<string, number>} - 3개, 4개, 5개, 5개 + 보너스, 6개 일치하는 개수를 저장하는 객체
*/
#lottoResult;
constructor(lottos, winningNumber, bonusNumber) {
this.#lottos = lottos;
this.#winningNumbers = winningNumber;
this.#bonusNumber = bonusNumber;
this.#lottoResult = { 3: 0, 4: 0, 5: 0, 5.5: 0, 6: 0 };
}
이 코드에서 object<string, number>
는 any 가 나오게 된다. 즉, 이러한 jsdoc 문법은 맞지가 않다.
3가지로 해결할 수 있는데
(1)
/**
*@type {Record<string, number>}
*/
(2)
/**
*@type {{ [key: string]: number }}
*/
(3)
/**
* @type {{
* '3': number,
* '4': number,
* '5': number,
* '5.5': number,
* '6': number
* }}
*/
이렇게 나타낼 수 있다.