[OOP] 다형성과 추상화

세동네·2022년 6월 20일
0
post-thumbnail

· 서론

다형성과 추상화. 객체지향에서 매우 중요하게 여겨지는 개념들이다. 따라서 객체지향을 가르치는 커리큘럼에선 꽤나 초반부터 이 개념들을 가르치지만 쉬운 내용들이 아니다. 특히나 이 개념들이 머리에 잘 남지 않는 이유는 "활용해볼 기회가 적은 개념들"이기 때문이다.

클래스나 포인터 등은 다양한 예제로 사용하고, 여러 프로젝트에서 빈번하게 이용하므로 조금씩 감을 찾아가는 반면에 다형성과 추상화는 작은 규모의 프로젝트나 혼자 개발하는 프로젝트에선 사용하지 않아도 충분히 개발이 가능하다.

이를 위해 다형성과 추상화를 최대한 쉽게 이해할 수 있도록 정리해볼 예정이다.

· 상속

다형성과 추상화를 이야기하면서 빠질 수 없는 것은 함수, 클래스, 그리고 상속이다. 우선 다형성과 추상화의 정의를 보자.

다형성 : 하나의 개체가 여러 의미를 가질 수 있는 성질
추상화 : 비슷한 개념의 데이터나 작업을 묶어 정의해 공통되게 관리하는 것

정의만 보면 무슨 뜻인지 이해하기 힘들 수 있지만, 예시를 들면 다음과 같다. '개'와 '고양이'에 대한 정보와 그 정보를 이용한 작업 등을 묶어 각 클래스를 만들 수 있을 것이다. 코드로 표현하면 다음과 같다.

class Dog {
private:
	std::string name;

public:
	Dog() {
		name = "개";
	}
	Dog(std::string _name) {
		name = _name;
	}

	void Cry() {
		std::cout << "멍멍!!\n";
	}

	void Move(){
		std::cout << name << "이(가) 이동합니다.\n";
    }
    
	void Eat(){
		std::cout << name << "이(가) 먹습니다.\n";
    }
    
	std::string GetName() {
		return name;
	}
};

class Cat {
private:
	std::string name;

public:
	Cat() {
		name = "고양이";
	}
	Cat(std::string _name) {
		name = _name;
	}

	void Cry() {
		std::cout << "왜애앵!!\n";
	}

	void Move(){
		std::cout << name << "이(가) 이동합니다.\n";
    }

	void Eat(){
		std::cout << name << "이(가) 먹습니다.\n";
    }

	std::string GetName() {
		return name;
	}
};

이러한 클래스를 만드는 작업도 각 동물의 개별 데이터나 작업을 묶어 관리하는 것이므로 추상화의 일종이다. 그런데 세상에는 개 말고도 고양이 외에도 수많은 동물들이 있다. 동물마다 특징이 다르고 서로 다른 행동을 하기 때문에 각 동물을 위한 개별 클래스를 만드는 것이 좋을 것이다.

하지만 동물들이 모두 공통으로 지닌 특성들이 있다. 움직이고 음식을 먹어 소화할 것이다. 하지만 동물의 클래스를 만들 때마다 공통으로 가지는 특성을 일일이 정의해주는 것은 그리 효율적인 방법이 아니다. 비슷한 코드를 더 효율적으로 작성하기 위해 객체지향에선 상속이라는 개념을 사용할 수 있다.

엄밀히 말하자면 상속은 '다른 클래스의 기능을 받아 사용하는 것'이다. 하지만 인간의 상속처럼 클래스도 상속을 해주는 클래스를 부모 클래스/기초 클래스(Base class)라 하고, 상속을 받는 클래스를 자식 클래스/파생 클래스(Derived class)라 한다. 상속을 이용하면 클래스 간의 관계를 구축할 수 있다.

상속을 이용하여 개와 고양이 클래스를 정의하면 다음과 같다.

#include <iostream>

class Animal {
private:
	std::string name;
	std::string type;
	std::string sound;
public:
	Animal() {
		name = "홍길동";
		type = "인간";
		sound = "ㅠㅠ";
	}
	Animal(std::string _name) {
		name = _name;
	}

	void Cry() {
		std::cout << name << "이(가) " << sound << "하고 웁니다.\n";
	}

	void Move() {
		std::cout << name << "이(가) 이동합니다.\n";
	}

	void Eat() {
		std::cout << name << "이(가) 먹습니다.\n";
	}

	std::string GetName() {
		return name;
	}

	void SetName(std::string _name) {
		name = _name;
	}

	void SetType(std::string _type) {
		type = _type;
	}
	
	void SetSound(std::string _sound) {
		sound = _sound;
	}
};

class Dog : public Animal {
public:
	Dog() {}
	Dog(std::string _name) {
		SetName(_name);
		SetType("개");
		SetSound("멍멍");
	}
};

class Cat : public Animal {
public:
	Cat() {}
	Cat(std::string _name) {
		SetName(_name);
		SetType("고양이");
		SetSound("왜애앵!!");
	}
};

파생 클래스(Derived Class) DogCat은 기초 클래스(Base Class) Animal을 상속받아 멤버 변수 및 함수를 사용할 수 있다. 이때 기초 클래스의 접근지시자는 유효하며, 외부에는 노출되지 않지만 상속하는 클래스에게만 노출시키고 싶다면 protected 접근지시자를 사용할 수 있다.

· 다형성

다형성 : 하나의 개체가 여러 의미를 가질 수 있는 성질

객체지향에서 다형성은 매우 중요한 개념으로, 다양한 방법으로 다형성을 구현할 수 있다.

- 오버로딩

  • 오버로딩(Overloading)은 기호 또는 식별자가 같은 개체가 다른 성질을 갖도록 재정의하는 것을 말한다. 대표적으로 연산자 오버로딩과 함수 오버로딩을 예로 들 수 있다.
    • 연산자 오버로딩

      정해진 목적으로 사용하는 연산자를 특수한 경우에 다른 목적으로 사용하기 위해 연산자의 역할을 재정의하는 것을 말한다. 연산자 오버로딩의 대표적인 예시는 우선순위 큐에서 원소가 어떤 정보를 기준으로 우선순위를 매길 것인지 결정하기 위한 대소 연산자 < 오버로딩이 있다.
      bool operator < (const Info& info) const {
           return distance > info.distance;
       }
    • 함수 오버로딩

      달성하고자 하는 목표는 똑같지만 반환형이나 매개변수를 다르게 설정할 필요가 있을 때 같은 식별자에 반환형과 매개변수를 다르게 설정하여 함수를 재정의하는 것을 말한다. 대표적인 예로 잠금 해제를 위해 비밀번호와 지문 확인을 하는 각각의 함수가 필요한 경우가 있다.
      bool Login (FingerPrint userFingerPrint) {
           ...
       }
       
       bool Login (string password) {
       	 ...
       }

- 함수 오버라이딩

  • 함수 오버라이딩(Overriding)은 클래스 상속 관계에서 기초 클래스의 함수를 파생 클래스에서 똑같은 형태이지만 내용만 다르게 재정의하는 것을 말한다. 반환형, 식별자, 매개변수 형태가 일치해야 한다.

    #include <iostream>
    
    class Base {
    public:
       Base() { }
    
       void print() {
           std::cout << "Base\n";
       }
    };
    
    class Derived : public Base {
    public:
       Derived() { }
    
       void print() {
           std::cout << "Derived\n";
       }
    };

- 상속(업/다운 캐스팅)

  • 상속 관계에 있는 클래스의 객체 포인터가 자신의 파생 클래스 객체 주소를 가리키는 것을 업 캐스팅, 업 캐스팅된 객체 포인터가 다시 원래 클래스 객체 주소를 가리키는 것을 다운 캐스팅이라 한다.

    기초 클래스가 파생 클래스 객체를 가리킬 수 있게 된다면 기초 클래스에 정의해 놓은 주요 기능을 반복적으로 사용해야 할 때 많은 객체를 생성할 필요 없이 단일, 또는 적은 수의 기초 클래스 객체로 다른 객체를 업 캐스팅하여 호출하는 객체 재사용이 가능하기 때문이다. [OOP] 업 캐스팅, 다운 캐스팅 게시글에서 더 자세한 내용을 확인할 수 있다.

    int main() {
       Derived derived;
       Derived* pDerived = &derived;
    
       Base* pBase = pDerived;	// 업 캐스팅
     }

이러한 다형성의 활용은 업 캐스팅에서 설명한 코드 재사용성 때문이다. 매번 다른 식별자로 개체들을 생성하면 추후 사용 또는 관리에서 어려움을 겪을 수 있어 다형성을 활용하는 것이 유지보수 또는 재사용성 측면에서 유리하다.

· 추상화

추상화 : 비슷한 개념의 데이터나 작업을 묶어 정의해 공통되게 관리하는 것

앞선 클래스 정의, 상속도 추상화의 한 가지 방식이며, 추상화를 통해 개체들은 다형성을 가질 수 있게 된다. 그렇지만 객체지향에서 추상화의 명확한 목적은 '여러 클래스들의 중요하고 공통된 성질을 묶어 기초 클래스로 정의하는 것'이라고 볼 수 있다.

즉, 추상화를 적용한 클래스는 그 자체로 이용하기보단 하위 클래스가 해당 클래스를 최대한 효과적으로 사용할 수 있는 형태로 만드는 것이 적합하다. 객체지향 프로그래밍 언어에서 기초 클래스를 추상 클래스(Abstract class)로 만들게 되면 해당 클래스의 인스턴스 생성이 불가능해지는 것에서 추상화의 본질적인 목적을 확인할 수 있다.

- 추상 클래스

  • 추상 클래스(Abstract class)는 순수 가상 함수를 포함하는 기초 클래스를 말한다. 추상 클래스는 중복 코드를 줄이고 파생 클래스를 그룹화할 수 있다는 상속의 장점과 더불어 파생 클래스들의 기초 역할'만' 하고 그 자체가 기능을 하는 개체가 되어선 안 되는 기초 클래스를 엄격하게 통제할 수 있다.

    순수 가상 함수는 기초 클래스에서 가상 함수를 구현 없이 선언만 하고 파생 클래스에서 그 내용을 구현하도록 정의한 가상 함수를 뜻하며, 아래와 같이 작성한다.

    virtual void func() = 0;

virtual 키워드를 이용한 가상 함수에 대한 내용은 이 포스팅에서 확인할 수 있다. 위와 같이 가상 함수에 = 0이라는 구문을 추가해주면 된다. 이는 0을 대입하겠다는 것이 아닌 함수 내부를 구현하지 않겠다는 선언과도 같다.

유의해야 할 점은 기초 클래스가 순수 가상 함수를 포함하면 파생 클래스들은 '반드시' 순수 가상 함수를 오버라이딩해야 한다는 것이다.

순수 가상 함수를 오버라이딩하지 않으면 위처럼 에러를 일으킨다. 순수 가상 함수의 오버라이딩 방식은 일반 가상 함수를 오버라이딩하는 것과 똑같다.

void func() override {
	std::cout << "func\n";
 }

또한 순수 가상 함수를 포함한 추상 클래스는 객체를 생성할 수 없다.


· 참고

[나무위키] 상속
[추상화와 추상 클래스]

0개의 댓글