JS에서의 OOP

Raccoon·2025년 4월 22일
OOP의 네 가지 특성 출처: https://blog.algomaster.io/p/basic-oop-concepts-explained-with-code

OOP(Object Oriented Programming) 이란 무엇이고, JS에서 어떻게 객체지향을 구현할 수 있을까?

객체란?

객체란?

일단, 객체에 대해 알아보자.
객체 란 속성(데이터)과 기능(동작)을 가진 독립적인 단위를 말한다.
예를 들어, 강아지 라는 동물을 객체라고 했을 때, 이름 이라는 속성을 가지고, 짖기 라는 동작 을 한다는 걸 알 수 있다.

다시 말해서, 데이터와 그 데이터를 조작하는 기능까지 갖춘 구조체를 객체 라고 한다.

객체 지향이란?

객체 지향 이란, 프로그램 전체를 이런 객체들의 상호작용으로 구성하는 방식이다.
현실 세계처럼 여러 객체들이 소통하고 상호작용하여 문제를 해결하는 구조이다.
객체 지향은 누가 무엇을 할 것인가 에 초점을 맞추기 때문에, 각 객체가 자신의 역할과 책임을 가지고, 서로 메시지를 주고 받으면서 동작한다.

절차 지향이란?

객체 지향의 대조적인 개념인 절차 지향 이란 프로그램을 순서대로 실행되는 절차 중심으로 구성하는 방식이다.
절차 지향은 누가 무엇을 할 것인가 가 아닌, 어떻게 할 것인가 에 초점을 맞춘다.

아래는 객체 지향과 절차 지향의 차이를 간단한 예시로 나타낸 코드이다.

// 절차 지향
catBark(); 
dogBark();
wolfBark();

-------------
// 객체 지향
cat.bark();  
dog.bark(); 
wolf.bark();

그럼 절차 지향은 나쁜가?

당연하게도, 절차 지향만의 장점이 존재한다. 절차 지향은 단순하고 직관적인 흐름과 성능 최적화에 유리하기 때문에, 하드웨어와 밀접한 작업, 임베디드 시스템, 시스템 프로그래밍과 같은 분야에서 주로 사용된다. 절차 지향은 특히 성능이 중요한 환경에서 자주 사용된다. 결국 메모리 관리나 최적화가 중요한 상황에서는 절차 지향이 매우 효과적이다.

다시 말해, 절차 지향은 단순하고 성능 중심의 문제를 해결하는 데 적합하고, 객체 지향은 유지보수성, 확장성, 재사용성이 중요한 대규모 시스템에 적합한 것이다. 사용되는 분야와 목적이 다를 뿐이다.

왜 OOP를 써야하는가?

그렇다면, 왜 OOP를 써야할까?

코드로 이해해보자

객체 지향에서는 상속 이라는 개념이 존재한다.
상속공통된 기능은 재사용하고, 새로운 기능은 확장할 수 있게끔 하는 개념으로, 코드의 중복을 줄이고, 기존 코드는 절대 수정할 필요가 없이 새로운 객체를 추가할 수 있다.

// 절차지향

catBark() {
 console.log('고양이가 소리를 낸다. 야옹 야옹!'); 
}

dogBark() {
  console.log('강아지가 소리를 낸다. 멍멍!');
}

wolfBark() {
  console.log('나는 늑대야. 아우우~~');
}

wolfBite() {
	console.log('나는 무니까 조심해');
}
--------------------------------------------------
// 객체 지향

class Animal {
  constructor(name){
    this.name = name;
  }
  
  bark(val) {
	console.log(`${this.name}가 소리를 낸다. ${val}`);
  }
}

class Cat extends Animal {
}

class Dog extends Animal {
}

class Wolf extends Animal {
 	bark() {
      console.log(`나는 ${this.name}야. 아우우~~`);
    }
  
  	bite() {
      console.log('나는 무니까 조심해');
    }
}
const dog = new Dog('강아지');
const cat = new Cat('고양이');
const wolf = new Wolf('늑대');

dog.bark('멍멍!');
cat.bark('야옹 야옹!');
wolf.bark();
wolf.bite();

재사용성

객체 지향에서는 Cat Dog Wolf 클래스는 똑같이 bark() 라는 메서드를 가지고 있지만, 중복되는 부분은 재사용하고 있다.
반면에, 절차 지향에서는 같은 내용이지만 catBark() dogBark() wolfBark() 와 같이 다른 이름의 함수로 정의를 하고 있는 것을 알 수 있다.

이렇듯, 객체 지향을 통해 새로운 클래스를 추가할 때, 기존 코드를 변경하지 않고 새로운 클래스를 재사용하여 간편하게 추가할 수 있다.

유지보수성

만약, catBark() dogBark() 의 메서드에서, ~가 소리를 낸다 -> ~는 소리를 낸다 의 형식으로 바꿔야 하는 상황을 생각해보자.
그리고, 이 메서드는 1억개가 있다고 가정하자.

절차 지향으로 구현된 이 메서드들을 바꾸기 위해서는, 1억 번의 수정 작업이 필요할 것이다.
이 번거로운 과정 속에서, 오타가 발생할 수도 있고, 빠뜨린 메서드가 있을 수 있다.

하지만, 객체 지향으로 구현된 이 메서드들을 바꾸기 위해서는, 부모 클래스인 Animal 만 단 한번 수정하면 된다.

극단적인 예시이긴 하지만, 이렇게 비교하니 얼마나 객체 지향이 유지보수성 측면에서 뛰어난지 와닿는다.

확장성

만약 새로운 메서드를 추가하는 상황이라고 가정해보자.
run() 이라는 메서드를 추가하려면, 절차 지향에서도 catRun() dogRun() 과 같이 추가할 수 있다.

하지만, 객체 지향에서는 상속과 오버라이드를 통해, 새로운 메서드를 추가하는 방식이 규칙적이고 일관성 있는 확장을 가능하게 만들어, 코드의 가독성과 유지보수성을 크게 향상시킨다.

객체 지향의 특성

객체 지향의 특성은 다음과 같다.

  1. 캡슐화(Encapsulation)
  2. 상속(Inheritance)
  3. 다형성(Polymorphism)
  4. 추상화(Abstraction)

1. 캡슐화(Encapsulation)

캡슐화는 객체의 데이터를 보호하고, 외부에서 직접 접근하지 못하게 한다.

예시 코드

class People {
  #age;
  
  constructor(age) {
    this.setAge(age);
  }
  
  setAge(age) {
    this.#age = age;
  }

  getAge(age) {
    return this.#age;
  }
}

캡슐화의 장점

위의 코드를 보면, setAge() 를 통해 나이를 설정할 수 있고, getAge() 를 통해 나이 정보를 얻을 수 있다.

이렇게 데이터를 보호하고, 외부에서 직접 접근하지 못하게 하면, 어떤 이점이 있을까?

데이터 무결성 보호

데이터의 무결성 이란 데이터가 정확하고 일관되며 신뢰할 수 있는 상태로 유지되는 것 을 의미한다.
setAge() 함수를 살짝 변형해보겠다.

...
setAge(age) {
 	if(typeof(age) !== 'number' || age < 0) throw new Error("나이는 0보다 작을 수 없습니다");
  	this.#age = age;
}

만약, 이런식으로 유효성 검사를 추가한다면 나이 값에 0보다 작은 값이 들어가는 것을 방지해줄 수 있다.

캡슐화의 단점

모든 속성에 대해 getter와 setter을 작성하다보면, 당연하게도 코드의 길이가 비약적으로 늘어날 것이다.
또한, 테스트 코드에서 내부 상태를 확인하고 싶을 때, 직접 접근이 제한되어 우회하여 접근하는 것도 번거로울 수 있다.

2. 상속(Inheritance)

상속 이란 부모 클래스의 속성과 메서드를 자식 클래스가 물려받아 재사용하는 특성이다.
위에서 살펴봤으니 넘어가겠다.

3. 다형성(Polymorphism)

다형성은 같은 이름의 메서드가 서로 다른 동작을 수행할 수 있는 특성이다.
원래라면 다형성은 오버라이딩오버로딩 을 통해 구현된다.
오버라이딩 은 상속 받은 메서드를 재정의하는 방식이고,
오버로딩 은 같은 메서드 이름으로 다른 매개변수를 받는 방식이다.

하지만 JS에서는 오버로딩 을 지원하지 않는다.
다른 언어에서는 인수의 개수에 따라 여러 메서드를 정의할 수 있지만, JS에서는 여러 메서드를 정의하면, 가장 나중에 정의한 메서드에 덮어 씌워지기 때문이다.

물론, 비슷하게 구현하는 방법은 존재한다.

다형성의 장점도 위에서 살펴보았으니 넘어가겠다.

4. 추상화(Abstraction)

추상화 란 복잡한 구현을 숨기고, 중요한 부분만을 드러내어 단순화하는 특성이다.
사용자는 클래스 내 메서드를 통해, 내부 로직을 알 필요 없이 동작을 수행할 수 있다.

혹은 아래와 같이 추상 클래스를 구현하도록 할 수도 있다.

class Car { // 추상 클래스
  startEngine() {
    throw new Error("startEngine() 메서드는 서브 클래스에서 구현해야 합니다.");
  }

  stopEngine() {
    throw new Error("stopEngine() 메서드는 서브 클래스에서 구현해야 합니다.");
  }
}

class ElectricCar extends Car {
  startEngine() {
    console.log("전기차 엔진을 시작합니다.");
  }

  stopEngine() {
    console.log("전기차 엔진을 멈춥니다.");
  }
}

OOP in JS

JS는 원래 프로토타입 기반(prototype-based) 언어로 설계되었다.
즉, 클래스 개념이 존재하지 않았고, 객체 간의 상속은 프로토타입을 이용하여 구현되었다.
ES6 부터 클래스 개념이 도입되었고, 이를 통해 OOP를 더 직관적으로 구현할 수 있게 된 것이다.

상속

상속은 프로토타입을 통해 아래와 같이 구현될 수 있다.

function Animal(name) {
  this.name = name;
  
  this.bark = function() {
    console.log(`${this.name}는 짖는다`);
  }
}

function Dog(name) {
  Animal.apply(this, [name]); // 생성자 및 프로퍼티 상속
}

Dog.prototype = Object.create(Animal.prototype); // 메서드 상속
Dog.prototype.constructor = Dog; // 위 설정으로 인해, 생성자가 Animal. 따라서 생성자를 Dog로 변경해줘야 함 .

캡슐화

위에서 설명했듯이, 나이 라는 속성에, 잘못된 값이 들어가지 않도록 유효성 검사를 해줄 수 있다.

var person = {
  age : 20,
};

person.age = -20;

// 위와 같은 상황을 방지해야 함!!

var person = {
  age : 20,
  setAge : function(newAge) {
    if(typeof(newAge) !== "number" || newAge < 0) {
      throw new Age("Invalid Age");
    } else {
      this.age = newAge;
    }
  },
  getAge: function() {
    return this.age;
  }
};

다형성과 추상화는 사실상 상속과 캡슐화의 변형으로 만들어지는 것이기 때문에, 코드는 생략하도록 하겠다.

JS에서의 OOP

ES2015 이전에는 클래스 개념이 존재하지 않았다.
그렇다면 어떻게 객체를 정의하고 인스턴스를 만들었는가?

가장 처음에는 객체 리터럴 방식 이 존재했다.

Object Literal Pattern(객체 리터럴)

const healthObj = {
  name: "달리기",
  lastTime: "PM10:12",
  showHealth() {
    console.log(
      this.name + "님, 오늘은 " + this.lastTime + "에 운동을 하셨네요"
    );
  },
};

healthObj.showHealth();

여러 인스턴스를 생성할 필요가 없고, 단일 인스턴스(1회용 객체)로 사용하기 좋다.

하지만 만약 여러 개의 비슷한 객체를 만들어야 한다면,
객체 리터럴을 반복 선언할 경우 메서드가 각 객체에 중복 정의되어 메모리 비효율과 코드 관리의 어려움이 발생할 수 있다.
이때부터는 생성자 함수, 프로토타입 패턴, 혹은 ES6 클래스 같은 인스턴스 생성 방식이 필요하다.

ES6 Class Pattern(ES2015 이후 도입)

class Health {
  constructor(name, healthTime) {
    this.name = name;
    this.healthTime = healthTime;
  }

  showHealth() {
    console.log(
      this.name + "님, 오늘은 " + this.healthTime + "에 운동을 하셨네요"
    );
  }
}

const ho = new Health("crong", "12:12");
ho.showHealth();

내부적으로는 여전히 prototype 기반으로 작동하고, 가독성이 좋아 클래스 상속, 추상화 등 OOP 개념을 사용하기 편리하다는 장점이 있다.

Constructor Function Pattern

function Health(name, healthTime) {
  this.name = name;
  this.healthTime = healthTime;
  this.showHealth = function () {
    console.log(
      this.name + "님, 오늘은 " + this.healthTime + "에 운동을 하셨네요"
    );
  };
}

const ho = new Health("crong", "12:12");
ho.showHealth();

function을 생성자로 사용하는 방식이다.

new 키워드로 호출하면 this가 새로운 인스턴스를 가리키기 때문에 가능한 방식이다.

하지만 모든 인스턴스가 showHealth 메서드를 개별적으로 갖기 때문에 메모리 비효율적이다.

const ho1 = new Health("crong", "12:12");
const ho2 = new Health("babo", "10:00");

console.log(ho1.showHealth === ho2.showHealth); // false

ho1.showHealthho2.showHealth 는 다른 함수 객체이다.

즉, 인스턴스를 100개 만들면 showHealth 함수도 100개가 생긴다는 의미이다.

prototype 기반일 경우, 인스턴스를 1억개 만들어도 showHealth 함수는 1개이기 때문에, 메모리 효율성에서 차이가 난다.

Prototype Pattern

공통 메서드는 prototype에 보관하여 메모리를 절약하는 방식이다.

function Health(name, healthTime) {
  this.name = name;
  this.healthTime = healthTime;
}

Health.prototype.showHealth = function () {
  console.log(
    this.name + "님, 오늘은 " + this.healthTime + "에 운동을 하셨네요"
  );
};

const ho = new Health("crong", "12:12");
ho.showHealth();

모든 인스턴스가 prototype 의 메서드를 공유하기 때문에, 메모리를 절약할 수 있다.

Class Pattern vs Prototype Pattern

JavaScript의 ES6 클래스 패턴과 기존 프로토타입 패턴은 내부적으로 동일한 프로토타입 기반 상속을 사용한다.

하지만 문법과 사용성 측면에서 차이가 있다.

  • 프로토타입 패턴은 생성자 함수와 prototype 객체를 직접 다루는 방식으로,
    복잡할 수 있지만 동작 원리를 명확히 이해하기에 좋다.

  • 클래스 패턴은 문법적 설탕(Syntactic Sugar)으로,
    코드가 간결하고 가독성이 좋아 유지보수와 확장성이 뛰어나다.

두 패턴 모두 공통 메서드는 프로토타입에 한 번만 존재하여,
수많은 인스턴스를 생성해도 메모리 효율이 뛰어나다는 장점이 있다.

문법적 설탕(Syntatic Sugar)

문법적 설탕 이란 기존에 가능했던 기능을 더 보기 좋고, 쓰기 쉽게 만들어주는 문법적 편의 기능 이다.

즉, 똑같은 일을 하지만, 코드를 더 간결하고 직관적으로 쓸 수 있게 맛있게 만들어 놓은 문법이다.

그래서 설탕 이라는 표현이 들어갔다고 한다.

profile
꾸준함을 목표로 합니다.

0개의 댓글