티스토리 뷰

상속과 가상함수에 대해 얘기하기 위해서는 클래스가 무엇인지 알아야 한다. 클래스는 데이터 필드와 그에 대한 메소드를 정의하는 것이다. 예를 들어 사람 클래스라면 이름, 나이, 성별, 주소를 가진다. 그리고 그 메소드로 자기소개를 하거나(intro) 집에 가거나(goHome) 나이를 한 살 더 먹는다(getOlder). 즉 메소드란 그 사람의 데이터로 무슨 일을 하거나 필드의 값을 바꾸는 것이다. 예를 들어 intro는 그 사람의 이름과 나이를 소개하고, goHome()은 그 사람의 주소로 가는 것이다.

클래스 객체란 클래스의 데이터 모음을 얘기하는데, 메모리에 그 데이터를 저장할 메모리가 생기면 객체가 생겼다고 한다. 사람이 열명이면 이름, 나이, 성별도 열 개가 필요하다. 즉 객체가 열개가 생겨야 하는 것이다. 또한 메소드는 동작이고 객체를 주어로 가진다는 점이다. 즉 홍길동이라는 사람이 있다면 그 사람이 자기 이름과 나이를 "소개"한다. 즉 메소드는 주어(객체)가 있고 그 객체(this)의 데이터로 메소드를 수행한다. (static 메소드는 객체가 없이 불려질 수 있는 함수다. 즉 this가 필요하지 않은 메소드다.)

상속을 통해서 사람 클래스에서 특수한 부류의 사람을 나타낼 수 있다. 사람을 상속한 클래스로 학생이나 직장인이 있을 수 있다. 예를 들면 학생 클래스는 사람이 가지는 데이터 이외에 학교와 학년을 가진다. 그리고 직장인이라면 직장과 업무 등을 가진다. 그러면 이제 "intro(소개하다)" 메소드에 대해 생각해 보자. 사람의 intro 메소드는 이름과 나이를 말하고 학생은 여기에 추가로 학교와 학년을 말한다고 하자. 학생 클래스는 사람을 상속해서 사람의 필드와 메소드를 물려받는데, "intro()"라는 메소드를 물려받지만 사람과는 다르게 소개를 해야 한다. 학생은 사람이 하는 소개 이외에 추가로 학교와 전공을 말한다. 이것을 다음 그림과 같이 나타내 볼 수 있다. 하늘색 네모는 클래스고 상속관계를 나타낸다. 노란색 둥근네모는 객체를 나타내고 파란색 말풍선은 intro() 메소드가 출력하는 내용을 보여준다.

그러면 우리는 학생이 슈퍼클래스인 사람의 intro() 메소드를 오버라이드한다고 얘기한다.

class 학생 extends 사람 { 
	String 학교; 
	int 학년; 
	@Override 
	void intro() { 
		super.intro(); 
		System.out.println(학교+학년); 
	} 
} 

위의 코드처럼 오버라이드하는 메소드 앞에는 @Override 라고 써준다. 오버라이드 메소드는 슈퍼가 가진 메소드와 시그너처(접근자, 반환형, 함수명, 매개변수)가 동일해야 한다. 직장인도 intro를 오버라이드한 메소드를 가진다. 

class 직장인 extends 사람 { 
	String 학교; 
	int 학년; 
	@Override 
	void intro() { 
		super.intro(); 
		System.out.println(직장+업무); 
	} 
} 

그럼 가상함수란 무엇일까? 가상함수는 오버라이드된 함수를 호출하는 방식에 관한 이야기로 객체지향의 중요한 특징 중 하나인 다형성의 측면에서 볼 수 있다. 다형성은 동일한 코드로 경우에 따라 다른 동작을 하게 되는 것을 말한다. 가상함수의 다형성은 슈퍼클래스의 참조가 상속한 객체를 모두 가리킬 수 있다는 업캐스팅을 이용하여 슈퍼클래스를 오버라이드한 메소드 여러 개를 표시할 수 있는 것을 의미한다.

사람 person;
if ( ... )
	person = new 학생();
else if (...)
	person = new 직장인();
person.intro();   // 다형성에 의한 호출

person.intro();라고 할 때 person 변수가 가리키는 객체는 사람, 학생, 직장인일 수 있다. 두 클래스에 대해 오버라이드한 intro() 메소드가 정의되어 있으므로 person의 intro()는 어떤 클래스 객체냐에 따라 세 개의 intro() 메소드 중에 하나가 동작하게 된다. 코드 상으로는 동일하게 사람의 intro() 호출인 것처럼 보이지만 실제로 실행될 때 코드는 객체가 사람이면 사람의 intro(), 학생이면 학생의 intro(), 직장인이면 직장인의 intro()가 호출된다. 이것을 가상함수 호출이라고 한다. intro()라는 메소드 호출에 대해 실행 시점에 호출된 객체의 타입을 보고 어떤 메소드가 호출될 것인지 결정하는 것을 가상함수라고 한다.

다형성은 하나의 코드가 타입에 따라 다르게 해석되어 다른 일을 할 수 있는 것을 말한다. 즉 호출하는 쪽에서 사람 변수 person이 가리키는 객체가 어느 것인지 구별할 필요없이 하나의 코드로 작성할 수 있다. 사람이 "자신을 소개"하는데 그 사람이 어떤 사람이냐에 따라 다르게 소개할 것이다. 이렇게 똑같이 "소개"라고 하지만 다형적인 의미를 가져서 다 다른 일을 하게 되는 것을 다형성이라고 할 수 있다. 즉 메소드 호출이 동일한 이름으로 각 클래스마다 다른 일을 할 수 있고 사용부에서는 메소드를 호출할 때 if로 구별하지 않고 다중적인 의미를 가지는 코드가 될 수 있다. 상속 클래스의 작성에서 코드의 재사용이 가능하게 해주는 중요한 개념이다. 

동적바인딩이라는 말은 언어의 구현 관점에서 실행시점에 뭔가를 결정하는 것을 얘기한다. 프로그램에서 실행시키기 전에 결정되지 않는 것을 동적바인딩이라고 한다. 예를 들면 동적할당 주소나 변수의 값, 함수호출스택 등은 동적으로 결정된다. 여기서는 참조 변수에 대해 메소드를 호출할 때 실제로 호출되는 메소드가 어떤 것인지를 결정하는 방법을 의미한다. 가상함수의 호출에서 (상속한 여러 클래스에서 오버라이드한 동일한 함수가 여러 개 있을 때) 어느 함수를 호출하는가는 실행시점에  (동적으로) 결정한다는 의미다. 위 코드 예에서 person이 가리키는 객체가 그냥 사람 클래스인지, 학생인지, 직장인인지를 컴파일러는 (또는 코드를 작성한 사람도) 알지 못한다. 그것은 입력값에 따라 어떤 객체인지가 결정될 수 있다. 그 경우 person은 세 가지 타입의 객체가 모두 될 수 있는 다중적인 변수이고 그것의 person.intro() 호출도 역시 다중적인 의미를 가진다. 

정리해 보면 다음과 같이 개념의 관계를 나타낼 수 있다.

  • 오버라이딩이란 상속에서 슈퍼클래스의 메소드를 동일한 시그너처로 재정의하는 것이다.
  • 오버라이딩된 함수는 호출할 때 가상함수로 호출되어 객체의 타입에 따라 해당 함수가 불려지는 방식으로 다형성을 가진 코드가 될 수 있다.
  • 가상함수는 실행시점에 동적바인딩에 의해 어떤 메소드 코드를 호출해야 할지 결정하는데, 이 때 실제 객체의 타입에 따라 어느 메소드가 불려져야 할지를 가지고 있는 vtable을 이용하게 된다.

다음 그림은 이러한 오버라이드관계와 그에 대응하는 vtable을 보여준다. 제일 왼쪽의 박스는 각 클래스에 정의된 함수 코드(.java)를 나타내고 가운데는 클래스별 vtable을 보여준다. 그리고 오른쪽 열은 바이트코드로 컴파일된 함수의 코드 부분을 나타낸다. 각 vtable은 자신으로부터 Object 까지의 모든 조상들까지 정의된 메소드를 전부 가지는데, 자신을 포함해 가장 마지막으로 오버라이드된 코드를 가리킨다. 

그럼 객체의 메소드가 호출되면 각 객체의 제일 앞부분(Object에 해당하는 메모리 영역의 제일 앞)에 이 클래스 vtable을 가리키는 포인터가 있고 거기서 해당 메소드가 가리키는 코드 부분을 실행(함수의 시작 명령문 주소로 점프)하게 된다.

마지막으로 한 가지 언급할 것은 C++은 가상함수로 재정의할 수 있는 함수의 정의부 앞에는 virtual이라고 붙이게 되어 있다. 즉 virtual 키워드가 없으면 일반 함수고 있어야 오버라이드할 수 있는 함수가 된다는 뜻이다. 그러므로 가상함수테이블에는 virtual이 붙은 함수들만 있고 다른 일반 함수는 컴파일할 때 어느 주소의 코드를 실행해야 할지 알고 바로 호출할 수 있다. 메소드를 호출할 때마다 가상함수 테이블을 찾아서 해당 주소로 가야 한다면 실행 시간이 상당히 늦어질 수 있기 때문이다.

자바는 모든 메소드를 가상함수라고 본다. 즉 모든 메소드는 반드시 가상함수 테이블에 가서 주소를 찾은 다음에 호출을 위해 점프할 수 있다. 이것은 당연히 C++에 비해 효율성이 많이 떨어진다. 그러나 자바는 virtual이라든가 다른 키워드를 사용하지 않아도 되고 구별없이 오버라이드할 수 있다. (최근에는 @Override를 써주라고 하는데, 이것은 성능과는 관계없이 컴파일러가 메소드 선언을 슈퍼에 맞게 잘 했는지 검사해 주는 역할과 가독성을 위한 문서화 역할을 한다.[각주:1]  여전히 모든 메소드는 가상함수다.)

그럼 왜 자바는 모든 메소드를 가상함수로 구현할까? 앞에서 얘기한 대로 어느 게 가상이고 어느 게 아닌지 구별없이 무조건 오버라이드할 수 있다. 하위 클래스에서는 무엇이든 자기가 필요한 메소드는 오버라이드해서 사용할 수 있다. 그러므로 상속 클래스를 작성할 때나 코드를 설계할 때 훨씬 자유롭게 (구별없이) 오버라이드를 따로 고려하지 않아도 된다. 

그럼 여기서 또 하나의 이슈, 어떤 메소드를 오버라이드 못하게 하고 싶다면? 그렇게 하고 싶은 이유는 클래스를 작성한 사람이 그 이름의 메소드로 기능이 달라지는 것을 원치 않을 수 있다. 또는 동적바인딩의 성능상의 손실에 대해 우려되어 정적바인딩(컴파일러가 호출 시 바로 메소드 코드의 주소를 넣어서 vtable을 거치지 않게 해 줌)하고 싶은 경우가 될 수 있다.  final 키워드가 떠오를 것이다. final은 컴파일러에 의해 그 메소드를 상속한 클래스에서 오버라이드 못 하게 해주는 역할을 한다. 하지만 구현상으로는 동적바인딩이 된다. private을 쓴다면? private은 상속만 못하는 것이 아니라 호출도 못한다. static 키워드를 사용하면? static 메소드는 상속하는 클래스에서 오버라이드할 수 없다.[각주:2] 또 static 메소드는 정적바인딩된다. 그러나 필자의 생각에는 이것이 원하는 바를 이루지는 못한다. private과 마찬가지로 static은 또 전혀 다른 메소드 사용 방식을 강요한다. 결국 자바에서 내 메소드를 상속하는 클래스가 호출은 할 수 있으나 오버라이딩 못하게 하는 방법은 final 이다. 그러나 어떤 메소드를 정적바인딩되게 하게 하는 뾰족한 방법은 없다. (누구든 final이 아닌 메소드를 상속한 클래스는 마음대로 바꾸어 사용할 권리를 가진다. 자바는 민주적인 상속 체계를 가지고 있다^^)

https://javarevisited.blogspot.com/2015/04/3-ways-to-prevent-method-overriding-in.html

 

  1. 자바 언어는 메소드 오버로딩을 허용하므로 매개변수가 달라지면 다른 함수로 생각한다. 그러므로 흔히 발생하는 실수지만 컴파일러가 잡아주지 못하는 예 중에 하나다. [본문으로]
  2. 아래 링크된 글에서는 private static final 세 가지를 같이 사용하라고 한다. [본문으로]
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함