📒 객체 지향 프로그래밍의 특징
📌 객체란?
- 객체 지향 이론에서는 사물과 같은 유형적인 것뿐만 아니라, 개념이나 논리와 같은 무형적인 것들도 객체로 간주한다.
- 프로그래밍에서의 객체는 클래스에 정의된 내용대로 메모리에 생성된 것을 뜻한다.
📌 객체 지향 프로그래밍이란?
- 객체들의 유기적인 협력과 결합으로 파악하고자 하는 컴퓨터 프로그래밍의 패러다임
- 예를 들어, 컴퓨터를 만든다고 할 때 CPU, 그래픽 카드 등 여러 부품들을 결합하여 하나의 컴퓨터를 만드는 것과 같다.
- 즉, 여러 작은 부품 객체를 만들고 이것들을 조합하여 하나의 기능을 하는 프로그램을 만드는 프로그래밍 방법론을 뜻한다.
📌 추상화(Abstration)
- 추상이란 공통성과 본질을 모아 추출한 것을 의미한다.
class IVehicle {
public:
virtual void moveForward() = 0;
virtual void moveBackward() = 0;
virtual ~IVehicle() = default;
};
class Car : public IVehicle {
public:
virtual void moveForward() {
}
virtual void moveBackward() {
}
void openWindow() {
}
};
class Bike : public IVehicle {
public:
virtual void moveForward() {
}
virtual void moveBackward() {
}
};
- 자동차와 자전거의 공통성과 본질을 추출하여 IVehicle이라는 상위 클래스에 정의하였다.
- IVehicle 클래스에서 규칙을 정의하고, 정의한 규칙에 따라 Car와 Biek 클래스에서 구현하도록 하고 있다.
- 이것을 객체 지향 프로그래밍에서는 '역할과 구현의 분리'라고 하며, 이러한 규칙을 잘 지켜 설계한다면 결합도가 낮아져 교체가 용이하고 확장성이 좋아진다.
📌 상속(Inheritance)
- 상속이란 기존의 클래스를 재활용하여 멤버변수 및 멤버함수 등을 새로운 클래스에게 물려주는 것
- 즉, 상위 클래스에 구현된 것들을 물려받아, 확장시킬 수 있다.
- 클래스들 간 공통된 기능들을 반복적으로 정의하지 않고 재사용할 수 있다.
#include <iostream>
using namespace std;
class Vehicle {
public:
virtual void moveForward() {
cout << "Forward" << endl;
}
virtual void moveBackward() {
cout << "Backword" << endl;
}
};
class Car : public Vehicle {
public:
void openWindow() {
cout << "open the window" << endl;
}
};
class Bike : public Vehicle {
public:
virtual void moveForward() override {
cout << "bike forward" << endl;
}
void stunt() {
cout << "raise the front wheel" << endl;
}
};
int main() {
Car car;
Bike bike;
car.moveForward();
car.openWindow();
bike.moveForward();
bike.moveBackward();
bike.stunt();
return 0;
}
Output:
Forward
open the window
bike forward
Backword
raise the front wheel
- 위 예제를 보면 Car와 Bike의 공통된 기능을 상위 클래스(Vehicle)에 정의하였고, 그것을 상속받아 나머지 기능을 확장해 나갔다. 또한, 원래 있던 기능을 맥락에 맞게 재정의하기도 하였다.
- 이렇듯, 상속을 통해 반복적인 코드를 줄일 수 있고, 상위 클래스에서의 단 한번의 수정으로 모든 클래스에 변경 사항이 반영될 수 있다.
- 앞서 봤던 '추상화'에서는 상위 클래스에서 부모가 가진 함수의 구현을 강제할 수 있지만, 상속은 그렇지 않다.
- 즉, 상속에 경우 인터페이스를 사용하는 구현에 비해서는 추상화의 정도가 낮다고 할 수 있다.
📌 다형성(Polymorphism)
- 다형성이란 어떤 객체의 속성이나 기능이 상황에 따라 여러 가지 형태를 가질 수 있는 성질을 의미한다.
- 예를 들어, 축구선수 손흥민은 국가대항전에서는 한국 대표 선수, 소속 리그에서는 소속팀의 선수, 가정에서는 아들의 역할을 할 것이다. 이렇듯, 상황과 환경에 따라 달라지는 것과 비슷한 맥락이다.
- 프로그래밍에서는 대표적으로 함수 오버라이딩(overriding)과 함수 오버로딩(overloading)이 있다.
#include <iostream>
using namespace std;
class Vehicle {
public:
virtual void moveForward() {
cout << "전진" << endl;
}
virtual void moveBackward() {
cout << "후진" << endl;
}
};
class Car : public Vehicle {
public:
virtual void moveForward() override {
cout << "자동차 전진" << endl;
}
virtual void moveBackward() override {
cout << "자동차 후진" << endl;
}
};
class Bike : public Vehicle {
public:
virtual void moveForward() override {
cout << "자전거 전진" << endl;
}
virtual void moveBackward() override {
cout << "자전거 후진" << endl;
}
};
int main() {
Vehicle vehicle;
Car car;
Bike bike;
vehicle.moveForward();
car.moveForward();
bike.moveForward();
vehicle.moveBackward();
car.moveBackward();
bike.moveBackward();
return 0;
}
Output:
전진
자동차 전진
자전거 전진
후진
자동차 후진
자전거 후진
- 오버라이딩을 활용하여 부모 클래스에 있는 함수를 맥락에 맞게 재정의하여 사용하였다.
- 오버라이딩, 오버로딩 또한 다형성의 중요한 예시이지만, 객체 지향 맥락에서 이것보다 더 중요한 정의는 "상위 클래스 타입의 참조 변수로 그것과 관계있는 하위 클래스들을 참조할 수 있도록 하는 것"이다.
#include <iostream>
using namespace std;
class Vehicle {
public:
Vehicle() {
cout << "부모 생성자" << endl;
}
virtual ~Vehicle() {
cout << "부모 소멸자" << endl;
}
virtual void moveForward() {
cout << "전진" << endl;
}
};
class Car : public Vehicle {
public:
Car() {
cout << "자식 생성자" << endl;
}
~Car() {
cout << "자식 소멸자" << endl;
}
virtual void moveForward() override {
cout << "자동차 전진" << endl;
}
};
int main() {
Vehicle* car = new Car();
car->moveForward();
delete car;
return 0;
}
Output:
부모 생성자
자식 생성자
자동차 전진
자식 소멸자
부모 소멸자
- 위 코드를 보면 부모 타입으로 자식 객체를 생성하였다.
- 이처럼 하나의 타입만으로 여러 가지 타입의 객체를 참조할 수 있게 되다 보니, 간편하고 유연하게 코드를 작성할 수 있다.
- 또한 위 코드를 보면 가상 소멸자를 사용하였는데, 만약 부모 클래스의 소멸자에 virtual 키워드를 붙이지 않았다면 자식 소멸자가 호출되지 않아 메모리 누수가 생길 수 있다.
- 만약 자식 객체의 멤버로 동적 할당된 멤버가 있고 그것을 소멸자에서 처리해 주는데, 부모 타입의 소멸자만 호출된다면 자식 객체의 멤버로 남아있는 메모리는 누수될 것이다.
📌 캡슐화(Encapsulation)
- 캡슐화란 클래스 안에 서로 연관된 속성과 기능들을 하나의 캡슐 형태로 만들어 데이터를 외부로부터 보호하는 것을 의미한다.
- 예를 들어, 우리가 먹는 캡슐 알약을 생각해 볼 수 있는데, 우리 입장에서는 캡슐 안에 어떤 성분이 있는지 알 수 없다. 또한 안의 내용물은 캡슐을 통해 외부로부터 오염되지 않도록 보호받을 수 있다.
- 프로그래밍에서의 캡슐화 또한 이와 같다. 즉, 외부로부터 내부에 정의된 속성과 기능들을 보호하고, 필요한 부분만 노출하므로 인해 객체간의 책임을 명확히 할 수 있다.
- C++에서는 사용하는 class는
public
, protected
, private
이렇게 세 가지의 접근지정자로 접근 영역을 구분시킬 수 있다.
#include <iostream>
using namespace std;
class Car {
public:
int engine = 0;
void openWindow() {
}
};
int main() {
Car car;
engine = 1;
car.openWindow();
return 0;
}
- 위 코드를 보면, Car 클래스에서 자동차의 엔진과 관련된 변수, 창문 열기를 동작하는 함수가 있다.
- 그런데 엔진 관련된 것들이 운전자에게 의미가 있을까 하는 의문이 든다. 또한 엔진 관련하여 잘못 조작하는 위험성 또한 존재한다.
- 그 때문에 접근지정자를 활용하여 접근에 제한을 두는 것이 좋다.
- 또한 캡슐화는 또 다른 핵심 이점이 있는데, 아래 예제를 살펴보자.
#include <iostream>
using namespace std;
class Car {
private:
string model = "";
string color = "";
public:
Car(string _model, string _color) : model(_model), color(_color) {}
void showCarInfo() {
cout << "model : " << model;
cout << ", color : " << color << endl;
}
void onEngine() {
cout << "on engine" << endl;
}
void drive() {
cout << "drive" << endl;
}
};
class Driver {
private:
string name = "";
Car* car;
public:
Driver(string _name, Car* _car) : name(_name), car(_car) {}
void moveForward() {
car->showCarInfo();
car->onEngine();
car->drive();
}
};
int main() {
Car* car = new Car("Benz The new E-Class", "Black");
Driver* driver = new Driver("jin", car);
driver->moveForward();
delete car;
delete driver;
return 0;
}
Output:
model : Benz The new E-Class, color : Black
on engine
drive
- 위 코드를 보면 Dirver 클래스의 moveForward 함수에서 Car의 모든 함수를 실행하고 있다.
- 그런데 만약, Car 클래스의 3가지 함수들을 수정해야 한다고 가정해 보자.
- 그렇게 되면 Dirver 클래스의 moveForward 함수 또한 수정이 불가피하다.
- 즉, Dirver 클래스가 Car 클래스의 내부 로직에 대해 너무 직접적으로 알고 있기 때문이다.
- 이것을 객체 간의 결합도가 높다고 표현하는데, 이럴 때는 캡슐화를 이용하여 객체가 해당 객체의 속성과 기능에 대한 한정적인 책임만 담당하게 만들고, 이를 통해 객체 간의 결합도를 낮게 만드는 것이 좋다.
#include <iostream>
using namespace std;
class Car {
private:
string model = "";
string color = "";
void showCarInfo() {
cout << "model : " << model;
cout << ", color : " << color << endl;
}
void onEngine() {
cout << "on engine" << endl;
}
void drive() {
cout << "drive" << endl;
}
public:
Car(string _model, string _color) : model(_model), color(_color) {}
void operate() {
showCarInfo();
onEngine();
drive();
}
};
class Driver {
private:
string name = "";
Car* car;
public:
Driver(string _name, Car* _car) : name(_name), car(_car) {}
void moveForward() {
car->operate();
}
};
int main() {
Car* car = new Car("Benz The new E-Class", "Black");
Driver* driver = new Driver("jin", car);
driver->moveForward();
delete car;
delete driver;
return 0;
}
Output:
model : Benz The new E-Class, color : Black
on engine
drive
- 결과는 아까와 동일하지만, Car 클래스의 함수들을 operate 함수에 묶어 관리하도록 하고, Dirver 클래스에서는 Car의 내부 동작을 전혀 신경 쓰지 않고 단순히 operate 함수만 호출하여 사용하도록 하였다.
- 또한, Car 클래스의 다른 함수들은 외부에서 사용될 일이 없으므로
private
로 변경해 주었다.
- 이에 따라서, 이제 Driver 클래스는 Car 클래스에 대한 로직을 알 수 없고, 알 필요도 없어졌다.
- 이렇듯, 캡슐화를 활용하면 객체 내부의 동작을 외부로의 노출을 최소화하여 각 객체의 자율성을 높이고, 객체 간의 결합도를 낮추어 유지보수가 용이해진다.
📌 객체 지향 프로그래밍의 장단점
장점
- 잘 설계된 클래스를 통해 독립적인 객체를 활용하여 개발의 생산성을 향상할 수 있다.
- 일상생활에서 쓰는 개념을 토대로, 객체라는 구조로 표현하여 개발함으로써 설계한 것을 그대로 구현할 수 있다.
- 모듈 단위로 수정이 가능하기 때문에 유지 보수에 용이하다.
- 코드를 재사용성이 높다.
단점
- 처리 속도가 상대적으로 느리다.
- 객체 단위로 프로그램을 만들다 보면, 불필요한 정보들이 같이 삽입될 수 있기 때문에 용량이 커질 수 있다.
- 클래스, 객체, 상속 등의 구조를 설계해야 하므로 설계 단계에서부터 많은 시간이 소모된다.