우아한테크코스 프리코스 6기 FE 1주차와 OOP, Class 알아보기

modi·2023년 10월 26일
0
post-thumbnail

이번년도 우아한테크코스 프론트엔드에 지원했다! 프리코스를 진행하며 느끼고 배운 점을 정리하려고 한다.

미션

간단히 미션을 설명하자면 자바스크립트의 클래스를 활용해 숫자 야구 프로그램을 만드는 것이다. 자세한 미션은 이곳에서 확인할 수 있다.

나는 리액트 프로젝트 경험은 꽤 있는 편이지만, 프론트엔드 개발을 리액트로 시작했기 때문에 자바스크립트에 대해서 잘 모른다. 스터디와 학습을 통해 자바스크립트 공부를 했지만, 아무래도 클래스를 직접 사용한 것은 저번 프로젝트에서 WebRTC 로직을 클래스로 분리했던 것 뿐이다.

그래서 겸사겸사 우테코 프리코스 1주차와 공부도 진행하며 클래스에 대해 탐구해보려 한다.

지피티야 도와줘..!!

기능 요구사항 문서 작성도 마치고 컴퓨터 숫자 정하기 및 사용자 입력 받기 기능을 모두 App.js에 추가했을 때, 갑자기 이런 생각이 들었다.

이러다가 메서드도 엄청 많아지고 가독성도 떨어질 것 같다!!

그래서 클래스에 대해 조금 더 탐구하고 활용하기로 했다. 나의 긴급 선생님 지피티와 함께~!

참고로 나는 무료 계정을 사용하고 있다. 그동안 지피티에게 단순히 이런 기능을 구현해줘! 라고 요구하는 것은 생각보다 유연하지 못하며 프로젝트 관점보다 코드 한줄한줄 구현에 가까웠다.

이번에는 이런 기능을 구현하고 싶어!보다 어떻게 하면 나에게 낯선 클래스를 활용할 수 있을까를 중심으로 지피티에게 질문해 보았다.

Q1) 자바스크립트 클래스와 객체 지향에 관해 설명해줘

GPT says...

클래스는 객체를 생성하기 위한 템플릿이며, 객체 지향 프로그래밍의 핵심이다.

객체는 데이터와 해당 데이터를 처리하는 메서드의 묶음이다. 객체는 클래스를 통해 생성되며 클래스의 인스턴스로 간주된다.

객체 지향 프로그래밍코드를 객체의 집합으로 구조화하고, 객체 간의 상호 작용을 중심으로 프로그램을 작성하는 프로그래밍 패러다임이다. 객체 지향 프로그래밍은 코드의 재사용성, 유지보수성, 확장성을 촉진하며 복잡한 시스템을 구축하는데 유용하다. 객체 지행 프로그래밍의 핵심 개념으로 클래스, 상속, 다형성, 캡슐화 등이 있다.

나의 생각

  • 전에 자바 프로그래밍 수강 경험과 모던 JS에서 클래스 관련 문서를 읽은 덕분에 어느 정도 알고 있던 내용이다
  • 객체의 접근자 프로퍼티(getter, setter)를 활용해 캡슐화를 할 수 있겠다
  • 다형성에 대해서 아직 잘 모르겠다!

Q2) 객체 지향 프로그래밍 언어 특징 중 다형성에 대한 예시를 알려줘

GPT says...

다형성은 객체나 클래스가 동일한 인터페이스를 공유하거나 동일한 부모 클래스를 상속하며, 각각 다르게 구현될 수 있는 능력을 의미합니다. 다형성을 통해 코드의 유연성과 재사용성이 향상됩니다.

나의 생각

  • 상속은 코드 재사용성
  • 다형성은 동일한 인터페이스(메서드, 속성)을 가져도 다르게 작동할 수 있음!

이제 어느 정도 객체 지향에 대해 이해한 것 같으니 이를 코드에 활용해보자

중간 리팩토링

  • 현재 App 클래스에 모든 메서드를 작성 중이었는데 클래스를 관심사 별로 분리해보자
  • 일단 사용자의 입력 숫자와 컴퓨터의 랜덤 숫자를 관리하는 클래스를 생성하자
  • App과 숫자 야구 게임은 별개다! App을 종료하지 않으면 게임은 계속된다 숫자 야구 게임을 관리하는 클래스도 생성해보자
  • 콘솔창 입출력을 관리하는 클래스도 분리하자
  • 객체 간의 상호 작용을 생각하자!

기능 구현 및 중간 리팩토링 중간 결과

  • class Computer

    • this.selectedNumberArray: 컴퓨터가 선택한 숫자 배열
    • createNumbers(): 서로 다른 1~9까지의 숫자를 3개 가진 배열을 생성한다
  • class User

    • this.inputNumberArray: 유저가 입력한 숫자 배열
    • getValidatedInput(): 유저의 입력값이 유효성 검사를 통과하면 반환한다
  • class ConsoleUtils

    • print(message): message를 콘솔창에 출력한다
    • readLineAsync(message): message를 출력하고 콘솔창 입력을 읽는다
  • class Baseball

    • playGame(): 사용자가 컴퓨터의 숫자를 맞힐 때까지 게임을 반복한다
    • startGame(): 시작 문구를 출력하고 컴퓨터 숫자 배열을 결정한다
    • playUntilUserFindComputer(): 사용자가 입력한 숫자 배열과 컴퓨터의 숫자 배열을 비교하고 결과를 심판해 메시지를 출력한다
  • 아직 게임을 반복하는 로직은 구현 안 함

  • Baseball의 playUntilUserFindComputer()의 메서드명이 마음에 들지 않는다...

가독성은 훨씬 좋아진 것 같은데 아직 OOP의 특징을 살려 클래스를 나눴는지 모르겠다... 🙄 다시 한번 도와줘요 지피티

Q3) OOP 장점을 살려 클래스를 나눌 건데 팁 좀 줘

GPT says...

  1. 단일 책임 원칙 (Single Responsibility Principle, SRP): 클래스는 한 가지 주요 책임을 가져야 합니다. 클래스의 역할이 명확하고 일관성 있어야 합니다. 한 클래스에 여러 가지 역할을 부여하지 말고, 각 클래스가 하나의 역할을 수행하도록 분리하세요.

  2. 의존성 역전 원칙 (Dependency Inversion Principle, DIP): 고수준 모듈은 저수준 모듈에 의존하면 안 되며, 둘 다 추상화에 의존해야 합니다. 추상화를 통해 인터페이스를 정의하고 이를 구현하는 클래스를 만들어 의존성을 관리하세요.

  3. 추상화 활용: 추상 클래스나 인터페이스를 사용하여 공통된 특성을 정의하고, 다른 클래스에서 이를 구현하여 코드 재사용과 다형성을 실현하세요.

  4. 캡슐화 (Encapsulation): 클래스 내부의 데이터와 메서드를 캡슐화하여 외부에서 직접 접근하지 못하도록 보호하세요. 정보 은닉을 통해 클래스의 내부 상태를 숨기고, 공개 인터페이스를 통해 상호 작용하세요.

  5. 상속보다는 조합 (Composition): 클래스 간의 관계를 상속보다는 조합을 통해 구현하세요. 상속은 강력한 도구이지만 상속 계층 구조를 너무 깊게 만들면 유지보수가 어려워질 수 있습니다.

  6. 의존성 주입 (Dependency Injection): 클래스가 외부 의존성을 직접 생성하지 말고 외부에서 주입받도록 설계하세요. 이렇게 하면 테스트 용이성을 향상시키고 유연한 구성이 가능해집니다.

  7. 명확한 네이밍과 주석: 클래스, 메서드, 변수 등에 명확하고 의미 있는 이름을 사용하세요. 주석을 통해 코드의 동작과 사용법을 명확하게 문서화하세요.

  8. 테스트 주도 개발 (Test-Driven Development, TDD): 클래스를 설계할 때 테스트를 먼저 작성하고, 그에 맞게 클래스를 구현하는 방식을 채택하여 코드의 품질을 높이세요.

  9. 리팩터링 (Refactoring): 지속적으로 코드를 검토하고 리팩터링하여 중복 코드를 제거하고 더 나은 설계를 찾아내세요.

  10. 디자인 패턴 활용: 객체 지향 디자인 패턴을 학습하고 적절한 패턴을 코드에 적용하여 코드의 구조와 유지보수성을 향상시키세요.

각 원칙과 팁을 적절하게 활용하면 객체 지향 프로그래밍의 장점을 최대한 활용할 수 있으며, 유연하고 유지보수가 쉬운 소프트웨어를 개발할 수 있습니다.

나의 생각

  • 단일 책임의 원칙은 잘 지킨 것 같다
  • 의존성 역전이 정확히 뭔지 모르겠다

Q4) 의존성 역전 원칙 예시랑 같이 설명해줘

GPT says...

의존성 역전 원칙(Dependency Inversion Principle, DIP)은 객체 지향 프로그래밍의 원칙 중 하나로, 고수준 모듈(클래스)은 저수준 모듈(클래스)에 의존하면 안 되며, 둘 모두 추상화(인터페이스, 추상 클래스)에 의존해야 한다는 원칙입니다. 이것은 코드의 유연성과 확장성을 향상시키는 데 도움을 줍니다.

예를 들어, 주문 처리 시스템을 개발한다고 가정해 봅시다. 이 시스템은 주문 정보를 저장하고 결제를 처리하는 두 가지 주요 모듈이 필요합니다.

DIP를 위반한 설계 (나쁜 설계):

class OrderService {
  constructor() {
    this.paymentGateway = new PaymentGateway();
  }

  processOrder(order) {
    // 주문 처리 로직
    this.paymentGateway.processPayment(order);
  }
}

class PaymentGateway {
  processPayment(order) {
    // 결제 처리 로직
  }
}

위의 코드에서 OrderService 클래스는 PaymentGateway 클래스에 직접 의존하고 있습니다. 이것은 DIP를 위반한 예시입니다. 왜냐하면 고수준 모듈(OrderService)이 저수준 모듈(PaymentGateway)에 의존하고 있기 때문입니다.

DIP를 준수한 설계 (좋은 설계):

class OrderService {
  constructor(paymentProcessor) {
    this.paymentProcessor = paymentProcessor;
  }

  processOrder(order) {
    // 주문 처리 로직
    this.paymentProcessor.processPayment(order);
  }
}

class PaymentGateway {
  processPayment(order) {
    // 결제 처리 로직
  }
}

const paymentGateway = new PaymentGateway();
const orderService = new OrderService(paymentGateway);

이번에는 OrderService 클래스가 PaymentGateway 클래스 대신 paymentProcessor라는 추상화에 의존하고 있습니다. 이것은 DIP를 준수한 설계입니다. OrderService 클래스는 어떤 종류의 결제 프로세스도 처리할 수 있도록 유연성이 확보되었으며, paymentProcessor는 어떤 결제 게이트웨이든 구현할 수 있습니다.

이렇게 DIP를 준수하면 코드는 확장 가능하고 변경에 대한 유연성을 가질 수 있으며, 모듈 간의 결합도가 낮아져 유지보수가 용이해집니다.

나의 생각

class Baseball {    
    ...

  startGame() {
    this.consoleUtils.print(MESSAGE.START_GAME);
    this.computerNumbers = new Computer().selectedNumberArray;
  }

  async playUntilUserFindComputer() {
    this.userNumbers = await new User().inputNumberArray;

    let judgeResult = this.calculateResult(
      this.computerNumbers,
      this.userNumbers
    );

    this.createJudgeMessage(judgeResult);
    this.isCorrectAnswer = this.isThreeStrike(judgeResult.strikeCount);
  }
}
  • 현재 Baseball 클래스에서 정확히 예시와 똑같이 DIP를 위반하고 있다!!!

의존성 역전 원칙을 준수하며 리팩토링해보자

class App {
  constructor() {
    this.computer = new Computer();
    this.user = new User();
    this.consoleUtils = new ConsoleUtil();
    this.game = new Baseball(this.computer, this.user, this.consoleUtils);
  }

  async play() {
    await this.game.play();
  }
}
class Baseball {
  constructor(computer, user, consoleUtils) {
    this.computer = computer;
    this.user = user;
    this.consoleUtils = consoleUtils;

    this.computerNumbers = [];
    this.userNumbers = [];
    this.isCorrectAnswer = false;
  }

  async play() {
    this.startGame();

    while (!this.isCorrectAnswer) {
      await this.getUserInputAndCompareToComputer();
    }
  }

  startGame() {
    this.consoleUtils.print(MESSAGE.START_GAME);
    this.computerNumbers = this.computer.createNumberArray();
  }

  async getUserInputAndCompareToComputer() {
    this.userNumbers = await this.user.getValidatedNumberArray();

    const judgeResult = this.calculateResult(
      this.computerNumbers,
      this.userNumbers
    );

    this.createJudgeMessage(judgeResult);
    this.isCorrectAnswer = this.isThreeStrike(judgeResult.strikeCount);
  }
  ...
}

기존에는 Baseball 클래스가 Computer, User, ConsoleUtils에 직접 의존 중이었는데, constructor()을 활용해 클래스 대신 추상화(computer, user, consoleUtils)에 의존하도록 리팩토링했다.

게임 반복과 어플리케이션 종료

숫자 야구 게임까지 구현하고 남은 것은 어플리케이션 상태 관리다! 해당 기능은 App 클래스에서 구현했다.


class App {
  constructor() {
    this.computer = new Computer();
    this.user = new User(this.consoleUtils);
    this.game = new Baseball(this.computer, this.user, this.consoleUtils);
    this.isAppRunning = true;
  }

  async play() {
    this.startApp();
    while (this.isAppRunning) {
      const shouldContinue = await this.replayGame();
      if (!shouldContinue) this.isAppRunning = false;
    }
  }

  startApp() {
    Console.print(MESSAGE.START_GAME);
  }

  async replayGame() {
    const isGameFinished = !(await this.game.play());

    if (isGameFinished) {
      return this.askGameRestartOrExit();
    }
  }

  async askGameRestartOrExit() {
    const answer = await Console.readLineAsync(MESSAGE.ASK_GAME_RESTART);

    if (answer === MESSAGE.FINISH_APP) return false;
    if (answer === MESSAGE.RESTART_GAME) return true;

    throw new Error(ERROR.VALID_RESTART_INPUT);
  }
}
  • 게임 시작 문구 출력을 App 클래스로 이동시켰다! 출력되는 문구는 게임 시작이지만 사실상 어플리케이션 작동 시작의 의미이기 때문이다
  • this.isAppRunning을 통해 어플리케이션 작동 여부를 관리한다
  • 기존의 ConsoleUtils 클래스가 너무 과도한 추상화 같아서 삭제 후 다시 @woowacourse/mission-utils의 Console로 돌아왔다

이를 통해 나는 OOP의 특징 중 단일 책임 원칙, 의존성 역전 원칙, 추상화 활용, 상속보다는 조합, 의존성 주입을 지키려고 노력했다! 이제 남은 것은 명확한 네이밍과 주석, 그리고 캡슐화이다

Q5) 자바스크립트 클래스 캡슐화에 대해 알려줘

GPT says...

클래스 캡슐화(Class Encapsulation)는 객체 지향 프로그래밍(OOP)에서 중요한 개념 중 하나로, 데이터와 해당 데이터를 조작하는 메서드를 클래스 내에서 묶어서 캡슐화하는 것을 의미합니다. 이는 데이터와 데이터 조작 메서드를 외부에서 직접 접근하거나 조작하지 못하도록 보호하고, 클래스의 내부 동작을 추상화하여 클래스의 사용자가 클래스의 내부 구현 세부사항을 알 필요가 없도록 합니다.

클래스 캡슐화의 주요 원칙과 개념은 다음과 같습니다:

  1. 접근 제어(Access Control): 클래스 내의 멤버 변수와 메서드에 대한 접근을 제어합니다. 이를 통해 몇 가지 중요한 효과를 얻을 수 있습니다. 예를 들어, 멤버 변수를 외부에서 직접 접근하지 못하도록 하고, 메서드를 통해 접근 및 수정 권한을 부여할 수 있습니다.

  2. 데이터 은닉(Data Hiding): 클래스의 내부 데이터를 외부에서 직접 변경하거나 접근할 수 없도록 숨깁니다. 데이터 은닉은 클래스의 사용자에게 내부 데이터에 대한 직접적인 접근을 막아 데이터의 무결성을 보호합니다.

  3. 최소 권한 원칙(Principle of Least Privilege): 캡슐화를 사용하면 클래스의 사용자에게 필요한 최소한의 권한만 부여하여 클래스를 사용하도록 합니다. 이로써 클래스 내부 구현 세부사항은 숨겨지고, 인터페이스와 공개 메서드만 노출됩니다.

  4. 추상화(Abstraction): 클래스의 사용자는 클래스의 내부 동작을 이해하지 않아도 클래스를 사용할 수 있도록 합니다. 클래스 캡슐화를 통해 클래스의 사용자는 인터페이스와 메서드 호출로 작업을 수행하며, 내부 구현 세부사항을 몰라도 됩니다.

예를 들어, 아래는 JavaScript 클래스에서 캡슐화를 적용한 간단한 예제입니다:

class BankAccount {
  #balance = 0; // private field

  deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
    }
  }

  withdraw(amount) {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount;
    }
  }

  getBalance() {
    return this.#balance;
  }
}

const account = new BankAccount();
account.deposit(1000);
account.withdraw(500);
console.log(account.getBalance()); // 500
console.log(account.#balance); // Error: Cannot access private field

위의 예제에서 #balance는 클래스 내부에서만 접근 가능한 private 필드로, 외부에서 직접 접근할 수 없습니다. 대신, deposit, withdraw, getBalance와 같은 공개 메서드를 통해 데이터에 접근하고 조작할 수 있습니다.

클래스 캡슐화는 코드의 안정성, 유지 보수성, 확장성을 향상시키며, 객체 지향 프로그래밍의 주요 개념 중 하나입니다.

Q6) 자바스크립트 private 필드와 예시를 알려줘

GPT says...

ChatGPT
JavaScript에서 private 필드를 사용하려면 클래스 내부에서 사용하고자 하는 필드 이름 앞에 # 기호를 붙입니다. 이것은 필드가 클래스 외부에서 직접 접근 불가능하게 만듭니다. 아래에 JavaScript에서 private 필드를 사용하는 예시를 보여드리겠습니다:

class Person {
  #name; // Private 필드

  constructor(name) {
    this.#name = name; // Private 필드 초기화
  }

  get name() {
    return this.#name; // Private 필드를 읽기 위한 public getter
  }

  set name(newName) {
    this.#name = newName; // Private 필드를 쓰기 위한 public setter
  }
}

const person = new Person("Alice");

console.log(person.name); // 'Alice'
person.name = "Bob"; // Setter를 통해 private 필드 수정 가능
console.log(person.name); // 'Bob'
console.log(person.#name); // 에러! Private 필드에 직접 접근 불가능

위의 예시에서 #name은 Person 클래스의 private 필드로 정의되었습니다. 이 필드는 클래스 외부에서 직접 접근할 수 없으며, 값을 읽거나 수정하기 위해 public getter와 setter 메서드를 사용합니다. Private 필드의 이름 앞에 #을 붙이면, 해당 필드는 클래스 내부에서만 접근 가능합니다.

주의: 현재, private 필드는 일부 JavaScript 환경에서 지원되지 않을 수 있으며, 사용 가능한 환경에 따라 다를 수 있습니다. ES2022(ECMAScript 2022) 이상에서는 보다 널리 지원됩니다.

캡슐화 리팩토링

위의 답변을 토대로 App, Baseball, User, Computer 클래스를 리팩토링했다. 처음에는 모든 메서드를 private으로 정의할까 고민하다, private 필드를 접근하는 메서드만 private으로 정의했다.

결과

그리고...

제출 성공!

제출한 코드는 깃허브에서 확인할 수 있다. 이번 미션을 진행하며 자바스크립트의 Class와 객체 지향에 대해 깊이 생각해볼 수 있었다. 단일 책임 원칙, 의존성 역전 원칙, 추상화 활용, 캡슐화, 상속보다는 조합, 의존성 주입 등의 원칙을 지키려고 노력했다. 많이 부족한 코드일 수도 있지만 일단 미션을 수행하기 전의 나보다 많이 발전했음을 느낀다. 이번 미션을 기회로 클래스와 OOP에 대해 알아보자 생각한 것처럼 앞으로도 미션을 진행하며 개인 목표를 세워봐야겠다.

사실 작년에 우아한테크코스를 지원할까 고민하다, 당시에 프론트엔드 공부를 시작한지 1년도 채 안 됐고 진행 중인 프로젝트들이 있어서 다음해 지원하자 다짐했다. 작년보다는 여유가 생긴 지금 우아한테크코스를 진행해 조금 더 많이 고민할 수 있는 것 같다. 그리고 작년에 그렇게 다짐한 만큼 남은 미션도 많이 고민하며 열심히 하고 싶다!

참고자료

모던 자바스크립트 - 메서드와 this
모던 자바스크립트 - 클래스와 기본 문법

0개의 댓글