JavaScript

JavaScript로 진정한 객체지향(Object-Oriented Programming)하기

남희정 2025. 1. 19. 20:51

📌 목차

Javascript로 진정한 객체지향이 가능할까? 사실 어느 정도 도발이지만 스스로 이 간극을 탐구해보고 싶었다. 그리고 개인적인 욕심으로는 어렵기보다 쉽게 설명하고 싶다. Javascript와 객체지향 프로그래밍의 간극을 좁혀보고 진정한 객체지향이란 무엇인지 쉽고 편안하게 탐구하고자 한다!

 

객체지향 프로그래밍(Object-Oriented Programming)이란?

객체지향 프로그래밍은 프로그래밍의 패러다임 중 하나이다. 프로그램을 명령어의 목록으로 보는 시각에서 벗어나, 여러 개의 독립된 단위. 즉, "객체"들의 모임으로 파악하고자 하는 것이다.

 

이는 Wiki에서 정의한 객체지향 프로그래밍의 개념이다. 여기서 '프로그램을 명령어의 목록으로 보는 시각'은 절차적 프로그래밍(Procedural programming)을 지칭하는 표현이다. 

 

절차적 프로그래밍(Procedural Programming)

절차적 프로그래밍은 데이터를 처리하는 동작을 함수 단위로 분리하고 순서대로 실행하는 데에 초점을 맞춘다. (예: 고객 정보를 처리한다면 데이터를 전달받아, 저장, 처리, 출력 순으로 코드를 작성하고 각 동작을 함수로 나누어 호출한다.) 프로그램의 아무 위치에서나 함수를 호출할 수 있는데 다른 함수에서도 호출이 가능하고 심지어 자기 자신에서도 호출이 가능하다. 컴퓨터의 처리구조와 유사해서 실행 속도가 빠르다는 장점이 있지만 규모가 커지면 유지보수가 어려워진다는 단점이 있다. 특히 데이터와 동작이 긴밀하게 연결되어 있다는 건 의존성이 높다는 것이며 수정 시 그만큼 프로그램 전체에 영향을 미칠 가능성이 크다.

 

⚠️ 참고로 절차적 프로그래밍 !== 순차적 프로그래밍 단순히 코드가 Top-Bottom으로 한 줄씩 실행되는 순차적 프로그래밍과는 다르다!

이 차이에 대해 더 알고 싶다면 이전 포스팅을 참고하면 좋다.

 

2023.09.09 - [Software Engineering] - Imperative 명령형, Declarative Programming 선언형 프로그래밍

 

절차적 프로그래밍은 단순하고 효율적인 접근 방식을 제공했지만 점점 복잡해지는 소프트웨어를 효율적으로 관리하기엔 확장성, 유지보수성에 한계가 있었다. 이 문제를 해결하고자 등장한 것이 바로 객체지향 프로그래밍이다.

도식화 해본 절차적 프로그래밍 vs 객체지향 프로그래밍

 

 

객체지향의 본질 & 핵심원칙 4가지

객체지향이 왜 등장했는지는 이제 알 것 같다. 그럼, 진정한 객체지향이란 무엇일까? 객체지향에 대해 조금만 더 들여다보자.

 

객체지향은 데이터를 중심으로 한 프로그램 설계 방식이다. 데이터와 이를 처리하는 동작이 하나의 모듈로 관리되며, 작은 부품들이 독립적으로 동작할 수 있는 구조를 갖춘다. 이러한 부품들은 미리 설계된 객체들로 구성되며, 조립하고 결합하는 방식으로 개발의 유연성과 확장성을 극대화할 수 있다.

 

클래스 및 인스턴스

이제 객체를 만들어보면서 객체지향의 핵심원칙을 하나씩 알아보려 한다. 객체지향에서 클래스 Class는 공통된 특성과 동작을 정의하는 설계도 역할을 한다. 그리고 이 설계도를 바탕으로 만들어진 실제 객체를 인스턴스 Instance 라고 한다.

 

예를 들어, 대학교를 모델링한다고 했을 때 학교에 많은 교수님이 있다. 모든 교수님은 이름과 가르치는 수업이 있을 것이고 학생들한테 자신의 수업을 소개하거나 시험을 출제하는 등의 행동을 할 수 있다. 이때 Professor 클래스를 만든다. 교수님들이 공통적으로 가지는 속성과 행동을 정의하는 것이다. 이후에 "금교수님", "남교수님"처럼 교수님을 new 키워드를 통해 실제로 만들고 프로세스에 사용하는 것이 바로 인스턴스를 생성하는 과정이다. 

 

// Professor 클래스
class Professor {
    constructor(name, class) {
        this.name = name; // 교수의 이름
        this.class = class; // 교수의 수업
    }
    
    introduce() {
        console.log(`안녕하세요, 저는 ${this.name} 교수이고, ${this.class}수업을 담당해요.`);
    }
    
    gradePapers() {
        console.log(`${this.name} 교수가 논문을 채점합니다.`);
    }
}

// Professor 클래스로 인스턴스 만들기
const professorNam = new Professor("남희정", "알고리즘");
const professorKeum = new Professor("금소현", "서양화 실기");

// 각 인스턴스에서 메서드 호출
professorNam.introduce(); // "안녕하세요, 저는 남희정 교수이고, 알고리즘 수업을 담당해요."
professorKeum.introduce(); // "안녕하세요, 저는 금소현 교수이고, 서양화 실기 수업을 담당해요."

 

 

 

1️⃣ 상속 Inheritance

상속은 말 그대로 부모 클래스의 속성과 메서드를 자식 클래스가 물려받는 것이다. 상속을 통해 중복 코드를 줄이고, 공통적인 로직은 부모 클래스에서 처리하게 두어 재사용성을 높일 수 있다.

 

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

    speak() {
        console.log(`${this.name}가 소리를 냅니다.`);
    }
}

// 자식 클래스
class Dog extends Animal {
    speak() {
        console.log(`${this.name}가 멍멍! 짖습니다.`);
    }
}

const dog = new Dog("흰둥이");
dog.speak(); // "흰둥이가 멍멍! 짖습니다."

 

2️⃣ 캡슐화 Encapsulation

중요한 정보는 숨기고 꼭 필요한 기능만 외부에 보여주는 것. 객체가 내부적으로 어떤 데이터를 가지고 있든, 그 데이터는 객체가 제공하는 메서드를 통해서만 접근할 수 있다. 내부 상태를 보호할 수 있고 내부 상태가 어떻게 바뀌든 외부 코드에 영향을 주지 않아 좋다.

 

class Student {
    constructor(name, year) {
        this.name = name; // 학생 이름
        this.#year = year; // 학년 (# => 비공개 속성)
    }

    // 학생이 양궁 수업을 들을 수 있는지 확인할 수 있는 메서드
    canTakeArchery() {
        return this.#year >= 2;
    }
}

const student1 = new Student("희정", 2);
const student2 = new Student("소현", 1);

console.log(student1.canTakeArchery()); // true
console.log(student2.canTakeArchery()); // false

 

3️⃣ 추상화 Abstraction

구체적인 세부사항은 숨기고 공통적이고 본질적인 특징에 집중하는 것이다. 예를 들어 사각형, 원형 클래스가 있는 경우 형태라는 추상 클래스를 만들 수 있겠다. 

 

// 추상 클래스
class Shape {
    constructor(name) {
        this.name = name;
    }

    // 추상 메서드: 구현은 자식 클래스에서
    calculateArea() {
        throw new Error("이 메서드는 서브클래스에서 구현해야 합니다.");
    }
}

// 구체적인 클래스
class Rectangle extends Shape {
    constructor(width, height) {
        super("Rectangle");
        this.width = width;
        this.height = height;
    }

    calculateArea() {
        return this.width * this.height;
    }
}

class Circle extends Shape {
    constructor(radius) {
        super("Circle");
        this.radius = radius;
    }

    calculateArea() {
        return Math.PI * this.radius ** 2;
    }
}

const shapes = [
    new Rectangle(10, 5),
    new Circle(7),
];

shapes.forEach(shape => {
    console.log(`${shape.name}의 면적: ${shape.calculateArea()}`);
});
// "Rectangle의 면적: 50"
// "Circle의 면적: 153.938..."

 

4️⃣ 다형성 Polymorphism

같은 이름의 메서드가 객체에 따라 다르게 동작할 수 있는 것. 주로 상속과 함께 사용된다. 

 

class Animal {
    speak() {
        console.log("동물이 소리를 낸다.");
    }
}

class Dog extends Animal {
    speak() {
        console.log("멍멍!");
    }
}

class Cat extends Animal {
    speak() {
        console.log("야옹!");
    }
}

const animals = [new Dog(), new Cat()];

animals.forEach(animal => {
    animal.speak();
});

// "멍멍!"
// "야옹!"

 

Prototype기반 객체지향 프로그래밍 하기

네 가지 원칙을 알아보았는데 의문이 생길 수 있다. "진정한 객체지향이라는 말이 왜 나온 거지?", "JavaScript로 못할 게 대체 뭔데?".. JavaScript는 익숙하게 생각하는 클래스 기반 객체지향 언어 Java, C++ 등과는 다르다. JavaScript는 프로토타입 기반 언어로 설계되었기 때문이다. 😮

 

JavaScript에서 클래스와 프로토타입의 관계

JavaScript는 ES6이후 class 문법이 도입되었지만 이것은 MDN에도 나와있듯이 Java나 C++을 사용하던 개발자들에게 친숙한 문법을 제공하기 위한 *Syntatic Sugar일 뿐 내부적으로는 프로토타입을 사용해 동작한다.

(관련해서 보면 좋을 글을 슬쩍 공유해본다.)

 

*Syntactic Sugar: 기능은 동일하지만 사용하기 편리하도록 설계된 문법을 의미. 

 

ES6 Class는 단지 prototype 상속의 문법설탕일 뿐인가?

최근 모 커뮤니티에서 ‘ES6를 공부해야 할 필요는 없다’는 의견을 발견하였는데, 그 분의 견해를 읽던 중 ‘Class는 문법설탕일 뿐’이라는 내용에 대해서 정말로 그러한지 알아보고 싶은 마음

roy-jung.github.io

특징 클래스 기반 OOP (전통적 OOP) 프로토타입 기반 OOP
객체 생성 클래스 정의 후 인스턴스 생성 객체 생성 후 다른 객체와 연결
상속 상속 계층 구조 프로토타입 체인(위임)
클래스와 객체의 관계 클래스와 객체가 명확하게 구분 모든 객체가 다른 객체에서 파생
유연성 상대적으로 고정적 동적으로 위임 관계 변경 가능

 

객체 생성하기

생성자 함수

생성자 Constructor는 객체의 모양과 속성을 정의하는 함수이다. new 키워드로 호출되며, 객체를 초기화한다. 생성자의 프로토타입 속성에 메서드를 정의하면 생성자로 생성된 모든 객체가 해당 메서드를 공유한다. 

 

// 생성자 함수
function Box(value) {
  this.value = value;
}

// Box() 생성자에서 생성된 모든 속성
Box.prototype.getValue = function () {
  return this.value;
};

const boxes = [new Box(1), new Box(2), new Box(3)];

 

Object.create()

프로토타입을 직접 지정하여 객체를 생성하는 가장 간단한 방법 중 하나. 생성자 함수 없이 프로토타입을 명시적으로 지정할 수 있고 간단한 객체 생성 및 상속에 유용하다. 

const prototype = {
    introduce() {
        console.log(`안녕하세요, 제 이름은 ${this.name}입니다.`);
    },
};

const student = Object.create(prototype); // prototype과 연결된 새 객체 생성
student.name = "희정";
console.log(student.name); // "희정"
student.introduce(); // "안녕하세요, 제 이름은 희정입니다."

 

이외에도 여러 가지가 있으니 아래에 링크를 공유해 본다.

 

상속과 프로토타입 - JavaScript | MDN

JavaScript는 동적 타입이고 정적 타입이 없기 때문에, (Java 또는 C++와 같은) 클래스 기반 언어에 경험이 있는 개발자에게는 약간 혼란스럽습니다.

developer.mozilla.org

1️⃣ 상속? No. 위임 Yes

전통적인 상속은 클래스의 속성과 메서드를 복제해서 자식 클래스에 전달하는 방식이고 자식은 부모의 기능을 물려받은 것처럼 독립적으로 사용된다. JavaScript의 프로토타입 기반 상속은 전통적인 상속 개념이랑은 다르다. 오히려 위임 Delegation에 가깝다. 객체가 특정 속성이나 메서드를 요청할 때 자신이 처리하지 못하면 연결된 프로토타입 객체에 그 일을 위임한다. 객체 간 연결을 통해 기능을 공유할 뿐, 복제는 일어나지 않는다. 이를 프로토타입 체인 Prototype Chain이라 하며 속성을 찾을 때 이 체인을 따라 검색이 이루어진다.

 

const parent = {
    greet() {
        console.log("안녕하세요! 저는 부모 객체입니다.");
    }
};

// child 객체를 생성하고 parent를 프로토타입으로 설정
const child = Object.create(parent);
child.sayHello = function () {
    console.log("안녕하세요! 저는 자식 객체입니다.");
};

child.sayHello(); // "안녕하세요! 저는 자식 객체입니다."
child.greet();    // "안녕하세요! 저는 부모 객체입니다."

 

위 코드를 보면 sayHello 메서드의 경우 child 객체가 갖고 있지만 greet 메서드는 child 객체엔 없다. 하지만 parent로부터 위임받아 실행된다. 즉, 직접 메서드를 복사하지 않은 상태지만 연결된 부모 객체의 메서드를 참조해서 실행한다는 점이 상속보다 위임에 좀 더 가깝게 느껴진다.

 

이런 내부적인 차이가 있더라도, 캡슐화나 추상화 등 객체지향의 원칙들은 전부 JavaScript만의 방식으로 구현할 수 있다. (문법적 설탕이지만 클래스 사용도 가능)

 

2️⃣ 캡슐화 Yes. with closure

캡슐화의 경우 클로저를 사용해서 비공개 상태를 유지할 수 있다.

function BankAccount(owner, initialBalance) {
    let balance = initialBalance; // 비공개 변수 (클로저)

    this.owner = owner;

    this.deposit = function (amount) {
        if (amount > 0) {
            balance += amount;
            console.log(`${amount}원이 입금되었습니다.`);
        }
    };

    this.getBalance = function () {
        return `${owner}님의 잔액은 ${balance}원입니다.`;
    };
}

const account = new BankAccount("희정", 1000);
account.deposit(500);
console.log(account.getBalance()); // "희정님의 잔액은 1500원입니다."
console.log(account.balance); // undefined (비공개)

3️⃣ 추상화 Yes.

function Shape(name) {
    this.name = name;
}

Shape.prototype.calculateArea = function () {
    throw new Error("calculateArea 메서드는 반드시 구현해야 합니다.");
};

Shape.prototype.describe = function () {
    console.log(`${this.name}의 면적은 ${this.calculateArea()}입니다.`);
};

function Rectangle(width, height) {
    Shape.call(this, "사각형");
    this.width = width;
    this.height = height;
}

// 상속 (프로토타입 연결)
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

// 추상 메서드 구현
Rectangle.prototype.calculateArea = function () {
    return this.width * this.height;
};

const rect = new Rectangle(10, 5);
rect.describe(); // "사각형의 면적은 50입니다."

4️⃣ 다형성 Yes.

function Animal() {}

Animal.prototype.speak = function () {
    console.log("동물이 소리를 냅니다.");
};

function Dog(name) {
    this.name = name;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function () {
    console.log(`${this.name}가 멍멍 짖습니다.`);
};

function Cat(name) {
    this.name = name;
}
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
Cat.prototype.speak = function () {
    console.log(`${this.name}가 야옹 합니다.`);
};

const animals = [new Dog("흰둥이"), new Cat("야옹이")];
animals.forEach(animal => animal.speak());
// "흰둥이가 멍멍 짖습니다."
// "야옹이가 야옹 합니다."

 

결국 진정한 객체지향은 언어의 특성만으로 규정되지 않는다. 문제를 객체로 바라보고 구조화하려는 사고방식이 핵심인 것 같다. JavaScript는 완벽한 객체지향 언어는 아니지만, 진정한 객체지향을 실현할 수 있는 충분한 가능성을 가진 언어이다. 새삼 JavaScript의 유연함을 또다시 되새겨보게 되었다.

 


https://yozm.wishket.com/magazine/detail/1396/

https://roy-jung.github.io/161007_is-class-only-a-syntactic-sugar/

https://pozafly.github.io/javascript/prototype/

https://parksb.github.io/article/1.html

https://medium.com/@limsungmook/자바스크립트는-왜-프로토타입을-선택했을까-997f985adb42

https://ko.wikipedia.org/wiki/객체_지향_프로그래밍

https://developer.mozilla.org/ko/docs/Learn_web_development/Extensions/Advanced_JavaScript_objects/Object-oriented_programming

https://repository.unikom.ac.id/50991/1/2 Class and Object.pdf  

ChatGPT 🤖