SturdyCobble's Study Note

[프로그래밍] 5.5 클래스(5) - 클래스의 상속 (2) 본문

휴지통/['19.06-'20.07]프로그래밍101

[프로그래밍] 5.5 클래스(5) - 클래스의 상속 (2)

StudyingCobble 2020. 1. 9. 20:29

NOTICE : 독학인 만큼, 잘못된 내용이 있을 수 있습니다. 내용 지적은 언제나 환영합니다.

더 이상 이 블로그에는 글을 올리지는 않을 예정입니다. 그렇지만 댓글을 달아주시면 최대한 답변드리고자 노력하겠습니다.


※이 글은 프로그래밍 언어에 대한 기초적인 이해를 가정하고 있습니다. 최소 프로그래밍 언어 하나 정도를 약간이라도 접해보시는 것을 추천합니다. 또한, 이 글은 심화 내용은 되도록 피하여 서술했습니다.

 오버라이딩과 오버로딩은 비슷하게 생겼지만, 서로 다른 의미를 나타내고 있습니다. 특히 메소드 오버라이딩과 함수 오버로딩은 얼핏 보면 같은 것으로 착각하기 쉽지만 아래와 같은 차이를 지닙니다.

  메소드 오버라이딩 함수 오버로딩
영어 Method Overriding Function Overloading
의미 부모 클래스에서 구현된 메소드를 자식 클래스에서 재정의하는 것(재정의) 함수 이름은 같으나 매개 변수 등의 특징이 다른 여러 함수를 만드는 것(중복 정의)

이번 글에서는 상속에 관해 좀 더 알아보는 것과 함께 생성자 오버라이딩과 메소드 오버라이딩에 대해서 좀 더 알아보겠습니다.

 

 먼저 상속할 클래스의 액세스 지정입니다. 클래스를 쓰고 활용하는 이유 중 하나는 내부의 정보를 숨길 수 있다는 점입니다. 이러한 점은 클래스 상속에서도 유지되어, 자식 클래스가 접근할 수 있는 정보를 제한할 수 있습니다. 이러한 내용은 사실 https://stdcobble.tistory.com/167에서 다룬 바 있지만, C++의 경우 상속하면서도 이러한 액세스 지정이 필요합니다.

class Child : public Parents{ 
public:
 int num1;
}

 이는 다음과 같이 정리해볼 수 있습니다.

 

 

복잡하게 정리해놓았지만, 단순히 이야기하면 상한선을 만들어 놓은다고 보면 됩니다. 

 

 이러한 상속 액세스 지정이 없다면 클래스는 자동적으로 private으로 상속 액세스를 지정합니다. (나중에 언급할 구조체의 경우 public이 기본값입니다.)

 

 

 메소드 오버라이딩은 이미 부모 클래스에서 정의된 메소드를 자식 클래스에서 재정의하는 것을 말합니다. 이는 아예 기능을 갈아 엎기 위함일 수도 있고, 아니면 기능을 확장하기 위함일수도 있습니다. 여기서는 기능을 확장하는 예시로서 살펴보겠습니다.

 

# include <iostream>

using namespace std;

class Parents{
public:
  void exampleMethod(int num1){
    cout << num1 << endl;
  }
};

class Child : public Parents{
public:
  void exampleMethod(int num1, int num2){
    Parents::exampleMethod(num1+num2);
    cout << num1 << num2 << endl;
  }
};

int main(){
  Child ch;
  ch.exampleMethod(10,20);
}

-----------------실행 결과-------------
30
1020

여기서 :: 연산자가 사용되어 부모 클래스의 메소드에 접근했습니다. :: 연산자는 . 연산자처럼(ch.exampleMethod()에서) 멤버가 속한 클래스를 지정합니다. 차이점은 . 연산자는 C++의 경우 객체에만, ::은 C++에서 클래스에만 사용된다는 점입니다. Python이나 Java에서는 . 연산자가 존재하지 않습니다. 

 

이와 같은 방식으로 Java와 Python코드를 짤 것으로 기대할 수 있습니다. 하지만 다음 코드는 오류를 냅니다.

class Parents {
  public void methodExample(int num1){
    System.out.println(num1);
  }

}

class Child extends Parents {
  public void methodExample(int num1, int num2){
    Parents.methodExample(num1+num2);
    System.out.println(num1+","+num2);
  }
}

--------------
ERROR!

이는 Parents.methodExample()의 의미가 C++에서 Parents::methodExample()과는 다르기 때문입니다. C++에서는 non-static한 method도 위 식으로 접근가능했지만, Java는 이를 허용하지 않습니다. 이는 super라는 키워드로 대체됩니다.

 

public class HelloWorld {
    public static void main(String[] args) {
      Child ch = new Child();
      ch.methodExample(10,20);
    }
}

class Parents {
  public void methodExample(int num1){
    System.out.println(num1);
  }

}

class Child extends Parents {
  public void methodExample(int num1, int num2){
    super.methodExample(num1+num2);
    System.out.println(num1+","+num2);
  }
}

------------------------RESULT----------
30
10,20

 

100% 그런다고 장담은 못하지만, Parents가 들어갈 자리에 super를 넣는다고 생각하시면 됩니다. 생성자라면 그냥 super만 써야 겠죠.(생성자 오버라이딩의 경우 저번 글에서 가볍게 다루었습니다.) 다만, super를 두번 써서 super.super()와 같이 쓰는 것은 불가능합니다. 

 

Python도 비슷합니다. 그러나 약간의 차이점이 존재합니다.

 

class parents:
    def someNiceFuction(self,num1):
        print(num1)

class child(parents):
    def someNiceFuction(self,num1,num2):
        super.someNiceFuction(num1*num2)
        print(num1+num2)

ch = child()
ch.someNiceFuction(7,3)

---------------------
ERROR!

위 코드는 에러를 냅니다.

 

아래 코드는 에러가 안 납니다.

class Parents:
    def someNiceFuction(self,num1):
        print(num1)

class Child(Parents,Aparents):
    def someNiceFuction(self,num1,num2):
        super().someNiceFuction(num1*num2)
        print(num1+num2)

ch = Child()
ch.someNiceFuction(7,3)

 

괄호 하나의 차이인것 같지만, Python에서는 괄호가 꼭 필요합니다. 이는 다중상속이 지원되는 Python에서는 super가 단순히 부모 클래스를 의미하는 것으로만 쓰이지 않을 수 있기 때문입니다. 아래의 코드는 족보가 꼬인 예시입니다.('족보'라는 표현을 쓰지는 않지만, 부모-자식 관계여서 족보라고 표현해봤습니다.)

이러한 관계에서 Child의 부모 클래스가 뭐라고 말하긴 애매합니다. 이때 super는 정해진 순서에 따라 가장 가까운 부모나 친척 클래스를 탐방하게 됩니다. 위 상황은 코드로 옮겨봤습니다.

class Gparents:
    def someNiceFuction(self,num1):
        print("Giant Penguin says "+str(num1))

class Parents(Gparents):
    def someNiceFuction(self,num1):
        print("Parents say "+str(num1))

class Aparents(Gparents):
    def someNiceFuction(self,num1):
        print("Aparents say "+str(num1))

class Child(Parents, Aparents):
    def someNiceFuction(self,num1,num2):
        super().someNiceFuction(num1*num2)
        print("Child1 says "+str(num1+num2))
        
ch = Child()
ch.someNiceFuction(7,3)
     

여기서 실행결과는 다음과 같습니다.

Parents say 21
Child says 10

어떤 우선순위에 따라서 Parents 클래스가 우선되었습니다. (제 경우 그랬습니다.) 하지만, super의 사용은 좀 더 복잡합니다. super는 다음과 같이 사용됩니다.

super(type[, object or type])

만약 인수들이 모두 생략되면, 우리가 알다시피 자녀 클래스에서 부모 클래스를 찾게 됩니다. 그러나, 이를 직접 지정할 수도 있습니다. 먼저, 다음과 같이 외부에서 사용하는 것도 가능합니다. 

ch = Child()
super(Parents,ch).someNiceFuction(76)
super(Aparents,ch).someNiceFuction(77)
----------------RESULT-----------
Aparents says 76
Giant Penguin says 77

(이어지는 상황이라고 해봅시다)

위의 결과에서 ch 객체를 기준으로 탐색을 하는데, 두번째 줄은 Parents라는 클래스형을 기준으로 탐색해서 super class를 탐색합니다. 그 결과 Gparents가 아닌 Aparents가 선택되었습니다. 그 다음 줄은 Aparents의 부모 클래스인 Gparents가  선택되었습니다. 이 선택의 기준에 대해서는 나중에 다시 이야기 해보겠습니다.

 

 아직 결과가 이해가 되진 않을 수 있지만, 적어도 다음 코드의 결과를 예상해볼 수 있게 되었습니다.

class Parents(Gparents):
    def someNiceFuction(self,num1):
        print("Parents say "+str(num1))

class Aparents(Gparents):
    def someNiceFuction(self,num1):
        print("Aparents say "+str(num1))

class Child(Parents, Aparents):
    def someNiceFuction(self,num1,num2):
        super().someNiceFuction(num1*num2)
        super(Child,self).someNiceFuction(num1*num2)
        super(Parents,self).someNiceFuction(num1*num2)
        Aparents().someNiceFuction(num1*num2)
        super(Aparents,self).someNiceFuction(num1*num2)
        print("Child says "+str(num1+num2))


ch = Child()
ch.someNiceFuction(7,3)

어떤 걸 기준으로 할 때 뭐가 먼저일지는 모르지만, 위의 결과로 보건데 다음의 결과가 나타날 것이라는 걸 예측할 수 있을 겁니다.

Parents say 21
Parents say 21
Aparents say 21
Aparents say 21
Giant Penguin says 21
Child says 10

 

이러한 탐색 순서는 Method Resolution Order, MRO라고 하며, 다음의 메소드로 확인할 수 있습니다.

print(Child.mro())
-------------------RESULT---------------
[<class '__main__.Child'>, <class '__main__.Parents'>, <class '__main__.Aparents'>, <class '__main__.Gparents'>, <class 'object'>]

 

모든 결정 과정을 설명드리긴 힘들지만, 일반적으로 왼쪽에서 오른쪽, 아래에서 위로 진행합니다. 위의 경우는 다음과 같이 해석될 수 있습니다.

 

그러나 너무 꼬이면 다음과 같이 뜰 수 있습니다.

TypeError: Cannot create a consistent method resolution order (MRO) for bases A, C

 

한 가지 짚고 가면 좋을 점은 MRO의 가장 낮은 우선순위에 object클래스가 있다는 점입니다.(모든 클래스는 Python과 Java에서 Object 클래스를 상속합니다. C++은 int와 같은 자료형 클래스를 상속합니다.)

 

이러한 순서는 생성자, 소멸자에게도 비슷하게 적용됩니다. 하지만, 소멸자의 경우 그 순서가 반대가 됩니다.

 

 

이렇게 super에 맛을 들여버리니 C++에도 super를 쓰고 싶어집니다. 하지만, C++에는 현재로선 super가 없습니다. typedef로 super를 정의해서 super::과 같이 사용하는게 가능하기는 합니다. 다만, 각 클래스마다 부모 클래스를 super로 typedef로 정의해야 하는 번거로움이 있습니다.

 

 

 

 

한편, C++에는 friend라는 독특한 키워드가 존재합니다. 

# include <iostream>

using namespace std;

class Me{
private:
  void pricelessMethod(){
    cout << "cis-Diamminedichloroplatinum(II)" << endl;
  }

  friend class Bfriends;  //C++11의 경우 friend Bfriends; 도 허용
};

class Bfriends{
public:
  void say(){
    Me mf;
    mf.pricelessMethod();
  }
};

int main(){
  Bfriends f;
  f.say();
}

위와 같이 friend class Bfriends; 문장을 통해 (일방적인) 친구 관계가 형성되었습니다. 따라서, private으로 정의된 Method에 접근할 수 있게 됩니다. (참고로 친구의 친구는 친구가 아니고, A가 B를 친구로 생각해도, B는 A를 친구로 생각하지 않을 수도 있습니다. 마찬가지로 자식의 친구는 친구가 아니고, 부모의 친구도 친구가 아닙니다.)

 

friend키워드는 함수에도 사용할 수 있습니다.

# include <iostream>

using namespace std;

class Me{
private:
  string str1 = "Cisplatin";
  void pricelessMethod(){
    cout << "cis-Diamminedichloroplatinum(II)" << endl;
  }

  friend void say(int num1);
};

void say(int num1){
  Me me;
  cout << num1 << " " << me.str1 << endl;
  me.pricelessMethod();
}

int main(){
  say(300);
}

-------------출력 결과--------------
300 Cisplatin
cis-Diamminedichloroplatinum(II)

 

클래스 내부의 메소드에 friend 키워드를 적용하는 것도 가능합니다. 단, 서로 일부분에만 적용하는 것은 불가능하며, 한쪽은 일부, 한쪽은 전체에 적용해야만 합니다.

 

예시 코드는 다음과 같습니다.

class Me{
friend class Myfriend;
public:
  string str1 = "Cisplatin";
  void printer();// 사용하려면 먼저 선언되어야 해서 미리 선언만 했습니다.
};

class Myfriend{
friend void Me::printer();
private:
  void lineMake(){
    cout << "-----------" << endl;
  }
};

void Me::printer(){
  Myfriend mf;
  cout << "cis-Diamminedichloroplatinum(II)" << endl;
  mf.Myfriend::lineMake();
}

int main(){
  Me me;
  me.printer();
}
================RESULT================
cis-Diamminedichloroplatinum(II)
-----------

 

이러한 friend 키워드는 간편해보이지만, 어떻게 보면 객체 지향적 프로그래밍과 대치되는 모습(접근을 제한하기는 커녕 private으로 처리한 내용까지 접근을 허용함)을 보여주기에, 되도록 사용하지 않는게 좋으며 실제로 다른 언어에서도 존재하지 않는 경우가 많습니다.

Comments