본문 바로가기
GD's IT Lectures : 기초부터 시리즈/C, C++ 기초부터 ~

[C/C++ 프로그래밍 : 중급] 1. 객체 지향 프로그래밍의 개념

by GDNGY 2023. 5. 16.

Chapter 1. 객체 지향 프로그래밍의 개념

객체 지향 프로그래밍(Object-Oriented Programming, OOP)은 프로그래밍 패러다임 중 하나로, 복잡한 문제를 해결하기 위해 '객체'라는 개념을 중심으로 설계합니다. 이 챕터에서는 OOP의 기본 개념과 역사, 주요 구성 요소(클래스, 객체, 메서드, 상속, 다형성, 캡슐화)를 소개하며, OOP가 코드의 재사용성, 유지보수성, 안정성에 어떻게 기여하는지를 설명합니다. 또한 C++에서 OOP를 어떻게 구현하는지에 대한 예제를 제공하며, 효과적인 객체 지향 설계를 위한 SOLID 원칙을 소개합니다. 이 챕터를 통해 독자는 OOP의 핵심 개념을 이해하고 C++에서 이를 적용하는 방법을 배울 수 있습니다.

 

반응형

 


[Chapter 1. 객체 지향 프로그래밍의 개념]

 

1.1. 객체 지향 프로그래밍의 이해

1.1.1. 객체 지향 프로그래밍의 정의

1.1.2. 절차 지향 프로그래밍과의 비교

1.1.3. 객체 지향 프로그래밍의 역사

 

1.2. 객체 지향 프로그래밍의 기본 구성 요소

1.2.1. 클래스와 객체

1.2.2. 메소드와 속성

1.2.3. 상속

1.2.4. 다형성

1.2.5. 캡슐화

 

1.3. 객체 지향 프로그래밍의 장점

1.3.1. 코드 재사용성

1.3.2. 코드 유지보수성

1.3.3. 코드의 안정성

 

1.4. C++에서의 객체 지향 프로그래밍

1.4.1. 클래스의 정의와 사용

1.4.2. 상속의 구현과 사용

1.4.3. 다형성의 구현과 사용

1.4.4. 캡슐화의 구현과 사용

 

1.5. 객체 지향 설계 원칙

1.5.1. SOLID 원칙 소개

1.5.2. 단일 책임 원칙 (Single Responsibility Principle)

1.5.3. 개방-폐쇄 원칙 (Open-Closed Principle)

1.5.4. 리스코프 치환 원칙 (Liskov Substitution Principle)

1.5.5. 인터페이스 분리 원칙 (Interface Segregation Principle)

1.5.6. 의존 역전 원칙 (Dependency Inversion Principle)

 


1.1. 객체 지향 프로그래밍의 이해

객체 지향 프로그래밍(Object-Oriented Programming, OOP)은 프로그래밍 패러다임 중 하나로, 복잡한 문제를 해결하기 위해 '객체'라는 개념을 중심으로 설계합니다. 이 챕터에서는 OOP의 기본 개념과 역사, 주요 구성 요소(클래스, 객체, 메서드, 상속, 다형성, 캡슐화)를 소개하며, OOP가 코드의 재사용성, 유지보수성, 안정성에 어떻게 기여하는지를 설명합니다. 또한 C++에서 OOP를 어떻게 구현하는지에 대한 예제를 제공하며, 효과적인 객체 지향 설계를 위한 SOLID 원칙을 소개합니다. 이 챕터를 통해 독자는 OOP의 핵심 개념을 이해하고 C++에서 이를 적용하는 방법을 배울 수 있습니다.

1.1.1. 객체 지향 프로그래밍의 정의

객체 지향 프로그래밍(Object-Oriented Programming, OOP)은 프로그래밍 패러다임 중 하나로, '객체'라는 개념을 중심으로 소프트웨어를 설계하고 개발하는 방법론입니다.

 

객체란 무엇일까요? 객체는 실세계에서 인식할 수 있는 사물이나 개념을 프로그래밍에서 표현한 것을 말합니다. 예를 들어, '차'라는 객체는 '색상', '모델', '제조사' 등의 속성과 '운전하기', '정차하기' 등의 행동을 가질 수 있습니다. 이처럼 객체 지향 프로그래밍에서는 객체의 속성과 행동을 하나로 묶어서 표현합니다.

 

이러한 객체는 '클래스'라는 틀을 통해 만들어집니다. 클래스는 객체를 생성하기 위한 설계도나 틀로 볼 수 있습니다. 예를 들어, '차'라는 클래스는 '색상', '모델', '제조사'라는 속성과 '운전하기', '정차하기'라는 메소드를 정의할 수 있습니다. 그리고 이 클래스를 통해 실제 '차' 객체를 만들 수 있습니다.

 

C++ 코드 예제를 통해 이를 이해해봅시다.

 

[예제]

// Car 클래스 정의
class Car {
public: // 접근 지정자
    // 속성 (멤버 변수)
    string color;
    string model;
    string manufacturer;

    // 메소드 (멤버 함수)
    void drive() {
        cout << "차가 운전 중입니다." << endl;
    }
    
    void stop() {
        cout << "차가 정지했습니다." << endl;
    }
};


위의 코드에서 Car라는 클래스를 정의하였습니다. 이 클래스는 'color', 'model', 'manufacturer'라는 속성과 'drive()', 'stop()'이라는 메서드를 가지고 있습니다.

 

이제 이 Car 클래스를 통해 실제 '차' 객체를 만들어 보겠습니다.

 

[예제]

// Car 객체 생성
Car myCar;
myCar.color = "Red";
myCar.model = "Sedan";
myCar.manufacturer = "Hyundai";

// 객체의 메소드 사용
myCar.drive();
myCar.stop();


위의 코드에서 myCar라는 객체를 생성하였고, 이 객체의 속성을 설정하였습니다. 그리고 drive()와 stop() 메소드를 호출하여 차가 운전하고 정차하는 행동을 표현하였습니다.

 

객체 지향 프로그래밍은 이렇게 객체의 속성과 행동을 하나로 묶어서 프로그래밍하는 것을 말합니다. 이를 통해 코드의 재사용성을 높이고, 유지보수를 용이하게 하며, 보다 직관적인 코드 설계가 가능합니다.

 

이러한 객체 지향 프로그래밍은 '클래스(Class)'라는 개념을 통해 이루어집니다. 클래스는 '속성(Attribute)'과 '메서드(Method)'를 가진 '객체(Object)'를 생성하는 틀이나 설계도와 같습니다. '속성'은 객체의 상태를 나타내는 데이터를 의미하며, '메서드'는 객체가 수행할 수 있는 행동을 의미합니다.

 

C 언어로는 객체 지향 프로그래밍을 직접적으로 지원하지 않지만, 구조체를 통해 비슷하게 표현할 수 있습니다. 아래는 C 언어를 사용하여 '차' 객체를 표현하는 예제입니다.

 

[예제]

// Car 구조체 정의
typedef struct {
    // 속성
    char color[20];
    char model[20];
    char manufacturer[20];

    // 메소드
    void (*drive)();
    void (*stop)();
} Car;

// drive 메소드
void drive() {
    printf("차가 운전 중입니다.\n");
}

// stop 메소드
void stop() {
    printf("차가 정지했습니다.\n");
}

// 메인 함수
int main() {
    // Car 객체 생성
    Car myCar;

    // 속성 설정
    strcpy(myCar.color, "Red");
    strcpy(myCar.model, "Sedan");
    strcpy(myCar.manufacturer, "Hyundai");

    // 메소드 설정
    myCar.drive = drive;
    myCar.stop = stop;

    // 메소드 호출
    myCar.drive();
    myCar.stop();

    return 0;
}


이처럼 객체 지향 프로그래밍은 실세계의 객체를 프로그래밍으로 표현하여, 복잡한 문제를 더 간결하고 이해하기 쉬운 형태로 해결할 수 있게 도와줍니다. 특히 소프트웨어의 복잡성을 관리하고, 재사용성을 높이며, 유지보수를 용이하게 하는데 큰 도움을 줍니다.

 

1.1.2. 절차 지향 프로그래밍과의 비교

객체 지향 프로그래밍(Object-Oriented Programming, OOP)과 절차 지향 프로그래밍(Procedural Programming)은 프로그래밍의 주요한 패러다임 중 두 가지입니다. 두 방식은 서로 상당히 다른 접근 방식을 가지고 있으며, 이는 코드의 구조와 설계에 큰 영향을 미칩니다.

 

절차 지향 프로그래밍은 프로그램을 일련의 절차 혹은 순서로 이해하는 방식입니다. 이 패러다임은 주로 함수를 사용하여 코드를 구조화하며, 데이터와 함수가 별도로 존재합니다. 프로그램은 일련의 함수 호출로 구성되며, 각 함수는 특정 작업을 수행하고 결과를 반환합니다.

 

반면, 객체 지향 프로그래밍은 프로그램을 서로 상호작용하는 객체들의 집합으로 보는 방식입니다. 이 패러다임에서는 데이터와 함수가 객체라는 하나의 단위로 묶여 있습니다. 객체들은 자신만의 상태(속성)와 행동(메서드)을 가지며, 이를 통해 프로그램의 복잡성을 관리하고 코드의 재사용성을 높이는 것이 가능해집니다.

 

이제 C 언어와 C++ 언어를 사용하여 절차 지향 프로그래밍과 객체 지향 프로그래밍의 차이점을 보여주는 예제 코드를 살펴보겠습니다.

 

먼저, C 언어를 사용한 절차 지향적인 방식의 코드입니다:

 

[예제]

#include <stdio.h>

// 차의 속성을 표현하는 구조체
typedef struct {
    char color[20];
    char model[20];
    char manufacturer[20];
} Car;

// 차를 운전하는 함수
void driveCar(Car car) {
    printf("차가 운전 중입니다.\n");
}

// 차를 정지하는 함수
void stopCar(Car car) {
    printf("차가 정지했습니다.\n");
}

int main() {
    Car myCar;

    strcpy(myCar.color, "Red");
    strcpy(myCar.model, "Sedan");
    strcpy(myCar.manufacturer, "Hyundai");

    driveCar(myCar);
    stopCar(myCar);

    return 0;
}


이 예제에서는 Car라는 구조체를 정의하고, driveCar와 stopCar라는 함수를 사용하여 차를 운전하고 정지시킵니다. 각 함수는 특정 작업을 수행하며, 데이터와 함수가 별도로 존재합니다.

 

다음으로, C++을 사용하여 객체 지향적인 방식으로 동일한 기능을 구현한 코드를 살펴보겠습니다:

 

[예제]

#include <iostream>
#include <string>

// Car 클래스 정의
class Car {
public:
    std::string color;
    std::string model;
    std::string manufacturer;

    // 차를 운전하는 메소드
    void drive() {
        std::cout << "차가 운전 중입니다.\n";
    }

    // 차를 정지하는 메소드
    void stop() {
        std::cout << "차가 정지했습니다.\n";
    }
};

int main() {
    // Car 객체 생성
    Car myCar;

    myCar.color = "Red";
    myCar.model = "Sedan";
    myCar.manufacturer = "Hyundai";

    // 메소드 호출
    myCar.drive();
    myCar.stop();

    return 0;
}

 

이 예제에서는 Car라는 클래스를 정의하고, drive와 stop이라는 메서드를 사용하여 차를 운전하고 정지시킵니다. 클래스는 객체의 상태(속성)와 행동(메서드)을 묶어서 하나의 단위로 표현합니다. 객체는 자신만의 상태와 행동을 가지며, 이를 통해 코드의 복잡성을 관리하고 재사용성을 높이는 것이 가능해집니다.

 

이렇게 비교해 보면, 절차 지향 프로그래밍과 객체 지향 프로그래밍은 프로그램을 구성하는 방식에서 큰 차이를 보입니다. 절차 지향 프로그래밍은 함수와 데이터가 별개로 존재하고, 프로그램은 함수의 순서대로 실행되는 반면, 객체 지향 프로그래밍은 데이터와 함수가 객체라는 하나의 단위로 묶여 있으며, 객체들이 서로 상호작용하면서 프로그램이 실행됩니다.

 

따라서, 객체 지향 프로그래밍은 코드의 재사용성을 높이고, 유지보수를 용이하게 하며, 보다 직관적인 코드 설계가 가능하다는 장점이 있습니다. 또한, 실세계의 문제를 보다 자연스럽게 표현할 수 있으며, 복잡한 소프트웨어 시스템을 개발하고 관리하는데 유리합니다.

 

1.1.3. 객체 지향 프로그래밍의 역사

객체 지향 프로그래밍(Object-Oriented Programming, OOP)은 1960년대에 등장한 프로그래밍 패러다임입니다. 그 이전에는 절차 지향 프로그래밍(Procedural Programming)이 주를 이루었는데, 이는 프로그램의 흐름이 일련의 절차를 따르는 방식을 가리킵니다. 이러한 절차 지향적인 접근법은 작은 프로그램에는 적합했지만, 프로그램의 규모가 커짐에 따라 코드의 복잡도가 증가하면서 유지보수와 개발이 어려워지는 문제가 발생했습니다.

 

객체 지향 프로그래밍의 아이디어는 이러한 문제를 해결하기 위해 제시되었습니다. OOP는 프로그램을 여러 개의 독립적인 '객체'들로 구성하고, 이들 객체가 서로 상호작용하면서 프로그램을 실행하는 방식을 제안했습니다. 이는 실세계에서 사물이나 개체가 서로 상호작용하는 방식을 모방한 것이었습니다.

 

OOP의 초기 개념은 1960년대 후반에 노르웨이의 오슬로 대학에서 개발한 시뮬라(Simula)라는 언어에서 비롯되었습니다. 시뮬라는 컴퓨터 시뮬레이션을 위해 개발된 언어였지만, 클래스와 객체, 상속 등의 객체 지향 개념을 도입한 최초의 프로그래밍 언어로서 주목받았습니다.

 

그러나 객체 지향 프로그래밍이 널리 퍼지게 된 것은 1980년대에 들어서부터입니다. 이 시기에는 스몰토크(Smalltalk), C++, 자바(Java) 등의 언어가 등장하면서 OOP는 주류 프로그래밍 패러다임으로 자리 잡게 되었습니다.

 

특히, C++는 기존의 C 언어에 객체 지향 기능을 추가한 언어로, 1980년대 초반에 벨 연구소의 비야네 스트롭스트룹(Bjarne Stroustrup)에 의해 개발되었습니다. C++는 C의 효율성과 객체 지향의 강력한 추상화 기능을 결합함으로써, 다양한 분야에서 널리 사용되는 언어가 되었습니다.

 

이후, 1990년대에 들어서면서 자바(Java)라는 언어가 등장하였습니다. 자바는 웹 애플리케이션 개발을 위해 선명한 객체 지향 특징을 갖춘 언어로 개발되었으며, "한 번 작성하면 어디서든 실행된다(Write Once, Run Anywhere)"는 슬로건 아래에 크로스 플랫폼 실행이 가능한 JVM(Java Virtual Machine) 위에서 동작하는 특징을 가졌습니다. 이로 인해 자바는 웹 애플리케이션 개발뿐만 아니라 다양한 시스템 개발에 널리 쓰이게 되었습니다.

 

객체 지향 프로그래밍은 이렇게 시뮬라, 스몰토크, C++, 자바 등의 언어를 통해 발전하였고, 이러한 언어들은 각각의 독특한 특성과 장점을 가지면서 프로그래밍 세계에 큰 영향을 미쳤습니다. 더 나아가 현재는 파이썬(Python), 루비(Ruby), 스위프트(Swift) 등의 언어들도 객체 지향 특성을 갖추고 있어, 객체 지향 프로그래밍이 프로그래밍 패러다임에서 중요한 위치를 차지하고 있음을 알 수 있습니다.

 

이처럼 객체 지향 프로그래밍은 프로그래밍의 역사에서 중요한 위치를 차지하고 있으며, 실제 세계를 더 잘 반영하고 유지보수가 용이하며, 재사용성이 높은 코드를 작성할 수 있도록 돕는 중요한 도구입니다. 따라서 프로그래밍을 배우는 당신에게도 객체 지향 프로그래밍의 이해는 필수적인 일이며, 이를 통해 더 나은 프로그래머가 될 수 있을 것입니다. 이번 장에서는 그 중심에 있는 객체 지향 프로그래밍에 대해 자세히 알아보겠습니다.

 


1.2. 객체 지향 프로그래밍의 기본 구성 요소

객체 지향 프로그래밍의 핵심은 '객체'라는 개념에서 시작됩니다. 이 섹션에서는 객체 지향 프로그래밍의 기본 구성 요소인 클래스와 객체, 메서드와 속성, 상속, 다형성, 캡슐화에 대해 알아봅니다. 클래스는 객체를 만들기 위한 틀이며, 객체는 클래스에서 생성된 실체입니다. 메소드와 속성은 객체의 행동과 상태를 정의하고, 상속은 클래스 간의 관계를 구성하며, 다형성은 하나의 형태가 여러 가지 기능을 할 수 있게 하고, 캡슐화는 데이터와 기능을 하나로 묶는 것입니다.

1.2.1. 클래스와 객체

클래스와 객체는 객체 지향 프로그래밍의 핵심 구성 요소입니다. 클래스는 객체를 만들기 위한 틀 혹은 설계도로 생각할 수 있습니다. 객체는 클래스에 의해 생성되며, 클래스에서 정의한 행동(메서드)과 상태(속성)를 가집니다. 객체는 실제 메모리 공간에 할당된 것을 의미하며, 이를 통해 프로그램에서 사용됩니다.

 

객체는 실세계의 사물을 추상화하여 표현한 것입니다. 예를 들어 '자동차'라는 클래스를 생각해 봅시다. 자동차라는 클래스는 '색상', '브랜드', '모델' 등의 속성을 가질 수 있으며, '가속', '정지', '기어 변경' 등의 메서드를 가질 수 있습니다. 이러한 클래스를 통해 실제 '자동차' 객체를 생성하고 이를 사용할 수 있습니다.

 

다음은 C++에서 '자동차' 클래스를 정의하고 객체를 생성하는 간단한 예제입니다.

 

[예제]

#include<iostream>
using namespace std;

class Car { // Car 클래스 정의
  public:
    string color; // 색상 속성
    string brand; // 브랜드 속성
    string model; // 모델 속성

    void accelerate() { // 가속 메소드
      cout << "The car accelerates." << endl;
    }

    void stop() { // 정지 메소드
      cout << "The car stops." << endl;
    }

    void changeGear(int gear) { // 기어 변경 메소드
      cout << "The car changes to gear " << gear << "." << endl;
    }
};

int main() {
  Car myCar; // myCar 객체 생성
  myCar.color = "Red"; // 객체의 속성 설정
  myCar.brand = "Toyota";
  myCar.model = "Corolla";

  myCar.accelerate(); // 객체의 메소드 호출
  myCar.changeGear(3);
  myCar.stop();

  return 0;
}

 

이 코드를 실행하면 myCar라는 객체가 생성되고, 해당 객체의 속성을 설정하고 메소드를 호출하여 자동차의 행동을 시뮬레이션할 수 있습니다. 이렇게 클래스와 객체를 사용하면 실제 세계의 복잡한 사물을 코드 상에서 쉽게 표현하고 관리할 수 있습니다. 이는 코드의 재사용성을 높이고, 유지 보수를 용이하게 하는 데 중요한 역할을 합니다.

 

1.2.2. 메서드와 속성

객체 지향 프로그래밍에서, 객체는 클래스에서 정의된 속성과 메서드를 가집니다. 속성은 객체의 상태를 정의하는 변수입니다. 예를 들어, 자동차 객체는 색상, 브랜드, 모델 등의 속성을 가질 수 있습니다. 반면에 메소드는 객체가 수행할 수 있는 동작을 정의한 함수입니다. 자동차의 경우, 가속, 정지, 기어 변경 등의 메소드를 가질 수 있습니다.

 

메서드와 속성은 객체의 특성을 설명하고, 객체가 어떻게 동작해야 하는지를 정의합니다. 메소드는 객체의 동작을 캡슐화하고, 속성은 객체의 상태를 저장합니다. 이 둘은 함께 작동하여 객체의 동작과 상호작용을 정의합니다.

 

다음은 C++에서 메소드와 속성이 어떻게 작동하는지 보여주는 예제입니다.

 

[예제]

#include<iostream>
using namespace std;

class Car {
  public:
    string color; // 속성
    string brand; // 속성
    string model; // 속성

    void setColor(string c) { // 메소드
      color = c;
    }

    void setBrand(string b) { // 메소드
      brand = b;
    }

    void setModel(string m) { // 메소드
      model = m;
    }

    void displayInfo() { // 메소드
      cout << "Color: " << color << ", Brand: " << brand << ", Model: " << model << endl;
    }
};

int main() {
  Car myCar;
  myCar.setColor("Red"); // 속성 설정
  myCar.setBrand("Toyota"); // 속성 설정
  myCar.setModel("Corolla"); // 속성 설정

  myCar.displayInfo(); // 정보 출력

  return 0;
}

 

이 코드는 Car 클래스를 정의하고, 이 클래스에는 color, brand, model이라는 속성과 setColor, setBrand, setModel, displayInfo라는 메서드가 있습니다. 메서드는 객체의 속성을 설정하고, displayInfo 메소드는 현재 객체의 정보를 출력합니다. 이렇게 메서드와 속성을 사용하면 객체의 상태와 동작을 캡슐화하고 관리할 수 있습니다.

 

이것이 메소드와 속성이 객체 지향 프로그래밍에서 어떻게 사용되는지에 대한 기본적인 개요입니다. 이러한 개념을 이해하고 적용하면, 객체 지향 프로그래밍의 강력한 도구를 사용하여 복잡한 문제를 효과적으로 해결할 수 있습니다.

 

1.2.3. 상속

객체 지향 프로그래밍에서 상속은 클래스 간에 코드를 재사용하고 계층 구조를 형성하는 중요한 개념입니다. 상속을 사용하면 기존 클래스의 속성과 메서드를 새 클래스에 "상속"하여 코드를 재사용하고, 새로운 기능을 추가할 수 있습니다.

 

상속은 "부모-자식" 관계로 표현되며, 부모 클래스를 "기본 클래스" 또는 "슈퍼 클래스", 자식 클래스를 "파생 클래스" 또는 "서브 클래스"라고도 부릅니다. 서브 클래스는 슈퍼 클래스의 모든 속성과 메소드를 상속받고, 필요에 따라 새로운 속성과 메소드를 추가하거나 기존 메소드를 "오버라이드"하여 변경할 수 있습니다.

 

다음은 C++에서 상속이 어떻게 사용되는지 보여주는 예제입니다.

 

[예제]

#include<iostream>
using namespace std;

// 슈퍼 클래스 정의
class Vehicle {
  public:
    string brand; // 속성

    void setBrand(string b) { // 메소드
      brand = b;
    }

    void displayBrand() { // 메소드
      cout << "Brand: " << brand << endl;
    }
};

// 서브 클래스 정의
class Car : public Vehicle {
  public:
    string model; // 속성

    void setModel(string m) { // 메소드
      model = m;
    }

    void displayInfo() { // 메소드
      displayBrand(); // 상속받은 메소드 호출
      cout << "Model: " << model << endl;
    }
};

int main() {
  Car myCar;
  myCar.setBrand("Toyota"); // 상속받은 메소드 호출
  myCar.setModel("Corolla"); // 자신의 메소드 호출

  myCar.displayInfo(); // 정보 출력

  return 0;
}

 

이 코드는 Vehicle이라는 슈퍼 클래스를 정의하고, 이 클래스에는 brand라는 속성과 setBrand, displayBrand라는 메서드가 있습니다. 그리고 Car라는 서브 클래스를 정의하고, 이 클래스는 Vehicle 클래스를 상속받으며 model이라는 새로운 속성과 setModel, displayInfo라는 새로운 메서드를 추가합니다. 이렇게 상속을 사용하면 기존 코드를 재사용하고, 새로운 기능을 쉽게 추가할 수 있습니다.

 

이것이 상속이 객체 지향 프로그래밍에서 어떻게 사용되는지에 대한 기본적인 개요입니다. 이러한 개념을 이해하고 적용하면, 객체 지향 프로그래밍의 강력한 도구를 사용하여 복잡한 문제를 효과적으로 해결할 수 있습니다.

 

1.2.4. 다형성

다형성은 객체 지향 프로그래밍의 중요한 개념입니다. 이는 "많은 형태"를 의미하며, 하나의 객체가 여러 형태의 객체로 취급될 수 있음을 의미합니다. 이것은 코드를 더 유연하게 만들어 줍니다. 특히, 다형성은 상속과 밀접한 관련이 있습니다. 즉, 하위 클래스의 객체가 상위 클래스의 객체로 취급될 수 있습니다.

 

C++에서 다형성은 주로 가상 함수와 함께 사용됩니다. 가상 함수는 상위 클래스에서 선언되고 하위 클래스에서 재정의될 수 있는 함수입니다. 상위 클래스의 포인터나 참조를 사용하여 가상 함수를 호출하면, 실제 객체의 타입에 따라 하위 클래스의 함수가 호출됩니다. 이것이 바로 다형성입니다.

다음은 C++에서 다형성을 보여주는 예제 코드입니다:

 

[예제]

#include <iostream>
using namespace std;

// 상위 클래스 정의
class Animal {
public:
    // 가상 함수 정의
    virtual void makeSound() {
        cout << "The animal makes a sound." << endl;
    }
};

// 하위 클래스 정의
class Dog : public Animal {
public:
    // 가상 함수 재정의
    void makeSound() override {
        cout << "The dog barks." << endl;
    }
};

int main() {
    Animal* animal = new Animal(); // 상위 클래스의 객체 생성
    Animal* dog = new Dog(); // 하위 클래스의 객체 생성

    animal->makeSound(); // 상위 클래스의 메소드 호출
    dog->makeSound(); // 하위 클래스의 메소드 호출

    delete animal;
    delete dog;

    return 0;
}


이 코드는 Animal이라는 상위 클래스를 정의하고, 이 클래스에는 makeSound라는 가상 함수가 있습니다. 그리고 Dog라는 하위 클래스를 정의하고, 이 클래스는 Animal 클래스를 상속받고 makeSound 함수를 재정의합니다.

 

main 함수에서는 Animal 타입의 포인터를 사용하여 상위 클래스와 하위 클래스의 객체를 생성하고, makeSound 함수를 호출합니다. 이때, 포인터가 가리키는 객체의 타입에 따라 적절한 함수가 호출됩니다. 이것이 바로 다형성입니다.

 

이것이 다형성이 객체 지향 프로그래밍에서 어떻게 사용되는지에 대한 기본적인 개요입니다. 이러한 개념을 이해하고 적용하면, 객체 지향 프로그래밍의 강력한 도구를 사용하여 복잡한 문제를 더욱 효율적으로 해결할 수 있습니다.

 

1.2.5. 캡슐화

캡슐화는 객체 지향 프로그래밍의 중요한 개념 중 하나입니다. 이는 데이터와 데이터를 처리하는 방법을 하나의 '캡슐'로 감싸는 것을 의미합니다. 캡슐화를 통해 클래스의 내부 구현을 외부로부터 숨길 수 있습니다. 이로 인해 코드의 유지 보수가 용이해지고, 데이터를 보호할 수 있습니다.

 

클래스의 속성은 일반적으로 private로 설정되어 클래스 외부에서 직접 접근할 수 없습니다. 대신에 이러한 속성에 접근하기 위해서는 public 메서드를 사용해야 합니다. 이런 메소드를 'getter'와 'setter'라고 부르며, 각각 속성을 읽고 쓰는데 사용됩니다.

 

다음은 C++에서 캡슐화를 사용하는 간단한 예제입니다:

 

[예제]

#include <iostream>
using namespace std;

class Rectangle {
private:
    // 속성은 private로 설정됩니다.
    double width;
    double height;

public:
    // setter 메소드
    void setWidth(double w) {
        if (w > 0) {
            width = w;
        } else {
            cout << "Width must be positive." << endl;
        }
    }

    // getter 메소드
    double getWidth() {
        return width;
    }

    // setter 메소드
    void setHeight(double h) {
        if (h > 0) {
            height = h;
        } else {
            cout << "Height must be positive." << endl;
        }
    }

    // getter 메소드
    double getHeight() {
        return height;
    }

    // 메소드
    double getArea() {
        return width * height;
    }
};

int main() {
    Rectangle rect;
    rect.setWidth(5.0);
    rect.setHeight(3.0);
    cout << "Area: " << rect.getArea() << endl;

    return 0;
}

 

이 예제에서 Rectangle 클래스는 너비와 높이라는 두 개의 private 속성을 가지고 있습니다. 이러한 속성에 접근하기 위해 setter와 getter 메소드를 제공합니다. setWidth와 setHeight 메서드는 너비와 높이를 설정하며, 유효성 검사를 수행하여 음수가 설정되지 않도록 합니다. getWidth와 getHeight 메소드는 각각 너비와 높이를 반환합니다. 이렇게 해서 너비와 높이 속성은 외부로부터 직접 접근할 수 없으며, 클래스 내부에서만 수정할 수 있습니다.

 

이것이 캡슐화가 객체 지향 프로그래밍에서 어떻게 사용되는지에 대한 기본적인 개요입니다. 이러한 개념을 이해하고 적용하면, 데이터를 보호하고 코드의 유지 보수를 용이하게 할 수 있습니다.

 

더 나아가서, 캡슐화는 코드의 재사용성을 높여줍니다. 만약 우리가 클래스의 특정 부분을 수정하려고 한다면, 그 클래스를 사용하는 다른 코드 부분들에는 영향을 미치지 않습니다. 이는 클래스의 내부 구현이 외부로부터 숨겨져 있기 때문입니다. 따라서, 클래스를 사용하는 코드는 클래스의 내부 구현에 대해 알 필요가 없으며, 이는 코드의 가독성을 높여줍니다.

 

다음은 C에서 캡슐화를 모방하는 간단한 예제입니다. C에는 클래스가 없지만, 구조체와 함수를 사용하여 캡슐화를 어느 정도 구현할 수 있습니다.

 

[예제]

#include <stdio.h>

// 'Rectangle' 구조체 정의
typedef struct {
    double width;
    double height;
} Rectangle;

// 'Rectangle'의 너비와 높이를 설정하는 함수
void setWidthAndHeight(Rectangle* rect, double w, double h) {
    if (w > 0 && h > 0) {
        rect->width = w;
        rect->height = h;
    } else {
        printf("Width and height must be positive.\n");
    }
}

// 'Rectangle'의 넓이를 계산하는 함수
double getArea(Rectangle* rect) {
    return rect->width * rect->height;
}

int main() {
    Rectangle rect;
    setWidthAndHeight(&rect, 5.0, 3.0);
    printf("Area: %f\n", getArea(&rect));

    return 0;
}

 

이 예제에서는 Rectangle이라는 구조체를 정의하고, setWidthAndHeight와 getArea라는 함수를 사용하여 너비와 높이를 설정하고 넓이를 계산합니다. 이렇게 함수를 사용하면 데이터를 보호하고 코드의 유지 보수를 용이하게 할 수 있습니다. 함수를 통해 직접적인 데이터 접근을 제한하므로, 데이터가 무엇인지, 어떻게 작동하는지에 대해 걱정할 필요 없이, 원하는 기능만을 사용할 수 있게 됩니다.

 

그러나, 이것은 C++의 캡슐화와 완벽하게 동일하게 동작하지는 않습니다. 구조체의 멤버는 기본적으로 public이므로, 직접 접근할 수 있습니다. 이런 이유로, C++에서 제공하는 클래스와 같은 강력한 캡슐화 기능을 C에서 완벽하게 구현하는 것은 불가능합니다. 그럼에도 불구하고, C에서는 함수를 사용하여 데이터와 기능을 어느 정도 결합시킬 수 있으므로, 캡슐화의 기본 원칙을 따르는 것이 가능합니다.

캡슐화는 객체 지향 프로그래밍의 핵심 원칙 중 하나로, 데이터와 그 데이터를 다루는 방법을 하나로 묶어서, 외부에서 접근을 제한하고, 코드의 안정성과 유지보수성을 높이는 데 큰 역할을 합니다. 이 원칙을 잘 이해하고 적용하면, 코드의 재사용성과 안정성을 획기적으로 높일 수 있습니다.

 


1.3. 객체 지향 프로그래밍의 장점

객체 지향 프로그래밍의 장점은 다음과 같습니다.

모듈성 : 오류가 발생한 경우, 특정 객체를 쉽게 찾아 문제를 해결할 수 있습니다. 이는 각 기능이 독립적으로 작동하며, 다른 기능에 영향을 주지 않기 때문입니다. 또한, 이러한 모듈성은 IT 팀이 여러 객체를 동시에 작업하면서 중복 기능의 가능성을 최소화하는데 도움이 됩니다.

상속을 통한 코드 재사용 : 상속을 사용하면 기본 클래스의 특성을 공유하는 서브클래스를 만들 수 있습니다. 이로 인해 코드를 재사용하고, 모든 자동차 객체에 대한 변경사항을 쉽게 적용할 수 있습니다.

다형성을 통한 유연성 : 다형성을 통해 단일 함수가 어떤 클래스에 있든지 적응할 수 있습니다. 이를 통해 부모 클래스에 하나의 함수만을 생성하고, 이 함수를 다양한 하위 클래스에서 작동시킬 수 있습니다.

효과적인 문제 해결 : 객체 지향 프로그래밍은 큰 문제를 해결 가능한 작은 문제들로 나누는데 초점을 맞추고 있습니다. 각각의 작은 문제에 대해 클래스를 작성하고, 이 클래스들을 재사용함으로써 다음 문제를 더 빠르게 해결할 수 있습니다.

1.3.1. 코드 재사용성

객체 지향 프로그래밍(OOP)의 중요한 장점 중 하나는 코드 재사용성입니다. 코드를 재사용한다는 것은 이미 작성된 코드나 모듈을 다른 프로그램에서 다시 사용할 수 있다는 것을 의미합니다. 이는 개발 시간을 줄이고, 코드의 일관성을 유지하며, 오류를 줄일 수 있습니다.

 

C/C++에서 코드 재사용성은 주로 상속과 함께 논의됩니다. 클래스의 상속을 통해 부모 클래스의 속성과 메서드를 자식 클래스가 받아들이게 되므로, 비슷한 기능을 가진 클래스를 새로 작성하는 대신에 이미 작성된 클래스를 재사용할 수 있습니다.

 

C++에서의 코드 재사용을 위한 예제를 살펴보겠습니다. 먼저, 자동차를 나타내는 간단한 Car 클래스를 생성해 봅시다.

 

[예제]

class Car {
public:
    void startEngine() {
        cout << "Engine started.\n";
    }
};

 

이제 이 Car 클래스를 상속받는 RaceCar 클래스를 만들어 봅시다.

 

[예제]

class RaceCar : public Car {
public:
    void startEngine() {
        Car::startEngine();
        cout << "Ready to race!\n";
    }
};

 

여기서 RaceCar 클래스는 Car 클래스의 startEngine 메서드를 재사용하고 있습니다. RaceCar의 startEngine 메서드가 호출되면, 먼저 Car 클래스의 startEngine 메서드가 호출되어 "Engine started."를 출력하고, 그다음에 "Ready to race!"를 출력합니다. 이렇게 하면 Car 클래스의 startEngine 코드를 RaceCar 클래스에서도 재사용할 수 있습니다.

 

이것이 코드 재사용성의 힘이며, 이를 통해 우리는 더 효율적인 코드를 작성하고, 유지 관리를 용이하게 할 수 있습니다.

 

1.3.2. 코드 유지보수성

객체 지향 프로그래밍은 코드 유지보수성을 높이는 데 크게 기여합니다. 이 말은 다시 말해, 코드 변경이 필요할 때 객체 지향 프로그래밍이 그 과정을 더 간편하게 만들어준다는 뜻입니다.

 

객체 지향 프로그래밍의 핵심 요소 중 하나는 캡슐화입니다. 캡슐화는 데이터와 데이터를 처리하는 함수를 하나의 '객체'로 묶는 과정을 말합니다. 이러한 객체들은 독립적으로 작동하며, 각각의 내부 데이터는 외부로부터 보호받습니다. 이렇게 되면, 한 부분을 변경해야 할 때 다른 부분에 미치는 영향을 최소화할 수 있습니다. 이는 유지보수를 간편하게 해 줍니다.

 

다음은 C++에서 캡슐화를 이용한 코드 유지보수성 향상의 예시입니다.

 

[예제]

class Car {
private:
    int speed;

public:
    void setSpeed(int s) {
        if (s < 0) {
            cout << "Speed cannot be negative.\n";
            return;
        }
        speed = s;
    }

    int getSpeed() {
        return speed;
    }
};

 

이 코드에서 Car 클래스는 속도(speed)라는 속성을 가지고 있습니다. 하지만 이 속성은 직접 접근할 수 없으며, setSpeed와 getSpeed라는 메서드를 통해서만 접근할 수 있습니다. 이렇게 함으로써 Car 클래스의 speed 속성은 외부로부터 보호받게 됩니다.

 

이제 speed 속성에 어떤 변경이 필요하다고 가정해 봅시다. 예를 들어, 속도가 음수가 되는 것을 막아야 한다면, setSpeed 메서드만 변경하면 됩니다. 이렇게 하면 Car 클래스를 사용하는 다른 코드에는 아무런 영향을 미치지 않고 Car 클래스 내부만 변경할 수 있습니다.

 

이는 코드 유지보수성을 높여줍니다. 기능이 추가되거나 변경되어야 할 때, 캡슐화를 통해 코드의 특정 부분만 집중적으로 변경할 수 있게 되므로 유지보수가 훨씬 쉬워집니다. 이로써 코드 변경 시 발생할 수 있는 오류를 줄이고, 유지보수에 드는 시간과 노력을 줄일 수 있습니다.

 

1.3.3. 코드의 안정성

객체 지향 프로그래밍(OOP)은 코드의 안정성을 높이는 데 큰 역할을 합니다. 이 말은 다시 말해, OOP가 프로그램의 예상치 못한 동작이나 오류를 최소화하는데 도움이 된다는 것을 의미합니다.

 

OOP의 핵심 특징 중 하나인 캡슐화는 코드의 안정성을 향상하는 주요한 방법입니다. 캡슐화는 객체의 데이터를 직접적으로 접근하는 것을 제한하며, 대신 데이터에 접근하려면 그 객체의 메서드를 통해야 합니다. 이렇게 하면, 데이터를 올바르게 사용하도록 강제하여 프로그램의 안정성을 향상할 수 있습니다.

 

예를 들어, C++의 클래스를 사용하여 캡슐화를 구현하는 것이 일반적입니다. 다음은 간단한 BankAccount 클래스를 보여주는 C++ 코드입니다:

 

[예제]

class BankAccount {
private:
    double balance;

public:
    BankAccount(double initial_balance) {
        if (initial_balance < 0) {
            cout << "Initial balance cannot be negative. Setting it to 0.\n";
            initial_balance = 0;
        }
        balance = initial_balance;
    }

    void deposit(double amount) {
        if (amount < 0) {
            cout << "Cannot deposit a negative amount.\n";
            return;
        }
        balance += amount;
    }

    void withdraw(double amount) {
        if (amount > balance) {
            cout << "Insufficient balance.\n";
            return;
        }
        balance -= amount;
    }

    double getBalance() {
        return balance;
    }
};

 

이 BankAccount 클래스는 balance라는 private 변수를 가지고 있으며, 이 변수는 클래스 내부에서만 접근할 수 있습니다. balance에 접근하려면 클래스가 제공하는 public 메서드(deposit, withdraw, getBalance)를 사용해야 합니다. 이 메서드들은 balance를 올바르게 사용하도록 강제하기 때문에, 이 클래스를 사용하는 코드는 balance를 잘못 변경하거나 사용하는 것을 피할 수 있습니다. 이로 인해 코드의 안정성이 향상됩니다.

 

마지막으로, 이러한 안정성은 유지보수를 더 쉽게 만들고, 프로그램의 전체적인 품질을 향상하는데 도움이 됩니다. 따라서 객체 지향 프로그래밍은 소프트웨어 개발에서 매우 중요한 도구입니다.

 


1.4. C++에서의 객체 지향 프로그래밍

C++에서의 객체 지향 프로그래밍은 절차적 프로그래밍에 비해 여러 가지 장점이 있습니다. 객체 지향 프로그래밍(OOP)은 실행이 빠르고 쉽습니다. OOP는 프로그램에 명확한 구조를 제공하며, C++ 코드를 DRY(Don't Repeat Yourself)하게 유지하는 데 도움을 줍니다. 이는 코드를 유지 보수하고 수정하고 디버그 하기 쉽게 만들어줍니다. 또한, OOP는 완전히 재사용 가능한 애플리케이션을 만들 수 있게 합니다.

1.4.1. 클래스의 정의와 사용

C++에서 클래스는 사용자가 정의하는 데이터 유형입니다. 이는 우리가 프로그램에서 사용할 수 있고, 이를 "객체 생성자" 또는 "객체를 생성하는 청사진"이라고도 합니다. C++의 모든 것은 클래스와 객체, 그리고 그것들의 속성과 메서드와 연관이 있습니다. 예를 들어, 실제 세계에서 자동차는 객체입니다. 자동차는 무게와 색상과 같은 속성과, 운전과 제동과 같은 메서드를 가집니다. 이러한 속성과 메서드는 클래스의 변수와 함수이며, 이들은 종종 "클래스 멤버"라고 불립니다. 

 

클래스를 생성하려면 class 키워드를 사용합니다. 예를 들어, "MyClass"라는 클래스를 만들어 보겠습니다:

 

[예제]

class MyClass { // The class  
public: // Access specifier  
  int myNum; // Attribute (int variable)  
  string myString; // Attribute (string variable)  
};


여기서 class 키워드는 MyClass라는 클래스를 생성하는 데 사용됩니다. public 키워드는 액세스 지정자로서, 클래스의 멤버(속성과 메서드)가 클래스 외부에서 접근 가능하다는 것을 지정합니다. 클래스 내부에는 myNum이라는 정수 변수와 myString이라는 문자열 변수가 있습니다. 변수가 클래스 내에서 선언되면, 이들을 속성이라고 합니다. 마지막으로, 클래스 정의는 세미콜론 ;으로 끝납니다. 

 

C++에서 객체는 클래스에서 생성됩니다. 우리는 이미 MyClass라는 클래스를 생성했으므로, 이제 이를 사용하여 객체를 생성할 수 있습니다. MyClass의 객체를 생성하려면, 클래스 이름 다음에 객체 이름을 지정하면 됩니다. 클래스 속성(myNum과 myString)에 액세스 하려면, 객체에 점 문법(.)을 사용하면 됩니다. 

[예제]

class MyClass { // The class  
public: // Access specifier  
  int myNum; // Attribute (int variable)  
  string myString; // Attribute (string variable)  
};  

int main() {  
  MyClass myObj; // Create an object of MyClass  

  // Access attributes and set values  
  myObj.myNum = 15;   
  myObj.myString = "Some text";  

  // Print attribute values  
  cout << myObj.myNum << "\n";   
  cout << myObj.myString;   
  return 0;
}

 

이 예에서, 우리는 MyClass의 객체 myObj를 생성하고, 속성에 접근하여 값들을 설정합니다. 그리고 속성 값을 출력합니다.

 

한 클래스의 여러 객체를 계속 생성할 수 있습니다. 예를 들어, 몇 가지 속성을 가진 Car 클래스를 만들어 두 개의 Car 객체를 생성해 보겠습니다:

[예제]

// Create a Car class with some attributes   
class Car {  
public:  
  string brand;   
  string model;  
  int year;  
};  

int main() {  
  // Create an object of Car  
  Car carObj1;  
  carObj1.brand = "BMW";  
  carObj1.model = "X5";  
  carObj1.year = 1999;  

  // Create another object of Car  
  Car carObj2;  
  carObj2.brand = "Ford";  
  carObj2.model = "Mustang";  
  carObj2.year = 1969;  

  // Print attribute values  
  cout << carObj1.brand << " " << carObj1.model << " " << carObj1.year << "\n";  
  cout << carObj2.brand << " " << carObj2.model << " " << carObj2.year << "\n";  
  return 0;
}


이 예제에서, Car 클래스의 두 객체인 carObj1과 carObj2를 생성합니다. 각각의 속성에 다른 값을 설정하고, 그 값을 출력합니다. 이렇게 클래스와 객체를 사용하면, 유사한 객체가 공유하는 속성과 메서드를 재사용하고, 코드의 복잡성을 줄이는 등 여러 가지 장점을 누릴 수 있습니다.

 

1.4.2. 상속의 구현과 사용

객체 지향 프로그래밍에서 중요한 개념 중 하나는 '상속'입니다. 상속은 하나의 클래스에서 정의된 속성과 메소드를 다른 클래스가 받아들이는 방식을 의미합니다. 이를 통해 코드 재사용성이 증가하고, 프로그램의 복잡성이 감소합니다.

 

C++에서의 상속

C++에서 클래스 상속은 매우 간단합니다. : 연산자를 사용하여 하위 클래스가 상위 클래스를 상속받을 수 있습니다. 예를 들어, Animal 클래스에서 Dog 클래스로 상속하는 경우는 다음과 같습니다:

 

[예제]

// Base class
class Animal {
public:
    void eat() {
        cout << "I can eat!" << "\n";
    }
};

// Derived class
class Dog : public Animal {
public:
    void bark() {
        cout << "I can bark! Woof woof!" << "\n";
    }
};


여기서 Dog 클래스는 Animal 클래스를 상속받아, Animal 클래스의 eat 메서드를 사용할 수 있습니다. 또한 Dog 클래스에는 고유한 메소드인 bark가 추가되었습니다. 이렇게 Dog 객체를 생성하면 eat와 bark 두 메소드 모두를 사용할 수 있습니다:


[예제]

int main() {
    Dog dog;
    dog.eat();  // Output: I can eat!
    dog.bark(); // Output: I can bark! Woof woof!
    return 0;
}


이 코드는 Dog 객체가 Animal 클래스의 eat 메소드를 사용하고, 그 자신의 bark 메소드를 사용하는 것을 보여줍니다. 이는 C++의 상속 메커니즘이 어떻게 작동하는지를 잘 보여주는 예입니다.

 

상속의 이점

상속의 주요 이점 중 하나는 코드 재사용입니다. 기반 클래스의 메서드와 속성을 파생 클래스에서 재사용할 수 있기 때문에 코드 중복을 줄이고, 유지 보수를 쉽게 할 수 있습니다. 또한, 상속은 클래스 간의 관계를 논리적으로 표현할 수 있게 해 줍니다. 예를 들어, Dog 클래스가 Animal 클래스를 상속받는다는 것은 "Dog는 Animal이다"라는 관계를 코드 상에서 명확히 나타낼 수 있습니다.

 

하지만 상속을 사용할 때는 주의해야 할 점이 있습니다. 상속 구조가 과도하게 복잡해지면 코드를 이해하고 유지 보수하는데 어려움을 겪을 수 있습니다. 따라서 상속을 적절하게 사용하는 것이 중요합니다.

 

1.4.3. 다형성의 구현과 사용

다형성이란 말 그대로 '다양한 형태'를 가질 수 있는 능력을 의미합니다. 프로그래밍에서는 하나의 인터페이스나 메서드가 다양한 형태의 객체에 대해 다르게 동작하는 기능을 말합니다. 이를 통해 코드의 유연성과 재사용성이 증가하며, 더욱 명확하고 이해하기 쉬운 코드를 작성할 수 있습니다.

 

함수 오버로딩

함수 오버로딩은 같은 이름을 가진 함수를 여러 개 만드는 것을 의미합니다. 이들 함수는 매개변수의 타입이나 개수가 다르면서 같은 이름을 가질 수 있습니다. 이렇게 하면 프로그래머가 같은 이름의 함수를 사용하면서도 다양한 매개변수를 사용할 수 있습니다.


C++에서는 함수 오버로딩이 가능하지만, C에서는 지원되지 않습니다.


[예제]

#include <iostream>

void print(int i) {
    std::cout << "Printing int: " << i << std::endl;
}

void print(double  f) {
    std::cout << "Printing float: " << f << std::endl;
}

void print(char* c) {
    std::cout << "Printing character: " << c << std::endl;
}

int main() {
    print(5);
    print(500.263);
    print("Hello World");

    return 0;
}

 

함수 오버라이딩

함수 오버라이딩은 상속 관계에 있는 두 클래스에서 같은 이름과 형태를 가진 함수를 각각 정의하는 것을 의미합니다. 하위 클래스에서 상위 클래스의 함수를 '오버라이드'하면, 하위 클래스의 객체가 그 함수를 호출할 때 오버라이드된 함수가 실행됩니다. 이를 통해 상위 클래스의 행동을 하위 클래스에서 변경하거나 확장할 수 있습니다.

C++에서는 함수 오버라이딩이 가능하지만, C에서는 지원되지 않습니다.

[예제]

#include <iostream>

class Base {
public:
    virtual void print() {
        std::cout << "This is base class." << std::endl;
    }
};

class Derived : public Base {
public:
    void print() {
        std::cout << "This is derived class." << std::endl;
    }
};

int main() {
    Base *baseptr;
    Derived derived;
    baseptr = &derived;

    // print() is called according to the pointer type
    baseptr->print();

    return 0;
}

 

 

가상 함수

가상 함수는 C++의 다형성을 구현하는 기능 중 하나입니다. 기본 클래스에서 선언되고 파생 클래스에서 재정의될 수 있는 함수를 '가상 함수'라고 합니다. 가상 함수는 포인터나 참조를 통해 호출되면, 해당 포인터나 참조가 가리키는 객체의 타입에 따라 적절한 함수가 실행됩니다. 이렇게 하면, 한 클래스의 객체를 다른 클래스의 객체로 간주할 수 있게 되는데, 이를 '업캐스팅'이라고 합니다.

 

C에서는 가상 함수의 개념이 없지만, 함수 포인터를 사용하여 비슷한 효과를 얻을 수 있습니다.

[예제]

#include <stdio.h>

typedef void (*SoundFunction)();

void AnimalSound() {
    printf("The animal makes a sound \n");
}

void DogBark() {
    printf("The dog barks \n");
}

int main() {
    SoundFunction sound = AnimalSound;
    sound();  // Output: The animal makes a sound

    sound = DogBark;
    sound();  // Output: The dog barks

    return 0;
}

여기서 SoundFunction은 함수 포인터 타입입니다. main() 함수에서는 이 타입의 변수 sound를 만들고, AnimalSound()와 DogBark() 함수에 대한 포인터를 할당합니다. sound()를 호출하면 현재 sound가 가리키는 함수가 실행됩니다. 이는 C++의 다형성과 비슷한 효과를 나타냅니다.

이러한 다양한 형태의 다형성은 코드의 유연성을 높이며, 동일한 인터페이스 아래에서 다양한 동작을 구현할 수 있게 합니다. 이것이 바로 객체 지향 프로그래밍의 힘인 것입니다.

 

1.4.4. 캡슐화의 구현과 사용

캡슐화란, 데이터와 해당 데이터를 다루는 함수를 하나의 '캡슐'로 묶는 것을 의미합니다. 이 개념은 객체 지향 프로그래밍의 핵심 요소 중 하나로, 데이터와 메서드를 결합하여 객체라는 '캡슐'을 만들어 내는 것입니다.

 

캡슐화의 주요 목표는 추상화를 제공하고, 객체의 내부 상태를 숨기며, 객체와 상호작용하는 방식을 제어하는 것입니다. 이를 통해 데이터에 직접적으로 접근하는 것을 제한하고, 데이터를 수정하거나 접근하는 방식을 통제할 수 있습니다.

 

C++에서는 클래스와 접근 제한자를 사용하여 캡슐화를 구현합니다. 접근 제한자에는 public, private, 그리고 protected가 있습니다. private 멤버는 같은 클래스의 다른 객체나 외부에서 접근할 수 없습니다. 반면, public 멤버는 어디서든 접근 가능합니다. protected 멤버는 동일한 클래스 또는 파생 클래스의 객체에서만 접근 가능합니다.

 

다음은 C++에서 캡슐화를 구현한 예제입니다.

 

[예제]

#include <iostream>

class Rectangle {
private:  // private members
    int length;
    int breadth;

public:  // public members
    void setLength(int l) {
        length = l;
    }

    void setBreadth(int b) {
        breadth = b;
    }

    int getArea() {
        return length * breadth;
    }
};

int main() {
    Rectangle rect;
    rect.setLength(5);
    rect.setBreadth(10);

    std::cout << "Area of rectangle: " << rect.getArea() << std::endl;

    return 0;
}


이 코드에서 Rectangle 클래스는 length와 breadth라는 두 개의 private 데이터 멤버와, 이들에 접근하고 조작하는 public 메서드를 가집니다. 이렇게 데이터를 숨기고 메서드를 통해서만 접근하게 함으로써, 데이터의 무결성을 보장하고 객체의 내부 동작을 숨길 수 있습니다.

 

C언어에서는 캡슐화를 직접적으로 지원하지 않지만, 구조체와 함수를 이용하여 비슷한 효과를 얻을 수 있습니다. 구조체를 사용하여 데이터를 묶고, 해당 데이터를 다루는 함수를 제공하는 방식입니다.

 

다음은 C에서 캡슐화를 구현한 예제입니다.

 

[예제]

#include <stdio.h>

typedef struct {
    int length;
    int breadth;
} Rectangle;

void setLength(Rectangle *rect, int l) {
    rect->length = l;
}

void setBreadth(Rectangle *rect, int b) {
    rect->breadth = b;
}

int getArea(Rectangle *rect) {
    return rect->length * rect->breadth;
}

int main() {
    Rectangle rect;
    setLength(&rect, 5);
    setBreadth(&rect, 10);

    printf("Area of rectangle: %d\n", getArea(&rect));

    return 0;
}


이렇게 캡슐화를 통해 데이터와 함수를 묶어 관리할 수 있게 되면, 코드의 가독성과 유지보수성이 높아지고, 프로그램의 복잡성을 관리하는 데 도움이 됩니다. 캡슐화는 코드를 잘 정리하고 구조화하는 데 중요한 도구로, 객체 지향 프로그래밍의 핵심 원칙 중 하나입니다.


1.5. 객체 지향 설계 원칙

객체 지향 설계 원칙은 프로그램을 더 효과적이고 관리 가능하게 만들기 위해 사용되는 가이드라인들입니다. 대표적인 원칙으로는 SOLID 원칙이 있습니다. Single Responsibility (한 클래스는 하나의 책임만 가져야 함), Open-Closed (클래스는 확장에 대해 열려 있고 수정에 대해 닫혀 있어야 함), Liskov Substitution (하위 타입은 상위 타입으로 대체 가능해야 함), Interface Segregation (클라이언트가 사용하지 않는 메서드에 의존하지 않아야 함), Dependency Inversion (고수준 모듈은 저수준 모듈에 의존하면 안 됨) 원칙들이 있습니다. 이 원칙들은 객체 지향 프로그래밍에서 효과적인 설계를 위한 핵심 가이드라인입니다. 

1.5.1. SOLID 원칙 소개

SOLID는 객체 지향 프로그래밍과 설계에 있어서 중요한 원칙들을 의미하는 약자입니다. SOLID 원칙은 코드의 유연성, 읽기 쉬움, 유지보수성을 향상하는 데 도움이 됩니다.

 

Single Responsibility Principle (SRP, 단일 책임 원칙)

한 클래스는 하나의 책임만 가져야 한다는 원칙입니다. 이 원칙을 따르면, 클래스가 바뀌어야 하는 이유는 오직 하나뿐이어야 합니다.

 

예를 들어, 사용자 정보를 관리하는 클래스가 있다면, 사용자 정보를 데이터베이스에 저장하고, 사용자에게 이메일을 보내는 두 가지 책임을 가지고 있다고 가정해 봅시다. 이 경우, 두 가지 다른 이유로 인해 클래스가 변경될 수 있습니다. 이런 문제를 해결하기 위해, 각각의 책임을 분리한 두 개의 클래스를 생성하는 것이 좋습니다.

 

Open-Closed Principle (OCP, 개방-폐쇄 원칙)

클래스는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다는 원칙입니다. 즉, 기존의 코드를 변경하지 않고 기능을 추가할 수 있어야 합니다. 이 원칙은 상속과 인터페이스를 활용하여 구현할 수 있습니다.

 

Liskov Substitution Principle (LSP, 리스코프 치환 원칙)

하위 타입은 그것의 상위 타입으로 대체될 수 있어야 한다는 원칙입니다. 이는 모든 클래스를 사용하는 클라이언트가 특정 클래스의 하위 클래스를 마치 원래 클래스인 것처럼 사용할 수 있어야 함을 의미합니다.

 

Interface Segregation Principle (ISP, 인터페이스 분리 원칙)

클라이언트는 자신이 사용하지 않는 인터페이스에 의존하지 말아야 한다는 원칙입니다. 즉, 하나의 일반적인 인터페이스보다는, 여러 개의 구체적인 인터페이스가 낫다는 것입니다.

 

Dependency Inversion Principle (DIP, 의존성 역전 원칙)

고수준의 모듈은 저수준의 모듈에 의존하면 안 됩니다. 양쪽 모듈 모두 추상화에 의존해야 합니다. 이 원칙은 코드의 결합도를 낮추는 데 도움이 됩니다.

 

SOLID 원칙은 C++과 같은 객체 지향 프로그래밍 언어에서 매우 중요한 개념입니다. 각 원칙은 서로 독립적으로 존재할 수 있지만, 함께 사용하면 코드의 품질을 크게 향상할 수 있습니다.

 

1.5.2. 단일 책임 원칙 (Single Responsibility Principle)

단일 책임 원칙은 객체 지향 설계 원칙 중 하나로, 각 클래스가 하나의 책임만을 가져야 한다는 것을 의미합니다. 이는 클래스가 변경되는 이유가 오직 하나뿐이어야 함을 의미합니다. 이 원칙은 코드의 읽기 쉬움과 유지보수성을 향상합니다.

 

예를 들어, 한 클래스가 사용자의 정보를 관리하고, 동시에 사용자에게 이메일을 보내는 책임을 가진다면, 이 클래스는 두 가지 책임을 지고 있습니다. 이렇게 되면 이메일 시스템이 변경되었을 때 사용자 정보 관리 클래스를 변경해야 할 필요가 생길 수 있습니다. 이것은 단일 책임 원칙을 위반한 예입니다.

 

이를 해결하기 위해, 각각의 책임을 분리하여 사용자 정보를 관리하는 클래스와 사용자에게 이메일을 보내는 클래스를 따로 만드는 것이 좋습니다. 이렇게 하면 각 클래스는 자신의 책임에만 집중하고, 다른 클래스의 변경에 영향을 받지 않습니다.

 

다음은 이 원칙을 지키지 않았을 때의 예를 보여주는 C++ 코드입니다:

 

[예제]

class User {
public:
    void SetName(string name) {
        this->name = name;
    }

    void SendEmail(string emailContent) {
        // 이메일을 보내는 로직
    }

private:
    string name;
};


위의 코드에서 User 클래스는 사용자의 이름을 설정하고 이메일을 보내는 두 가지 책임을 가지고 있습니다. 이 경우 이메일 시스템의 변경에 따라 User 클래스도 변경해야 할 가능성이 생깁니다. 이를 단일 책임 원칙에 따라 수정하면 다음과 같습니다:


[예제]

class User {
public:
    void SetName(string name) {
        this->name = name;
    }

private:
    string name;
};

class Email {
public:
    void SendEmail(string emailContent) {
        // 이메일을 보내는 로직
    }
};


이제 User 클래스와 Email 클래스는 각각 하나의 책임만 가지게 되었습니다. 이로써 클래스의 변경이 다른 클래스에 미치는 영향을 최소화하고, 코드의 유지보수성을 향상할 수 있습니다.

 

1.5.3. 개방-폐쇄 원칙 (Open-Closed Principle)

개방-폐쇄 원칙은 "소프트웨어 엔티티(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야 하며, 수정에 대해서는 닫혀 있어야 한다"는 원칙을 의미합니다. 이 원칙의 핵심은 기능을 변경하거나 확장할 때, 기존의 코드를 수정하는 것이 아니라 새로운 코드를 추가함으로써 시스템의 동작을 변경하거나 확장하는 것입니다.

 

이 원칙을 따르면 시스템은 더 유연하고 재사용성이 높아질 뿐만 아니라 유지보수가 쉬워집니다. 왜냐하면 새로운 기능을 추가하더라도 기존 코드를 수정하지 않기 때문에 기존 기능이 손상될 가능성이 줄어들기 때문입니다.

 

C++에서의 개방-폐쇄 원칙을 설명하는 예제를 살펴봅시다.

 

[예제]

class Shape {
public:
    virtual double Area() const = 0; // 순수 가상 함수
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}

    double Area() const override {
        return radius * radius * 3.14;
    }
};

class Rectangle : public Shape {
private:
    double width;
    double height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}

    double Area() const override {
        return width * height;
    }
};

double CalculateTotalArea(const vector<Shape*>& shapes) {
    double totalArea = 0;
    for (Shape* shape : shapes) {
        totalArea += shape->Area();
    }
    return totalArea;
}


위의 코드에서 'Shape'는 추상 기본 클래스이며, 'Circle'과 'Rectangle'은 'Shape'를 상속받아 각자의 'Area' 메서드를 구현합니다. 'CalculateTotalArea' 함수는 'Shape' 객체의 목록을 받아 총 면적을 계산합니다.

 

이제 새로운 도형, 예를 들어 'Triangle' 클래스를 추가하려면 어떻게 해야 할까요? 단지 'Shape'를 상속받아 새로운 'Triangle' 클래스를 생성하고 'Area' 메소드를 오버라이드하면 됩니다. 'CalculateTotalArea' 함수는 수정할 필요가 없습니다. 이것이 바로 개방-폐쇄 원칙입니다 - 기능을 확장하려면 새로운 코드를 추가하고, 기존 코드는 수정하지 않습니다. 

 

1.5.4. 리스코프 치환 원칙 (Liskov Substitution Principle)

리스코프 치환 원칙(LSP)은 상속 구조에서 매우 중요한 원칙입니다. 이 원칙은 "서브타입은 그것의 기본 타입으로 치환 가능해야 한다"라는 개념을 의미합니다. 다시 말해, 부모 클래스를 사용하는 곳에서는 언제든지 자식 클래스를 대신 사용할 수 있어야 합니다.

 

이 원칙은 부모 클래스와 자식 클래스 간의 행동의 일관성을 보장합니다. 따라서 프로그램의 예측 가능성을 높이고, 오류를 줄이는 데 중요합니다.

 

C++에서의 리스코프 치환 원칙을 설명하는 예제를 살펴봅시다.

 

[예제]

class Bird {
public:
    virtual void fly() {
        cout << "I can fly!" << endl;
    }
};

class Penguin : public Bird {
public:
    void fly() override {
        // 펭귄은 날 수 없다.
        cout << "I can't fly!" << endl;
    }
};

void makeItFly(Bird& bird) {
    bird.fly();
}


위의 코드에서 Bird 클래스는 fly라는 메서드를 가지고 있고, Penguin 클래스는 Bird 클래스를 상속받아 fly 메서드를 오버라이드합니다. makeItFly 함수는 Bird의 참조를 인자로 받아 fly 메서드를 호출합니다.

 

이제 Penguin 객체를 makeItFly 함수에 전달하면 어떻게 될까요? Penguin은 Bird의 서브클래스이므로 이 함수에 전달될 수 있습니다. 그러나 펭귄은 실제로 날 수 없기 때문에, 이 경우 리스코프 치환 원칙이 위반되었습니다. 이것은 Penguin이 Bird의 서브클래스임에도 불구하고 Bird를 완벽하게 치환할 수 없다는 것을 보여줍니다.

 

따라서 이 원칙을 준수하려면 모든 서브클래스가 기본 클래스의 행동을 유지하도록 해야 합니다. 이것은 상속이 올바르게 사용되는지를 검사하는 좋은 방법이며, 코드의 가독성과 유지 관리성을 향상합니다.

 

1.5.5. 인터페이스 분리 원칙 (Interface Segregation Principle)

인터페이스 분리 원칙(ISP)은 "클라이언트는 사용하지 않는 인터페이스에 의존하도록 강제해서는 안 된다"라는 개념을 포함합니다. 다시 말해, 클라이언트가 필요로 하지 않는 메서드에 의존성을 갖지 않도록 설계해야 한다는 것입니다.

 

이 원칙은 클래스와 인터페이스가 효율적으로 최소한의 역할을 수행하도록 하는데 중요합니다. 이는 각 객체의 역할과 책임이 명확하게 분리되어야 함을 의미합니다.

 

C++ 에는 Java나 C#과 같이 명시적인 인터페이스 구문이 없지만, 순수 가상 함수를 사용하여 비슷한 역할을 수행할 수 있습니다.

 

[예제]

class Worker {
public:
    virtual void work() = 0; // 순수 가상 함수
};

class Eater {
public:
    virtual void eat() = 0; // 순수 가상 함수
};

class Human : public Worker, public Eater {
public:
    void work() override {
        cout << "I can work!" << endl;
    }
    void eat() override {
        cout << "I can eat!" << endl;
    }
};


위의 예제에서 Human 클래스는 Worker와 Eater 인터페이스를 모두 구현합니다. Human 클래스는 이 두 가지 역할을 수행할 수 있지만, 모든 인간이 반드시 이 두 가지 역할을 수행해야 하는 것은 아닙니다. 따라서 이 원칙에 따라 Worker와 Eater 인터페이스를 분리함으로써, Human 클래스가 필요한 메서드만 사용하도록 강제할 수 있습니다.

 

ISP는 특히 대형 시스템에서 중요합니다. 각각의 클래스나 인터페이스가 특정 역할에 집중하도록 하면, 시스템 전체의 복잡성을 줄이고 유지 관리성을 높일 수 있습니다. 이 원칙은 객체지향 설계에서 중요한 역할을 하며, 소프트웨어 아키텍처에 큰 영향을 미칩니다.

 

 

 

2023.05.16 - [GD's IT Lectures : 기초부터 시리즈/C, C++ 기초부터 ~] - [C/C++ 프로그래밍 : 중급] C/C++ 중급과정 소개

 

[C/C++ 프로그래밍 : 중급] C/C++ 중급과정 소개

C/C++ 중급과정 소개 [C/C++ 중급과정] Chapter 1. 객체 지향 프로그래밍의 개념 Chapter 2. 클래스와 객체 Chapter 3. 생성자와 소멸자 Chapter 4. 접근 제어 지시자 Chapter 5. 상속 Chapter 6. 다형성 Chapter 7. 가상

gdngy.tistory.com

 

반응형

댓글