디자인패턴, 프로그래밍 패러다임, 아키텍처/OOP

객체와 OOP의 4가지 특징

차가운에스프레소 2023. 3. 3. 19:42

1. 객체란?

- 객체란 현실 세계에서 실재하는 모든 "대상"을 "변수"(상태/속성)와 "함수"(행동)로 추상화 시킨 개념이다.

* 여기서 추상화는 대상의 본질적인 특징을 추출하여 표현한 것이라고 이해하고 있다.


2. OOP란?

- OOP란 Object Oriented Programming의 약자이다. 말 그대로, 객체 지향 프로그래밍이다.

- 즉, 하나의 소프트웨어가 동작하는 원리를 그것을 구성하는 여러 객체 간의 상호작용으로 정의하고, 이에 따라, 객체를 중심으로 소프트웨어를 설계/개발해야한다는 프로그래밍 패러다임인 것이다.


3. OOP의 4가지 특징

- 이러한 OOP는 크게 1) 추상화, 2) 상속, 3) 다형성, 4) 캡슐화라는 특징에 기반한 프로그래밍 패러다임이다.

- 요컨대, 위 4가지 특징에 기반하여 객체를 중심으로 설계된 프로그램은 개발 생산성이 뛰어날 것으로 기대하는 것이다.

- 각 특징을 하나씩 살펴보겠다.


1) 추상화

- 추상화란 위에서 언급한 것처럼 어떤 대상/집단의 공통적이고 본질적인 특징을 추출하여 정의한 것을 의미한다.

- 자연스레, OOP의 특징 중 하나인 추상화란 어떤 대상을 구현할 때, 그 대상의 본질적인 특징을 정의하고, 이것에 기반하여 대상을 객체로 구현하는 것을 의미한다.

- 이 떄, 대상의 본질적인 특징을 정의하는 데 프로그래밍적으로 활용되는 개념이 abstract class와 interface이다.

* abstract class와 interface의 차이는 다음 글에서 다룬다.

- 예시 코드는 다음과 같다. 스포츠팀을 추상화하여, 야구팀과 스포츠팀에 대한 class를 구현한다.

1. 스포츠 팀에 대한 추상화
- 팀에는 선수와 감독이 있다.
- 선수를 추가하고, 감독/선수를 조회할 수 있다.


abstract class SportsTeam {
  protected manager: string;
  protected players: string[] = [];

  constructor(manager: string) {
    this.manager = manager;
  }
}

interface Managable {
  getPlayers(): string[];
  createPlayers(player: string): string;
  getManager(): string;
}

interface SoccerIntroduction {
  getTeamSubject(): "soccer";
}

interface BaseballIntroduction {
  getTeamSubject(): "baseball";
}
2. 추상화를 기반으로 객체 생성자 구현


class BaseballTeam extends SportsTeam implements Managable, BaseballIntroduction {
  constructor(manager: string) {
    super(manager);
  }

  getPlayers() {
    return this.players;
  }

  getManager(): string {
    return this.manager;
  }

  createPlayers(player: string) {
    this.players.push(player);
    return "create player success";
  }

  getTeamSubject(): "baseball" {
    return "baseball";
  }
}

2. 추상화를 기반으로 객체 생성자 구현


class SoccerTeam extends SportsTeam implements Managable, SoccerIntroduction {
  constructor(manager: string) {
    super(manager);
  }

  getPlayers() {
    return this.players;
  }

  getManager(): string {
    return this.manager;
  }

  createPlayers(player: string) {
    this.players.push(player);
    return "create player success";
  }
  getTeamSubject(): "soccer" {
    return "soccer";
  }
}

2) 상속

- OOP의 주요 특징인 상속은 대상을 객체로 추상화/구현할 때, 기존에 구현한 클래스를 재활용하여 구현할 수 있는 것을 의미한다. (이 때, 재활용한 클래스는 상위 클래스 / 기존 클래스를 재활용하여 구현한 클래스는 하위 클래스가 된다.)

- 구체적으로, 대상을 추상화하여 객체로 구현할 때, 대상의 특징을 일부분 구현한 객체가 있을 경우, 이를 확장하여 대상을 추상화/구현할 수 있다는 것이다.

- 이러한 상속을 통해, 코드 재사용성을 높일 수 있다.

- 예시 코드는 다음과 같다. 위와 동일하게 스포츠 팀과 야구 팀의 예시다.

1. 스포츠 팀에 대해 구현한 SportsTeam class

class SportsTeam {
  protected manager: string;
  protected players: string[] = [];

  constructor(manager: string) {
    this.manager = manager;
  }
}


2. 스포츠 팀을 상속하여 구현한 BaseballTeam class

class BaseballTeam extends SportsTeam {
  constructor(manager: string) {
    super(manager);
  }

  getPlayers() {
    return this.players;
  }

  getManager(): string {
    return this.manager;
  }

  createPlayers(player: string) {
    this.players.push(player);
    return "create player success";
  }
}

3) 다형성

- OOP의 특징으로서 다형성이란 어떤 객체의 속성이나 기능이 상황에 따라 여러 형태로 변할 수 있다는 것을 의미한다. 

- OOP에서 다형성을 구현하는 예시로는 상속/구현 상황에서 메서드 오버라이딩/오버로딩이 있다.

- 다형성을 통해 개발 유연성, 코드 재사용성을 제고시킬 수 있다.

- 다형성이 구현된 구조에서는 상위 객체의 타입으로 하위 객체를 참조할 수 있기 때문이다.

- 다음은 예시 코드다.

 

1) 스포츠 팀을 상속하여 구현된 야구팀과 축구팀에서 메소드 오버라이딩을 이용해 getTeamIntroduction()를 변형시켰다.

interface Managable {
  getPlayers(): string[];
  createPlayers(player: string): void;
  getManager(): string;
}

1. 스포츠 팀 추상화
abstract class SportsTeam implements Managable {
  protected manager: string;
  protected players: string[] = [];
  protected name: string;
  protected abstract subject: string;
  constructor(name: string, manager: string) {
    this.manager = manager;
    this.name = name;
  }

  createPlayers(player: string) {
    this.players.push(player);
  }
  abstract getPlayers(): string[];
  abstract getManager(): string;
  abstract getTeamIntroduction(): string;
}

2. 추상화를 기반으로 야구팀 객체 생성자 구현
class BaseballTeam extends SportsTeam {
  subject: string = "baseball";
  constructor(name: string, manager: string) {
    super(name, manager);
  }

  getPlayers() {
    return this.players;
  }

  getManager(): string {
    return this.manager;
  }

  createPlayers(player: string) {
    this.players.push(player);
    return "create player success";
  }

3. 메소드 오버라이딩으로 팀 소개 조회 메소드 구현
  getTeamIntroduction(): string {
    return `${this.name} play ${this.subject} we need pitcher`;
  }
}

2. 추상화를 기반으로 축구팀 객체 생성자 구현
class SoccerTeam extends SportsTeam implements Managable {
  subject = "soccer";
  constructor(name: string, manager: string) {
    super(name, manager);
  }

  getPlayers() {
    return this.players;
  }

  getManager(): string {
    return this.manager;
  }

  createPlayers(player: string) {
    this.players.push(player);
    return "create player success";
  }

3. 메소드 오버라이등으로 팀 소개 조회 메소드 구현
  getTeamIntroduction(): string {
    return `${this.name} play ${this.subject}. Join the best soccer team`;
  }
}

2) 선수 객체에서 객체의 다형성을 이용해, 간결한 코드로 선수가 속한 팀과 그 팀의 종목을 조회하는 method를 구현했다.

class Player {
  private name: string;
  private clubList: SportsTeam[] = [];
  constructor(name: string) {
    this.name = name;
  }
  
  - 스포츠 팀의 다형성을 이용해 getTeamIntroduction()을 간결하게 구현.
  playWhichSubject(SportsTeam: SportsTeam) {
    return SportsTeam.getTeamIntroduction();
  }
  
  - 스포츠 팀의 다형성을 이용해 joinSprotsTeam()을 간결하게 구현.
  joinSportsTeam(SportsTeam: SportsTeam) {
    this.clubList.push(SportsTeam);
    SportsTeam.createPlayers(this.name);
  }
  
  getBelongedTeams() {
    return this.clubList;
  }
}

const eagles = new BaseballTeam("eagles", "yu hyeonjun");
const liverpool = new SoccerTeam("soccer", "klopp");
const user = new Player("yu");

ealges, liverpool은 SportsTeam을 상속받았기 때문에, 코드 재사용성이 높아진다.
user.joinSportsTeam(eagles);
user.joinSportsTeam(liverpool);

const clubList = user.getBelongedTeams();
for (let i = 0; i < clubList.length; i++) {
  console.log("소속 팀과 종목 조회", clubList[i].getTeamIntroduction()); 
  // 소속 팀과 종목 조회 eagles play baseball | 소속 팀과 종목 조회 liverpool play soccer
}

- 만약 BaseballTeam, SoccerTeam이 SportsTeam을 상속받지 않았다면, 다형성을 활용할 수 없어, SportsTeam이 아니라 BaseballTeam, SoccerTeam를 매개변수로 받는 각각의 메소드가 필요했을 것이다.

- SportsTeam의 다형성을 이용해 코드를 재사용한 사례라고 할 수 있겠다.


4) 캡슐화

- 캡슐화란 클래스 내의 연관된 속성(property)이나 함수(method)를 하나의 캡슐로 묶어 외부로부터 클래스로의 접근을 최소화하는 것을 의미한다.

- 캡슐화는 다음과 같이 두 가지 장점이 있다.

  1. 1) 외부로부터 클래스의 변수, 함수를 보호
  2. 2) 외부에는 필요한 요소만 노출하고, 내부의 상세한 동작은 은닉

- 클래스의 캡슐화는 public, static, private, protected와 같은 접근제한자를 통해 구현할 수 있다.

  1. public: 하위 클래스와 인스턴스에서 접근 가능.
  2. static: 클래스와 외부에서 접근 가능. 인스턴스에서는 접근 불가
  3. private: 클래스 본인만 접근 가능, 하위 클래스, 인스턴스 접근 불가
  4. protected: 클래스 본인, 하위 클래스에서 접근 가능, 인스턴스 접근 불가

- 캡슐화는 객체 간의 결합도를 감소시키고, 응집도를 강화하는데 기여합니다. 즉, 유지보수의 용이성이 좋아지는 효과를 기대할 수 있다.

  • 객체 간 결합도: 객체 간에 의존하는 정도를 의미합니다.
  • 객체의 응집도: 한 객체의 자율성, 특정 역할에 대한 독립적인 책임을 의미합니다.

- 예시 코드는 다음과 같습니다. 아래 예시를 통해 1) 캡슐화가 어떻게 응집도 개선에 기여하는지, 2) 응집도 개선이 유지보수의 용이성에 기여하는지를 확인할 수 있다.

 

1) 캡슐화 전

class Pharmacist {
  private medicine1 = "for covid";
  private medicine2 = "no pill";
  private medicine3 = "for new covid";

  greetToClinet() {
    return `약국입니다. 어서오세요.`;
  }
  examineClient(condition: string) {
    if (condition === "very hard") {
      return "new covid";
    } else if (condition === "hard") {
      return "covid";
    } else {
      return "no covid";
    }
  }
  makePill(disease: string) {
    if (disease === "new covid") {
      return this.medicine3;
    } else if (disease === "covid") {
      return this.medicine1;
    } else {
      return this.medicine2;
    }
  }
  givePillToClient(pill: string) {
    if (pill === this.medicine1) {
      return `${this.medicine1} 복용법입니다.`;
    } else if (pill === this.medicine2) {
      return `${this.medicine2}. 쉬세요`;
    } else {
      return `${this.medicine3} 복용법입니다.`;
    }
  }
}

class Patient {
  private name: string;
  private condition: string;
  disease = "?";
  constructor(name: string, condition: string) {
    this.name = name;
    this.condition = condition;
  }

  goToPharmacy() {
    const pharmacy = new Pharmacist();
    pharmacy.greetToClinet();
    this.disease = pharmacy.examineClient(this.condition);
    const pill = pharmacy.makePill(this.disease);
    return pharmacy.givePillToClient(pill);
  }
}

const patient = new Patient("yu", "hard");
console.log(patient.goToPharmacy()); // for covid 복용법입니다.

- Pharmacist class의 method들이 모두 public으로 설정되어 있다.

- 그래서 Patient class의 goToPharmacy() 메소드에서 생성된 Pharmacist 인스턴스에서 Pharmacist의 모든 속성과 기능에 접근할 수 있는 것을 확인할 수 있다.

- 만약, Pharmacist의 속성과 기능에 변경사항이 생기면, goToPharmacy도 모두 변경해줘야하는 번거로움이 남아있다.

 

2) 캡슐화 후

class Pharmacist2 {
  private medicine1 = "for covid";
  private medicine2 = "no pill";
  private medicine3 = "for new covid";

  private greetToClinet() {
    return `약국입니다. 어서오세요.`;
  }
  private examineClient(condition: string) {
    if (condition === "very hard") {
      return "new covid";
    } else if (condition === "hard") {
      return "covid";
    } else {
      return "no covid";
    }
  }
  private makePill(disease: string) {
    if (disease === "new covid") {
      return this.medicine3;
    } else if (disease === "covid") {
      return this.medicine1;
    } else {
      return this.medicine2;
    }
  }
  private givePillToClient(pill: string) {
    if (pill === this.medicine1) {
      return `${this.medicine1} 복용법입니다.`;
    } else if (pill === this.medicine2) {
      return `${this.medicine2}. 쉬세요`;
    } else {
      return `${this.medicine3} 복용법입니다.`;
    }
  }

  operate(condition: string) {
    this.greetToClinet();
    return this.givePillToClient(this.makePill(this.examineClient(condition)));
  }
}

class Patient2 {
  private name: string;
  private condition: string;
  disease = "?";
  constructor(name: string, condition: string) {
    this.name = name;
    this.condition = condition;
  }

  goToPharmacy() {
    const pharmacy = new Pharmacist2();
    return pharmacy.operate(this.condition);
  }
}

const patient2 = new Patient("yu", "hard");
console.log(patient.goToPharmacy()); // for covid 복용법입니다.

- public이었던 Pharmacist의 메소드들을 모두 private으로 변경했다. 그리고 이들을 모두 실행하는 public 메소드 operate를 새로 생성했다.

- Patient에서는 Pharmacist의 opreate 메소드만 접근 가능하다. 하지만, opreate에서 Pharmacist에서 접근해야하는 기능들은 모두 접근할 수 있다.

- 즉, Pharmacist  측면에서는 Patient에 operate 메소드만 노출시키고, 그 외 메소드들은 감춘 것이다. 즉, 데이터 은닉데이터 보호를 실현한 것이다.

- 아울러, 코드적으로도 Pharmacist의 변경사항이 생기면, 해당 Class 변경해주면 된다. 유지보수의 용이성이 개선된 것이다.


4. 요약

- OOP는 객체 지향 프로그래밍의 줄임말이다.

- 객체란 현실 세계에서 실재하는 모든 대상을 추상화하여 변수와 함수로 구성한 프로그래밍적 개념이다.

- OOP는 4가지 특징을 가지고 있고, 이 특징들로 말미암아, OOP를 통해 개발 생산성을 제고할 수 있다.

  • 추상화: 대상의 본질적인 특성을 추출하여 객체로 표현하는 것.
  • 상속: 기존에 있는 클래스를 활용하여 객체를 추상화 및 구현, 코드 재사용성 증가
  • 다형성: 메서드 오버라이딩 및 오버로딩으로 상황에 따라 여러 기능 구현 / 하위 클래스는 상위 클래스 타입으로 참조 가능 / 코드 재사용성 증가
  • 캡슐화: 외부로부터 데이터 보호, 은닉 / 유지보수의 용이성 증가

5. 참고자료

- https://www.codestates.com/blog/content/%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%ED%8A%B9%EC%A7%95

 

객체 지향 프로그래밍의 4가지 특징ㅣ추상화, 상속, 다형성, 캡슐화 -

객체 지향 프로그래밍은 객체의 유기적인 협력과 결합으로 파악하고자 하는 컴퓨터 프로그래밍의 패러다임을 의미합니다. 객체 지향 프로그래밍의 기본적인 개념과 그 설계를 바르게 하기 위

www.codestates.com

 

'디자인패턴, 프로그래밍 패러다임, 아키텍처 > OOP' 카테고리의 다른 글

SOLID 원칙  (0) 2023.03.06