객체지향 프로그래밍(OOP)에 대해서

객체지향이라 함은....붕어빵틀 애기 다들 아시죠...?

 

 

객체지향...

책을 펼쳐서 개념을 살펴보거나, 영상을 봐도 "아..."라고 속으로 생각했던 경험이 있는 거 다-아 안다. 그러니 이번 포스팅에서 좀 더 와닿게 이야기를 해보자.

 

 

 

일단 아래의 개념을 대강 쓰윽 훑어보자.

 

1. 객체지향 프로그래밍의 기본 개념

객체지향 프로그래밍은 프로그램을 객체라는 기본 단위로 분할하여 프로그래밍하는 방법이야.

객체는 데이터(속성)와 그 데이터와 관련된 기능(메서드)을 모아둔 것을 의미하지.

이러한 접근 방식은 프로그램을 더욱 모듈화 시키고, 코드 재사용을 용이하게 하며, 대규모 소프트웨어 개발에서 효율적인 관리를 가능하게 해.

 

2. 주요 개념

  • 클래스(Class) : 객체를 생성하기 위한 템플릿이야. 붕어빵을 찍어내기 위해 붕어빵틀이 있어야겠지? 그 틀이라 생각하면 돼. 클래스는 객체의 기본 형태를 정의한 거야.
  • 객체(Object) : 클래스로부터 생성된 인스턴스야. 인스턴스는 클래스로부터 생성된 객체를 인스턴스라고 불러.
  • 속성(Properties) : 객체가 가지고 있는 데이터를 의미해.
  • 메서드(Methods) : 객체가 수행할 수 있는 행동이야.

 

3. 예시 

예시 1: 자동차 객체

class Car {
  constructor(brand, model) {
    this.brand = brand;
    this.model = model;
  }

  displayInfo() {
    console.log(`This car is a ${this.brand} ${this.model}.`);
  }
}

const myCar = new Car("Hyundai", "Sonata");
myCar.displayInfo(); // "This car is a Hyundai Sonata."

 

짜잔... 이것은 자동차 설계도를 코드로 간략하게 나타낸 거야. 위의 Car클래스는 자동차를 나타내며,  brand와 model속성을 가지고 있어. displayInfo 메서드를 통해 자동차에 대한 정보를 출력할 수 있지.

 

예시 2: 은행 계좌 객체

class BankAccount {
  constructor(owner, balance) {
    this.owner = owner;
    this.balance = balance;
  }

  deposit(amount) {
    this.balance += amount;
    console.log(`Deposit ${amount}. New balance is ${this.balance}.`);
  }

  withdraw(amount) {
    if (amount <= this.balance) {
      this.balance -= amount;
      console.log(`Withdraw ${amount}. New balance is ${this.balance}.`);
    } else {
      console.log("Insufficient funds.");
    }
  }
}

const myAccount = new BankAccount("John Doe", 1000);
myAccount.deposit(500); // Deposit 500. New balance is 1500.
myAccount.withdraw(200); // Withdraw 200. New balance is 1300.

 

BankAccount 클래스는 은행 계좌를 나타내며, 계좌 소유자와 잔액을 속성으로 가져. 입금과 출금 기능은 메서드로 구현되어 있어. 

 

위의 예시를 이해하고 객체지향 프로그래밍의 핵심 개념과 '왜 쓰는지'에 대한 얘기를 해볼게.

 

우선 객체지향 프로그래밍의 핵심 개념 4가지는 캡슐화, 추상화, 상속화, 다형성이 있어.

 

1. 캡슐화(Encapsulation)

캡슐화는 객체의 내부 상태(데이터)와 그 데이터를 조작할 수 있는 메서드를 하나의 단위, 즉 '클래스'로 묶는 것을 말해. 이를 통해 객체의 상세한 구현 내용을 숨기고, 외부에서 오직 객체가 제공하는 인터페이스만을 통해 객체와 상호작용할 수 있어. 

 

 

# "어떻게 숨기고, 객체와 상호작용한다는 건데?"

 

➡️캡슐화에서 말하는 '내부 구현의 숨김'이라는 개념은 클래스 내부의 복잡한 로직이나 데이터 처리 방식을 사용자나 다른 개발자들로부터 숨기는 것을 의미해. 이를 통해, 클래스의 사용자는 클래스가 어떻게 구현되어 있는지 모르더라도, 해당 클래스가 제공하는 인터페이스(메서드 또는 프로퍼티)를 통해 필요한 기능을 사용할 수 있어.

예를 들어, 어떤 클래스가 파일을 다루는 기능을 제공한다면, 그 내부에서 파일 시스템에 접근하고 데이터를 읽거나 쓰는 등의 복잡한 작업이 이루어질 수 있어. 하지만 클래스를 사용하는 측면에서는 그러한 복잡성을 알 필요 없이, 단순히 파일을 읽는 메서드나 쓰는 메서드를 호출함으로써 원하는 작업을 수행할 수 있지!

'import'와 'export'를 사용하는 모듈 시스템에서도 이 개념이 적용돼. 클래스나 메서드들을 모듈로써 export 하고, 이를 필요한 곳에서 import 함으로써 해당 기능을 사용할 수 있어. 이 과정에서 모듈의 사용자는 모듈이 내부적으로 어떻게 구현되어 있는지 알 필요가 없으며, 단지 제공된 인터페이스를 통해 모듈을 사용할 수 있어.

이러한 방식으로, 캡슐화는 코드의 내부 구현 세부 사항을 숨기고, 코드 간의 결합도를 낮추며, 유지보수성을 향상시킬 수 있는 거야. 

 

그리고 위의 은행 계좌 객체에서 계좌 잔액(balance)은 외부에서 직접 접근하여 변경할 수 없고, 입금(deposit)과 출금(withdraw) 메서드를 통해서만 잔액을 변경할 수 있어. 이러한 방식으로 객체와 상호작용한다고 할 수 있지.

 

 

2. 추상화(Abstraction)

추상화는 복잡한 내부 구현 사항을 숨기고 필요한 부분만을 간략하게 표현하는 것을 말해. 사용자는 복잡한 내부 로직을 모르더라도 객체가 제공하는 인터페이스만을 사용하여 기능을 사용할 수 있어. 이로써 사용자는 복잡한 세부사항을 몰라도 되므로 더 쉽게 코드를 사용하고 재사용할 수 있어. 예를 들면 위의 자동차 객체가 있을 때, 운전자는 엔진의 내부 구조나 변속 시스템의 작동 방식을 모르더라도 핸들, 가속 페달, 브레이크 등의 인터페이스를 사용하여 운전할 수 있어.

 

 

# 캡슐화와 같은 말 아니에요? 

 

➡️서로 밀접하게 연관되어 있기에 비슷하지만, 그 용도와 초점에 있어서는 차이가 있어.

 

캡슐화

캡슐화는 데이터(객체의 상태)와 데이터를 처리하는 메서드를 하나의 단위로 묶는 것을 의미해. 이 개념의 핵심은 객체의 내부 구현을 외부로부터 감추고, 객체와 상호작용할 때는 공개된 인터페이스만을 사용한다는 거야. 이를 통해 코드의 수정이 필요할 때 내부 구현을 변경해도 다른 코드에 영향을 주지 않도록 함으로써, 코드의 유지보수성을 높이고 오류가 발생할 가능성을 줄여줘.

class BankAccount {
  #balance; // private property

  constructor(initialBalance) {
    this.#balance = initialBalance;
  }

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

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

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

let account = new BankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 1300, 외부에서 직접 #balance에 접근할 수 없으며, 메서드를 통해서만 상호작용 가능

 

추상화

추상화는 복잡한 실제 내용을 간단하게 만드는 과정이야. 이 개념은 복잡한 내부 작업을 숨기고 사용자에게는 필수적인 기능만을 제공함으로써 사용자가 쉽게 객체를 이해하고 사용할 수 있게 해 줘. 추상화는 사용자가 복잡한 내부 구현을 알 필요 없이 객체의 기능에 집중할 수 있도록 도와주지.

 

추상화는 복잡한 구현 내용을 감추고, 사용자가 쉽게 사용할 수 있는 인터페이스를 제공하는 것인데, 자바스크립트는 추상 클래스를 직접 지원하지는 않지만, 클래스 상속과 메서드 오버라이딩을 통해 비슷한 효과를 낼 수 있어. 그 예를 보자.

 

class PaymentMethod {
  pay(amount) {
    throw new Error("메서드를 구현해야 합니다.");
  }
}

class CreditCardPayment extends PaymentMethod {
  pay(amount) {
    console.log(`₩${amount}가 신용카드로 결제되었습니다.`);
  }
}

class PayPalPayment extends PaymentMethod {
  pay(amount) {
    console.log(`₩${amount}가 PayPal을 통해 결제되었습니다.`);
  }
}

function processPayment(method, amount) {
  method.pay(amount);
}

let creditPayment = new CreditCardPayment();
let paypalPayment = new PayPalPayment();

processPayment(creditPayment, 10000); // 신용카드로 결제 처리
processPayment(paypalPayment, 5000); // PayPal로 결제 처리

 

결론은
캡슐화는 BankAccount 클래스를 통해 구현되었으며, 이는 내부 #balance 속성에 대한 접근을 제한하고, 메서드를 통해 상호작용을 규제해.
추상화는 PaymentMethod 클래스와 그 구현체들(CreditCardPayment, PayPalPayment)를 사용하여 실현돼. 사용자는 pay 메서드의 구체적인 구현 내용을 몰라도 결제 처리를 할 수 있어.

 

3. 상속화(Inheritance)

상속은 한 클래스(부모 클래스)의 특성(속성과 메서드)을 다른 클래스(자식 클래스)가 물려받아 사용할 수 있게 하는 거야. 위의 예제를 보면 알 수 있지.

예를 들면, 동물 클래스가 일반적인 동물의 특성을 가지고 있을 때, 강아지 클래스와 고양이 클래스는 동물 클래스로부터 특성을 상속받아 각각의 특화된 기능을 추가할 수 있지.

 

4. 다형성(Polymorphism)

다형성은 같은 이름의 메서드가 다른 클래스 내에서 다른 작업을 수행할 수 있게 해주는 특성이야. 이를 통해 다른 기능을 하지만 같은 이름을 가진 메서드에 대해 일관된 인터페이스를 제공할 수 있어. 

예를 들면, 도형 클래스에서 '넓이를 구하다'는 메서드를 정의할 때, 이 메서드는 사각형, 원, 삼각형 클래스에서 각각 다른 방식으로 넓이를 계산해. 하지만 사용자는 단순히 '넓이를 구하다' 메서드를 호출함으로써, 형태에 상관없이 넓이를 얻을 수 있어.

 

// 슈퍼클래스 Animal 정의
class Animal {
  speak() {
    console.log('An animal makes a sound.');
  }
}

// Dog 클래스는 Animal 클래스를 상속받는다.
class Dog extends Animal {
  speak() {
    console.log('Woof! Woof!');
  }
}

// Cat 클래스는 Animal 클래스를 상속받는다.
class Cat extends Animal {
  speak() {
    console.log('Meow! Meow!');
  }
}

// Bird 클래스는 Animal 클래스를 상속받는다.
class Bird extends Animal {
  speak() {
    console.log('Tweet! Tweet!');
  }
}

// 각각의 인스턴스 생성
let dog = new Dog();
let cat = new Cat();
let bird = new Bird();

// speak 메소드를 호출. 각 객체의 구현에 따라 다른 출력을 나타낸다.
dog.speak(); // 출력: Woof! Woof!
cat.speak(); // 출력: Meow! Meow!
bird.speak(); // 출력: Tweet! Tweet!

 

위 코드에서 Animal 클래스는 speak 메서드를 가진 슈퍼클래스로, Dog, Cat, Bird 각각의 하위 클래스에서는 이 speak 메서드를 다른 방법으로 구현(오버라이딩)하여 다형성을 보여줘. 이렇게 하나의 인터페이스를 통해 서로 다른 클래스의 객체들이 다양한 실행을 할 수 있는 것이 다형성의 핵심이야.