TIL | 객체 지향 프로그래밍 핵심 원칙

bubblegum·2024년 2월 17일
0

Today I learn(TIL)

목록 보기
24/84
post-thumbnail

1) 캡슐화 (Encapsulation)

객체 내부의 세부적인 사항을 감추는 것, 즉 중요한 정보를 외부로 노출시키지 않도록 만드는 것을 캡슐화(Encapsulation)라고 합니다.

Javascript 는 완벽한 캡슐화를 지원하지 않습니다. 그러나, 개발자들은 변수 앞에 언더바(_)를 붙여 내부의 변수를 숨긴것 “처럼” 나타내는 규칙을 따르곤 합니다. 다음 예제는 TypeScript입니다.

class User {
  private name: string; // name 변수를 외부에서 접근을 할 수 없게 만듭니다.
  private age: number; // age 변수를 외부에서 접근을 할 수 없게 만듭니다.
  
  setName(name: string) { // Private 속성을 가진 name 변수의 값을 변경합니다.
    this.name = name;
  }
  getName() { // Private 속성을 가진 name 변수의 값을 조회합니다.
    return this.name;
  }
  setAge(age: number) { // Private 속성을 가진 age 변수의 값을 변경합니다.
    this.age = age;
  }
  getAge() { // Private 속성을 가진 age 변수의 값을 조회합니다.
    return this.age;
  }
}

const user = new User(); // user 인스턴스 생성
user.setName('이용우');
user.setAge(30);
console.log(user.getName()); // 이용우
console.log(user.getAge()); // 30
console.log(user.name); // Error: User 클래스의 name 변수는 private로 설정되어 있어 바로 접근할 수 없습니다.

User 클래스를 선언하고 내부에는 name, age 멤버 변수를 초기화 하였습니다. 여기서는 특별하게 private라는 접근 제한자(Access modifier)를 사용하고 있는데요, 인스턴스 내부에서만 해당 변수에 접근이 가능하도록 제한하는 문법 입니다. 기존 Javascript에서는 존재하지 않았지만 Typescript에서 제공하는 문법입니다. 따라서, User 클래스의 name, age 멤버 변수는 클래스 외부에서는 어떠한 방법으로도 직접 접근을 할 수 없습니다. 오로지 setter만 변수를 변경할 수 있고, getter만 변수를 조회할 수 있게 되었습니다. getter는 변수의 값을 가져오는 (getName, getAge)를 나타내고, setter는 변수의 값을 설정하는 (setName, setAge)를 나타냅니다.

2) 상속 (Inheritance)

상속(Inheritance)은 하나의 클래스가 가진 특징(함수, 변수 및 데이터)을 다른 클래스가 그대로 물려 받는 것을 말합니다. 이미 정의된 상위 클래스의 특징을 하위 클래스에서 물려받아 코드의 중복을 제거하고 코드 재사용성을 증대시킵니다. 개별 클래스를 상속 관계로 묶음으로써 클래스 간의 체계화된 구조를 쉽게 파악할 수 있게 됩니다. 상위 클래스의 데이터와 메서드를 변경함으로써 전체 코드에 대한 일관성을 유지할 수 있습니다. 상속(Inheritance)은 기존에 작성된 클래스를 재활용하여 사용할 수 있습니다.

class Mother { // Mother 부모 클래스
  constructor(name, age, tech) { // 부모 클래스 생성자
    this.name = name;
    this.age = age;
    this.tech = tech;
  }
  getTech(){ return this.tech; } // 부모 클래스 getTech 메서드
}
    
class Child extends Mother{ // Mother 클래스를 상속받은 Child 자식 클래스
  constructor(name, age, tech) { // 자식 클래스 생성자
    super(name, age, tech); // 부모 클래스의 생성자를 호출
  }
}
    
const child = new Child("이용우", "28", "Node.js");
console.log(child.name); // 이용우
console.log(child.age); // 28
console.log(child.getTech()); // 부모 클래스의 getTech 메서드 호출: Node.js

Mother 부모 클래스를 상속받은 Child 자식 클래스에서 name, age 멤버 변수를 직접 접근하여 호출하고, Mother 부모 클래스에서 정의된 getTech() 메소드를 호출할 수 있게 되었습니다. 이처럼, 상속의 이러한 특성 덕분에 코드를 재사용하기 수월해지고, 중복을 줄일 수 있게되는 장점이 있는것이죠.

3) 추상화 (Abstraction)

객체에서 공통된 부분을 모아 상위 개념으로 새롭게 정의하는 것을 추상화(Abstraction)라고 합니다. 즉, 불필요한 세부 사항을 생략하고, 중요한 특징만을 강조함으로써 코드를 더욱 간결하고 관리하기 쉽게 만드는 원칙입니다. 추상화를 통해 객체들의 불 필요한 특성을 제거함으로써, 공통적인 특성을 더욱 명확하게 파악할 수 있게 됩니다. 이를 통해 전체 시스템의 구조를 명확하게 이해하게 되고, 테스트를 더욱 쉽게 작성할 수 있게 됩니다.

클래스를 설계할 때, 공통적으로 묶일 수 있는 기능을 추상화(Abstraction) → 추상 클래스(Abstract Class) → 인터페이스(Interface) 순으로 정리한다면, 여러 클래스 간의 일관성을 유지하면서, 다양한 형태로 확장될 수 있는 코드, 즉 다형성(Polymorphism)이 가능해집니다.

여기서 인터페이스(Interface)는 객체의 형태를 정의하는 규약입니다. 클래스는 인터페이스를 구현하여 해당 인터페이스에 정의된 형태를 따르도록 강제할 수 있습니다. 객체가 가져야 할 속성과 메서드의 형태를 설명하는 추상적인 개념입니다.

interface Human {
  name: string;
  setName(name);
  getName();
}

// 인터페이스에서 상속받은 프로퍼티와 메소드는 구현하지 않을 경우 에러가 발생합니다.
class Employee implements Human {
  constructor(public name: string) {}

  // Human 인터페이스에서 상속받은 메소드
  setName(name) { this.name = name; }

  // Human 인터페이스에서 상속받은 메소드
  getName() { return this.name; }
}

const employee = new Employee("");
employee.setName("이용우"); // Employee 클래스의 name을 변경하는 setter
console.log(employee.getName()); // Employee 클래스의 name을 조회하는 getter

Employee 클래스는 Human 인터페이스에서 정의한 name 프로퍼티와 setName, getName 메서드를 강제로 구현하게 되었습니다. 따라서, Human 인터페이스를 구현하는 모든 클래스는 해당 인터페이스에 선언된 프로퍼티와 메서드를 구현해야 함을 보장하게 되었습니다. 이로 인해 코드의 일관성을 유지할 수 있게된 것이죠.

4) 다형성 (Polymorphism)

다형성(Polymorphism)은 하나의 객체(클래스)가 다양한 형태로 동작하는것을 의미합니다. 이는 객체가 가진 특성에 따라 같은 기능이 다르게 재구성되는 것을 의미합니다. 즉, 동일한 메서드나 함수 명을 사용하더라도, 클래스마다 그 메서드가 다르게 동작하는 것이 다형성의 핵심입니다. 다형성은 역할(인터페이스)과 구현을 분리하게 해줍니다. 따라서, 오버라이딩을 통해 특정 서비스의 기능을 유연하게 변경하거나 확장할 수 있게 합니다. Java의 오버로딩(Overloading), 오버라이딩(Overriding)은 대표적인 다형성의 예시입니다.

Java 오버로딩, 오버라이딩

class Person {
  constructor(name) { this.name = name; }

  buy() {}
}

class Employee extends Person {
  buy() { console.log(`${this.constructor.name} 클래스의 ${this.name}님이 물건을 구매하였습니다.`); }
}

class User extends Person {
  buy() { console.log(`${this.constructor.name} 클래스의 ${this.name}님이 물건을 구매하였습니다.`); }
}

const employee1 = new Employee("이용우");
const employee2 = new Employee("김창환");
const user1 = new User("이태강");
const user2 = new User("김민수");

const personsArray = [employee1, employee2, user1, user2];
// personsArray에 저장되어 있는 Employee, User 인스턴스들의 buy 메소드를 호출합니다.
personsArray.forEach((person) => person.buy());

// Employee 클래스의 이용우님이 물건을 구매하였습니다.
// Employee 클래스의 김창환님이 물건을 구매하였습니다.
// User 클래스의 이태강님이 물건을 구매하였습니다.
// User 클래스의 김민수님이 물건을 구매하였습니다.

위의 personsArray.forEach() 예제에서 person 변수는 Person 클래스를 상속받은 Employee 또는 User 클래스의 인스턴스를 참조합니다. 여기서, 각 인스턴스의 buy 메서드를 호출하는 것은 동일하지만, EmployeeUser 클래스의 buy 메서드는 서로 다른 행위를 수행하고 있는 것을 확인할 수 있습니다. 이것이 바로 다형성(Polymorphism)의 특징입니다!

profile
황세민

0개의 댓글

관련 채용 정보