호진방 블로그
학습

객체지향 설계의 모든것

# 자바스크립트에서의 객체지향 설계를 이해하고 각 핵심 개념을 이해해봅시다.

2024년 10월 19일

학습 배경

취업 준비 과정에서 다양한 기업의 면접을 경험하면서, 객체지향 설계의 원리와 그 의미에 대한 질문을 자주 받았습니다. 답변을 막힘없이 이어갈 수는 있었지만, 간결하고 명확하게 설명할 수 있는 깊이 있는 이해가 부족하다고 느꼈습니다. 이에 대한 학습의 필요성을 느끼게 되었고, 이번 포스팅을 통해 객체지향 설계에 대한 체계적인 학습과 더불어 그 의미를 명확히 이해하고자 합니다.


객체지향 설계 OOP 란

객체지향 프로그래밍(OOP)은 데이터를 중심으로 그 데이터를 처리하는 방법을 객체로 캡슐화하여 소프트웨어의 재사용성, 확장성, 유지보수성을 높이는 기법입니다. 자바스크립트는 프로토타입 기반의 언어로 이러한 객체 지향 프로그래밍을 지원합니다. OOP의 가장 큰 장점은 코드의 중복을 최소화하고, 모듈화를 통해 프로젝트의 복잡성을 효과적으로 관리할 수 있다는 점입니다.

자바스크립트에서는 클래스, 상속, 캡슐화, 다형성과 같은 OOP의 핵심 개념이 활용됩니다. 특히, ES6부터는 클래스 문법이 도입되어, 전통적인 객체 지향 프로그래밍 언어에서 사용하는 클래스 기반의 구조를 손쉽게 구현할 수 있게 되었습니다. 이를 통해 더 직관적이고 명확한 객체 지향 설계가 가능해졌습니다. ES6의 클래스 문법은 개발자들이 객체 지향 설계를 보다 쉽게 이해하고 적용할 수 있도록 도와줍니다.

OOP를 활용함으로써 코드의 구조를 더욱 체계적이고 논리적으로 설계할 수 있으며, 이는 곧 소프트웨어의 품질 향상으로 이어집니다. 이러한 객체 중심의 사고방식은 복잡한 문제를 해결하는 데 큰 도움을 줄 수 있습니다.


클래스와 인스턴스

클래스는 한마디로 객체를 생성하기 위한 템플릿입니다. 클래스는 데이터와 그 데이터를 처리하는 메소드를 포함하며, 이 클래스로부터 생성된 객체를 인스턴스라고 부릅니다. 쉽게 말해, 클래스는 일종의 '붕어빵 틀' 같은 역할을 하며, 이 틀을 통해 여러 객체를 생성해낼 수 있습니다.

❗️객체(Object)란 여러 속성을 하나의 단위로 묶은 데이터 타입입니다. 각 속성은 key와 value 형태로 저장됩니다.


자바스크립트에서 클래스를 사용하여 객체를 생성하는 방법을 아래 예시를 통해 살펴보겠습니다

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
 
  greet() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }
}
 
// Person 클래스로부터 새로운 인스턴스를 생성합니다.
const person1 = new Person('John', 30);
person1.greet(); // "Hello, my name is John and I am 30 years old."
 
// 또 다른 인스턴스를 생성할 수 있습니다.
const person2 = new Person('Jane', 25);
person2.greet(); // "Hello, my name is Jane and I am 25 years old."

위 코드에서 Person 클래스는 nameage라는 두 개의 속성을 가지며, greet라는 메소드를 통해 객체의 정보를 출력할 수 있습니다. person1person2는 각각 클래스로부터 생성된 인스턴스이며, 각 인스턴스는 고유의 nameage를 가집니다.

이처럼 클래스를 통해 같은 형식의 여러 객체를 효과적으로 생성할 수 있으며, 코드의 재사용성을 높일 수 있습니다. 클래스 기반의 설계는 유지보수가 용이하고, 확장이 쉬운 코드를 작성하는데 큰 도움이 됩니다.


객체지향 프로그래밍의 특징, 상속

상속은 객체지향 프로그래밍에서 코드의 재사용성을 높이기 위해 상위 클래스의 속성(변수)과 메소드(기능)를 하위 클래스가 물려받는 것을 의미합니다. 상속을 통해 공통된 기능과 속성을 다양한 클래스에서 사용할 수 있게 되어, 코드의 중복을 줄이고 유지보수를 향상 시킬 수 있습니다.

예를 들어, 우리가 '인간'이라는 클래스가 이미 존재한다고 가정해보겠습니다. 이 '인간' 클래스의 속성과 메소드를 활용하여 '축구선수'라는 클래스를 만들 수 있습니다. "공통적인 부분은 최대한 상속해서 쓰자"가 상속의 핵심 모토입니다. 자식 클래스는 부모 클래스를 상속받아 부모의 속성과 메소드를 그대로 사용하거나, 필요에 따라 오버라이딩(Overriding)하여 독자적인 기능을 추가할 수 있습니다.

자바스크립트에서 상속을 구현하는 방법을 아래와 같이 예시로 들어보겠습니다

// 부모 클래스
class Human {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
 
  speak() {
    console.log(`Hi, I am ${this.name} and I am ${this.age} years old.`);
  }
}
 
// 자식 클래스
class SoccerPlayer extends Human {
  constructor(name, age, position) {
    // 부모 클래스의 생성자를 호출하여 상속된 속성 초기화
    super(name, age);
    this.position = position;
  }
 
  play() {
    console.log(`${this.name} plays in the ${this.position} position.`);
  }
}
 
// Human 클래스를 상속받아 SoccerPlayer 인스턴스를 생성합니다.
const player1 = new SoccerPlayer('David', 28, 'forward');
player1.speak(); // "Hi, I am David and I am 28 years old."
player1.play(); // "David plays in the forward position."

위의 예시에서 SoccerPlayerHuman 클래스를 상속받고 있으며, nameage 속성은 Human 클래스에서 물려받습니다. SoccerPlayer 클래스만의 추가적인 속성인 position과 메소드 play는 별도로 정의되었습니다. super() 키워드는 부모 클래스의 생성자를 호출하여 상속받은 속성들을 초기화하는 데 사용됩니다.

오버라이딩(Overriding)

오버라이딩은 자식 클래스에서 부모 클래스에 정의된 메소드를 재정의하여 사용하는 것을 말합니다. 이는 상속 관계에서 주로 활용되며, 부모 클래스의 기본 동작을 자식 클래스에 맞게 변경할 수 있습니다. 오버라이딩 된 메소드는 같은 이름과 매개변수를 가지며, 자식 클래스의 인스턴스에서 호출될 때 부모 클래스의 메소드를 대신하여 실행됩니다.

class Animal {
  speak() {
    console.log('Animal makes a sound');
  }
}
 
class Dog extends Animal {
  speak() {
    console.log('Woof! Woof!');
  }
}
 
const myDog = new Dog();
myDog.speak(); // "Woof! Woof!"

오버로딩(Overloading)

오버로딩은 같은 이름을 가진 메소드를 여러 개 정의하고, 매개변수의 타입이나 개수를 다르게 함으로써 각각 다른 기능을 수행하도록 하는 것입니다. 주의할 점은, 자바스크립트는 원래 함수 오버로딩을 기본적으로 지원하지 않기 때문에 매개변수나 타입에 따라 다른 동작을 수행하도록 함수 내부에서 조건문을 사용하여 구현해야합니다.

class Calculator {
  multiply(a, b) {
    if (typeof b !== 'undefined') {
      return a * b; // 두 매개변수가 주어졌을 때
    }
    return a * a; // 하나의 매개변수가 주어졌을 때, 제곱값을 반환
  }
}
 
const calculator = new Calculator();
console.log(calculator.multiply(5, 3)); // 15
console.log(calculator.multiply(4)); // 16

❗️ 즉 **오버라이딩(Overriding)**은 "부모의 것을 자식이 덮어쓴다"라고 이해할 수 있습니다. 부모 클래스에 이미 정의된 메소드를 자식 클래스에서 새롭게 정의하여, 자식 클래스의 특징에 맞게끔 동작을 변경하는 것을 말합니다. 예를 들어, 동물 클래스가 "소리 내기"라는 메소드를 가지고 있다면, 이를 상속받은 개 클래스는 "동물이 내는 소리" 대신 "멍멍"이라고 소리를 낼 수 있도록 메소드를 변경할 수 있습니다.

**오버로딩(Overloading)**은 "같은 이름의 메소드가 다양한 상황에 맞춰 다르게 작동한다"라고 이해할 수 있습니다. 같은 이름의 메소드라도 입력받는 매개변수의 수나 종류에 따라 다른 기능을 수행하도록 여러 버전을 정의하는 것입니다. 자바스크립트는 기본적으로 메소드 오버로딩을 지원하지 않고, 필요에 따라 조건문을 통해 비슷한 기능을 구현하게 됩니다.


객체지향 프로그래밍의 특징, 추상화

추상화는 복잡한 시스템을 이해하고 설계하는데 중요한 역할을 합니다. 추상화는 객체지향 설계에서 구체적인 세부사항을 감추고, 필요한 핵심 개념만을 드러내어 복잡성을 줄이는 기법입니다.

추상화를 통해 복잡한 시스템을 단순화 할 수 있는데 예를 들어, 자동차를 운전할 때 우리는 핸들, 페달, 그리고 기어와 같은 기본적인 조작법에만 집중합니다. 엔진의 내부 구조나 연료의 흐름을 알 필요는 없습니다. 이처럼 추상화는 사용자가 필요로 하는 기능에만 집중할 수 있도록 도와줍니다.

추상화는 보통 클래스와 인터페이스를 통해 구현됩니다. 예를 들어, 다양한 종류의 '동물'을 표현하는 프로그램에서, '동물'이라는 공통의 속성과 메소드를 가진 추상적인 클래스나 인터페이스를 정의할 수 있습니다. 각각의 동물(예: 개, 고양이)은 '동물' 클래스를 상속받아 자신만의 구체적인 구현을 가질 수 있습니다. 이에 따라 각 동물은 자신의 방식대로 행동하지만, 외부에서는 동일한 '동물'로 취급할 수 있게 됩니다.


객체지향 프로그래밍의 특징, 캡슐화

캡슐화는 객체지향 프로그래밍에서 데이터를 보호하고, 불필요한 데이터 노출을 방지하기 위한 기법입니다. 캡슐화를 통해 클래스 내부의 데이터와 메소드를 외부로부터 숨기고, 클래스가 제공하는 인터페이스를 통해서만 접근할 수 있도록 합니다.

캡슐화는 객체의 속성과 메소드를 하나의 단위로 묶고, 외부에서 접근할 필요가 없는 세부 구현은 감추는 것을 의미합니다. 이는 마치 캡슐에 필요한 약만 담고 외부로부터 보호하는 것과 같습니다. 필요한 경우에만 약을 꺼내 쓸 수 있듯이, 객체 내부의 데이터는 지정된 메소드를 통해서만 접근할 수 있습니다.

캡슐화는 클래스 내에서 접근 제한자를 사용하여 구현됩니다. 일반적으로 public, protected, private과 같은 키워드를 사용하여 접근 범위를 지정합니다. 예를 들어, 자바스크립트에서 클래스 내의 변수는 외부에서 직접 접근할 수 없도록 let이나 const를 사용하고, 외부에서 접근 가능한 메소드를 통해서만 값을 설정하거나 읽어올 수 있습니다.

class Account {
  #balance;
 
  constructor(initialBalance) {
    this.#balance = initialBalance;
  }
 
  deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
    }
  }
 
  withdraw(amount) {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount;
    }
  }
 
  getBalance() {
    return this.#balance;
  }
}

위의 예시에서 #balance 프라이빗 필드(private field)는 외부에서 직접 접근할 수 없으며 클래스 내부의 메소드를 통해서만 접근 및 수정할 수 있습니다.


객체지향 프로그래밍의 특징, 다형성

다형성은 하나의 인터페이스를 통해 여러 객체가 서로 다른 방식으로 동작할 수 있게 하는 것입니다. 이로 인해 프로그램 설계가 유연해지고, 변경 및 확장이 용이해집니다. 실세계 비유를 통해 다형성의 개념을 쉽게 이해할 수 있습니다.

운전자와 자동차

JavaScript에서는 다형성을 클래스 상속, 메서드 오버라이딩, 그리고 동적 타입 특성을 통해 쉽게 구현할 수 있습니다. 아래는 JavaScript에서 Vehicle 인터페이스를 이용해 다양한 자동차 클래스를 구현하고, 이를 통해 다형성을 활용하는 예제입니다.

// 인터페이스 역할을 하는 Vehicle 클래스 정의
class Vehicle {
  drive() {
    console.log('Vehicle is driving.');
  }
}
 
// 각기 다른 자동차 구현체들
class K3 extends Vehicle {
  drive() {
    console.log('K3를 운전합니다.');
  }
}
 
class Avante extends Vehicle {
  drive() {
    console.log('아반떼를 운전합니다.');
  }
}
 
class Tesla extends Vehicle {
  drive() {
    console.log('테슬라를 운전합니다.');
  }
}
 
// 다형성을 이용한 함수 정의
function startDriving(vehicle) {
  vehicle.drive();
}
 
const k3 = new K3();
const avante = new Avante();
const tesla = new Tesla();
 
startDriving(k3); // "K3를 운전합니다."
startDriving(avante); // "아반떼를 운전합니다."
startDriving(tesla); // "테슬라를 운전합니다."

위 예제에서 Vehicle은 역할을, K3, Avante, Tesla는 각기 다른 구현을 담당합니다. startDriving 함수는 Vehicle 객체를 받아 drive 메서드를 호출하지만, 실제로 어떤 구체적인 자동차가 사용될지는 알 필요가 없습니다. 즉, Vehicle이라는 역할만 알면 K3, Avante, Tesla구현체의 변경에 영향을 받지 않습니다.

즉, 다형성은 클라이언트와 구현체의 결합을 줄이고, 인터페이스를 기반으로 객체 간의 협력을 효율적으로 설계하는 데 중요한 역할을 하는데, 이를 통해 코드의 가독성, 유지보수성, 확장성을 크게 향상시킬 수 있습니다.

상속 vs 다형성

상속(Inheritance)

다형성(Polymorphism)

상속과 다형성의 관계


React, Next에서의 객체지향 설계 적용

React 및 Next.js와 같은 현대적인 프론트엔드 프레임워크는 주로 함수형 프로그래밍의 패러다임을 많이 활용하지만, 객체지향 프로그래밍(OOP)의 개념도 여전히 사용할 수 있습니다. 두 접근 방식은 서로 배타적이지 않으며, 적절하게 조합하여 사용할 수 있습니다.

  1. 컴포넌트 기반 아키텍처: React와 Next.js는 컴포넌트를 중심으로 설계되며, 컴포넌트는 본질적으로 객체입니다. 컴포넌트는 상태(state)와 생명주기(lifecycle)를 관리하는 논리를 캡슐화하여, 재사용 가능하고 유지보수하기 쉬운 단위를 제공합니다.
  2. 상속과 컴포지션: React는 상속보다는 컴포지션(조합)을 권장하지만, 조합 역시 OOP의 중요한 설계 기법입니다. 여러 컴포넌트를 조합하여 더욱 복잡한 컴포넌트를 구성할 수 있으며, 이를 통해 코드 재사용성을 높일 수 있습니다.
  3. 상태 관리: 상태가 복잡한 애플리케이션에서는 객체지향적인 설계를 통해 상태를 체계적으로 관리할 수 있습니다. 예를 들어, Redux 같은 상태 관리 라이브러리는 액션과 리듀서의 객체지향적인 설계 패턴을 사용하여 예측 가능한 상태 변화를 관리합니다.

결론적으로, React와 Next.js에서는 함수형 프로그래밍의 패러다임이 많이 활용되고 있지만, 객체지향 설계의 원칙과 기법도 여전히 중요한 역할을 할 수 있습니다. 나는 객체지향을 적용할거야, 함수형 프로그래밍을 적용할거야 하는게 아닌 두 가지 접근 방식의 장점을 이해하고, 각 프로젝트의 요구사항에 맞게 선택적으로 사용하는 것이 중요하다고 생각합니다.


me
@banhogu
안녕하세요 배움을 나누며 함께 전진하는 1년차 주니어 개발자 방호진입니다.