[Javascript] 객체 지향 (Object Oriented)의 핵심, JS의 다중 패러다임

저는 요즘 클린 아키텍처(Clean Architecture) 책을 읽고 있어요. 이 책에서는 ‘좋은 아키텍처를 만드는 일은 객체 지향 원칙을 이해하고, 응용하는 것에서 출발한다’ 라고 말합니다. 왜 그런 것일까요?

책에서는 객체 지향을 통해 ‘아키텍트는 플러그인 아키텍처를 구성할 수 있고, 이를 통해 고수준의 정책을 포함하는 모듈을 저수준의 세부사항을 포함하는 모듈에 대해 독립성을 보장할 수 있다. (중략) 독립적으로 개발하고 배포할 수 있다’ 라고 말해요. 음 도통 무슨말인지 모르겠네요🤣

객체 지향은 쉽게 말하면 '실제 세계를 모델링 함으로써 소프트웨어를 조금 더 쉽게 이해할 수 있는 패러다임' 이라 말할 수 있겠네요. 그러면 객체 지향의 핵심을 파악하며, 지금까지의 말이 어떤 의미인지 알아가 보도록 합시다!

아! 그리고 JS가 왜 다중 패러다임 언어인지도 같이 이해해 보도록 합시다!

❓캡슐화

캡슐화 라는 것은 쉽게 말한다면 정보를 은닉하는 것입니다. 우선 예를 통해 빠르게 알아보도록 합시다!

우선 객체 지향의 개념이니 만큼 Class를 사용한 구현 예시를 먼저 보겠습니다.

class Account {
  #balance = 0; // private 속성

  deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
      console.log(`${amount} deposited. New balance: ${this.#balance}`);
    }
  }

  withdraw(amount) {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount;
      console.log(`${amount} withdrawn. New balance: ${this.#balance}`);
    } else {
      console.log("Insufficient funds.");
    }
  }

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

우선 정보 은닉을 위해서 private 변수를 사용할 수 있습니다. JS 클래스에서는 #을 이용하여 선언할 수 있구요. 이렇게 되면 #balance 변수는 deposit, withdraw, getBalance 메서드로 간접적인 접근을 할 순 있겠으나, 직접적으로 접근할 순 없어요.

그리고 클래스가 아닌 함수를 이용해서 이 캡슐화를 만들 수도 있어요.

function createPerson(name) {
  let _name = name;

  return {
    getName: function() {
      return _name;
    },
    setName: function(newName) {
      _name = newName;
    }
  };
}
const person = createPerson("Lurgi");

console.log(person.getName()); // "Lurgi"
person.setName("jeongwoo");
console.log(person.getName()); // "jeongwoo"

함수 내부의 _name 은 캡슐화를 통한 정보 은닉에 성공한 상태입니다. 접근은 getName과 setName 을 통해서 간접적으로만 가능합니다! createPerson의 실행이 끝났음에도 _name 변수가 가비지 컬렉션의 대상이 되지 않은 이유는 getName setName이 _name 변수를 참조하고 있기 때문입니다! 이를 클로저(Closure)라고 하구요!

클로저를 이용하여 이렇게 함수를 이용한 캡슐화를 할 수 있습니다!

❓상속화

상속화는 말 그대로 다른 객체의 속성을 상속받는 행위를 뜻합니다. 이는 예시를 통해 빠르게 알아보도록 합시다!

// 수퍼 클래스
class Animal {
  constructor(name) {
    this.name = name;
  }

  getAnimalname() {
    return this.name;
  }

  makeSound() {
    console.log("Some sound");
  }
}

// 자식 클래스
class Dog extends Animal {
  constructor(name) {
    super(name);
  }

  makeSound() {
    console.log("Mung!");
  }
}

const dog = new Dog("Mott");

console.log(dog.getAnimalname()); // Mott
dog.makeSound(); // Mung!

이는 상속화를 class를 이용하여 구현한 모습입니다. Animal 클래스 객체의 메서드를 그대로 가져와 dog 클래스에서 사용할 수도 있고, 심지어 메서드를 덮어쓰는 것 역시 가능합니다. 덮어쓰는 것을 오버 라이딩(재정의) 라고 하구요! 이에 관해서는 이전에 포스팅한 글을 참고해주세요!

 

[개발 상식] 오버로딩과 오버라이딩(Overloading & Overriding)

오버로딩과 오버라이딩을 알아봅시다. 프론트엔드를 공부하기 때문에 타입스크립트를 기준으로 글을 적습니다. 우선 오버로딩과 오버라이딩을 알아보기 앞서 Call signature와 다형성(Polymorphism)

lurgi.tistory.com

그리고 이 상속화 역시 함수로 표현할 수 있습니다! 함수에서의 상속은 prototype 으로 구현할 수 있습니다.

// 수퍼 클래스
function Animal(name) {
  this.name = name;
}

Animal.prototype.getAnimalname = function () {
  return this.name;
};

Animal.prototype.makeSound = function () {
  console.log("Some sound");
};

// 자식 클래스
function Dog(name) {
  Animal.call(this, name);
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.makeSound = function () {
  console.log("Mung!");
};

const dog = new Dog("Mott");

console.log(dog.getAnimalname()); // Mott
dog.makeSound(); // Mung!

클래스 보다는 확실히 복잡해진 것 같아요.

Object.crate()를 이용하여 Dog 클래스의 프로토타입을 Animal 클래스의 프로토타입으로 설정하여 상속을 구현하였습니다! 그리고 함수의 call 메서드를 통해 함수 호출시 인자를 더해주는 역할을 해주었구요. 이처럼 함수를 이용해서도 상속화를 구현할 수 있다는 점!

❓다형성 (Polymorphism)

클린 아키텍처 책에서 O.O(객체 지향. Object Oriented)에서의 핵심은 다형성이라고 말합니다. 다형성이 무엇일까요?

다형성은 같은 이름의 메서드나 함수가 다른 상황에서 다르게 동작할 수 있는 능력을 의미합니다.

우선 예제를 통해 알아보도록 합시다!

function makeSound(animal) {
  return animal.sound();
}

const dog = {
  sound: function() {
    return "Mung!";
  }
};

const cat = {
  sound: function() {
    return "Meow!";
  }
};

console.log(makeSound(dog)); // "Mung!"
console.log(makeSound(cat)); // "Meow!
class Animal {
  sound() {
    return "sound";
  }
}

class Dog extends Animal {
  sound() {
    return "Mung!";
  }
}

class Cat extends Animal {
  sound() {
    return "Meow!";
  }
}

function makeSound(animal) {
  return animal.sound();
}

const dog = new Dog();
const cat = new Cat();

console.log(makeSound(dog)); // "Mung!"
console.log(makeSound(cat)); // "Meow!"

함수를 이용한 예제와, 클래스를 이용한 예제를 만들어 보았습니다. 함수에서는 makeSound 메서드 하나를 통해 두 개의 결과값을, 클래스 에서는 Animal 하나의 클래스에서 상속받은 메서드를 통해 두 개의 결과값을 가져올 수 있었습니다. 이렇듯 각기 다른 상황에서 다른 동작을 할 수 있는 것을 다형성이라고 합니다!

조금 더 설명드리기 위해 JS에서의 고차함수를 예시로 들 수 있겠네요!

const arr = [1,2,3,4,5]

console.log(arr.map((number)=> number+1)) // [2,3,4,5,6]
console.log(arr.map((number)=> number*2)) // [2,4,6,8,10]

이렇게 map 메서드는 인자로 받은 콜백 함수에 의존하는 고차함수 입니다. 이를 의존성 주입 (Dependency Injection) 이라고 표현합니다!

다형성과 의존성에 대해서는 다음번에 더 자세하게 다뤄보도록 하겠습니다!!

😊결론..!

JS는 ES6에서 본격적으로 클래스가 도입됨에 따라 클래스 기반의 객체 지향 프로그래밍을 지원하게 되었는데요. 이 때문에 요즘 많은 분들이 Class = 객체 지향 이란 생각을 가지고 계시는 것 같습니다. 그런 의미에서 이번 시간에는 클래스 예시와 함수 예시를 모두 보여드렸구요!

JS는 함수형과 객체지향 모두를 지원하는 다중 패러다임 프로그래밍입니다! 저는 개인적으로 좋은 프로그래밍을 하기 위해선 함수형과 객체지향의 장단점을 알고 적시에 사용할 줄 알아야 한다는 생각이 드네요. 이 둘의 균형을 잘 잡는 것도 중요하다 생각하구요. React가 아무리 함수 지향 라이브러리에 가깝다고 하더라도, 엄밀히 함수형만을 따르지는 않는다는 것! 그러니 우리 객체 지향의 개념을 공부하도록 합시다!