티스토리 뷰

자바 언어가 C와 다른 가장 큰 특징이 무엇일까? 여러 가지가 있겠지만 필자는 모든 클래스가 Object를 상속한다는 점을 들겠다. 다른 말로 Object가 모든 클래스의 슈퍼클래스다. 놀라운 일이지만 모든 객체는 Object가 될 수 있다. 좀더 정확히 말하면 Object 타입의 변수에 의해 참조될 수 있다. 스트링이나 새로 만든 학생 객체나 심지어는 int 값도 Object 타입의 변수 obj에 들어갈 수 있다. 

Object obj = null;
obj = "abcde";
obj = 3;
obj = new Student();

이것이 어떻게 가능할까? 자바에서 모든 것은 객체고 객체는 모두 Object 클래스를 상속한 어떤 클래스의 인스턴스다. 그러므로 모든 객체는 Object 타입의 변수에 들어갈 수 있다. 그럼 왜 상속한 클래스의 객체는 obj에 들어갈 수 있는 것일까? 여기서 진짜 자바 언어의 비밀인 업캐스팅이 나온다.

업캐스팅이란 위 방향으로 타입을 바꿀 수 있다(캐스팅)는 뜻이다. 즉 자동 타입변환의 일종으로 모든 객체는 그 객체 클래스의 슈퍼클래스 타입의 객체로 취급될 수 있다. 위에서 마지막 줄의 Student 타입 객체가 슈퍼클래스인 Object 타입 객체로 자동 형변환되어 Object 타입의 변수로 사용될 수 있다(그 타입의 객체로 취급될 수 있다). 우리는 Student 클래스가 Object를 상속한 것을 알고 있다. 그러므로 업캐스팅에 의해 이 객체는 Object 타입의 객체로 취급될 수 있다.

그럼 스트링은 어째서 obj에 지정되는 것일까? 스트링도 클래스고 Object를 상속하므로 스트링 객체도 역시 Object 객체로 취급될 수 있다. 마지막으로 3이 왜 obj에 들어가는지는 오토박싱이라고 하는 개념을 이해해야 한다. 사실 3은 Integer 타입의 객체로 바뀌고(오토박싱) 그 객체가 다시 Object 타입의 객체로 취급된 것이다. 

업캐스팅은 Object에 대해서만 적용되는 것이 아니고 모든 클래스와 인터페이스의 상속에 대해서도 적용된다. 그러므로 우리는 언제든지 상위의 클래스나 인터페이스 타입의 변수에 그것을 상속한 타입의 객체를 지정할 수 있다. 다음 예는 라이브러리 클래스에서 상위 클래스와 인터페이스 타입의 변수로 지정된 예다.

List<String> strList = new ArrayList<String>();
Iterable<String> itr = new ArrayList<String>();
itr = strList;

위의 코드로부터 ArrayList<String>이 List<String>을 상속한 클래스고 List<String>은 Iterable<String>을 상속한 클래스임을 알 수 있다. 이와 같이 복잡한 상속구조 트리에서 최상위 루트는 Object 클래스이고 그것을 상속한 여러 클래스가 있고 또 거기서 다시 상속한 클래스들이 나오게 된다. 이 때 우리는 그 트리에서 주어진 타입보다 상위에 있는 모든 타입으로 업캐스팅이 가능함을 알 수 있다. 다음과 같은 클래스 상속 구조를 생각해 보자. 여기서 네모는 클래스고 타원은 인터페이스를 나타낸다고 가정하자. (인터페이스 때문에 상속 관계도는 트리가 아니고 한 노드가 여러 개의 부모를 가진 그래프가 된다.)

이와 같은 상속구조에서 업캐스팅에 의해 상위 타입의 변수가 상속한 하위 클래스 타입의 객체를 가리킬 수 있다. 

Object obj = null;
Book b = new Book();
PrintBook pBook = new PrintBook();
EBook eBook = new EBook();;
UserBook uBook = new UserBook();; 

obj는 모든 객체들을 다 가리킬 수 있다. obj = b; obj = pBook; obj = eBook; 모두 가능하다. 또한 b는 그것을 상속한 타입의 객체들을 가리킬 수 있다. 그럼 다음 지정문 중에서 타입오류가 나지 않는 것은 어느 것일까?

(1) eBook = pBook;   (2) pBook = uBook;  (3) uBook = pBook;   (4) eBook = uBook;   (5) eBook = b;

답은 이 페이지 제일 아래에 있다.

슈퍼 타입 변수에 하위 타입의 객체를 넣을 수 있는 것이 업캐스팅이라면 그럼 다운캐스팅은 짐작하듯이 그 반대 방향일 것이다. 예를 들어 위에서 Book 타입의 참조인 b를 pBook에 넣으면 어떤 일이 생길까? Book은 PaperBook의 슈퍼 클래스다. 즉 b가 가리키는 것은 Book 객체일 수도 있고 PaperBook 객체일 수도 있으며 또는 EBook이나 UsedBook일 수도 있다. 사실 어느 것일지 알 수가 없다. 우리는 b가 가리키는 객체가 Book이나 그것을 상속한 타입이라는 것만 알지 실제 어느 타입인지는 알지 못한다. 그 이유는 다음과 같은 코드를 보면 알 수 있다.

n = scan.nextInt();
if (n == 1) b = new Book();
else if (n == 2) b = new PaperBook();
else if (n == 3) b = new EBook();

 

... pBook = b; // 컴파일 오류

위의 코드에서 우리는 b가 어떤 타입 클래스의 객체인지 알지 못한다. 실행시켜서 입력을 받아보기 전에는 알수 없다. 즉 b는 업캐스팅에 의해 지정되었으므로 Book일 수도 있고, 상속한 네 개 클래스 중 어느 것이든 될 수 있다. 그러나 문제는 우리가 b가 가리키는 객체를 PaperBook으로 바꾸어서 PaperBook만 가지고 있는 기능을 사용하려고 한다.

예를 들어 종이의 두께나 책의 무게, 책의 판형 등의 정보를 출력하고 싶다. 그러나 b.printPaperBookInfo(); 라고 호출하면 당연히 Book 클래스에는 그런 메소드가 없다 라는 오류메시지가 나올 것이다. 그래서 b를 pBook으로 바꾸고 나면 pBook.printPaperBookInfo(); 처럼 pBook이면 이러한 PaperBook의 속성을 출력하는 메소드를 호출할 수 있을 것이다. 

그런데 pBook = b;는 컴파일 오류가 난다. 이것은 업캐스팅이 아니므로 묵시적으로 타입이 다른 참조에 지정할 수가 없다. 이 경우 우리는 강제형변환을 위해 pBook = (PaperBook)b; 라고 강제 캐스팅을 해주어야 한다. 이것을 하위 클래스 타입으로 변환한다고 해서 다운캐스팅이라고 한다. 여기서 문제가 발생할 소지가 있다. 즉 b가 가리키는 것이 PaperBook이거나 그것을 상속한 타입이면 별 문제가 없으나 예를 들어 EBook이었다면? 캐스팅할 수 없는 타입이라는 오류가 나게 된다. 이것이 다운캐스팅의 심각한 문제다. 강제 타입변환이 불가능한 경우 타입캐스팅 예외가 발생하고 프로그램은 깨지게 된다. (컴파일러는 오류를 내지 않는다)

이것이 어떤 의미인지를 좀더 살펴보자. 변수 b는 Book의 객체를 가리킨다. Book 타입의 객체란 Book 또는 그것을 상속한 클래스의 객체다. 객체는 메모리에 그 클래스의 필드를 가질 영역이 잡히고 데이터를 가지고 있는 인스턴스라고 했다. 여기서 상속 객체의 메모리 구조가 어떻게 생겼는가를 한번 따져보자. Book 타입의 객체가 제목, isbn, 저자, 분야, 출판일 등의 데이터를 가진다면 PaperBook은 종이종류, 책의 무게, 판형, 인쇄일 등의 정보를 추가로 가진다. 메모리의 객체 인스턴스는 앞에 Book 클래스의 필드를 가지고 뒤에 PaperBook에 추가된 필드들을 가지게 된다. 사실 제일 앞에는 Object 클래스의 필드가 들어있다. 즉 슈퍼 클래스의 객체가 가질 필드가 앞에 자리잡고 상속 클래스의 필드들이 그 뒤에 이어서 자리를 잡게 된다. 그러므로 업캐스팅이란 객체의 앞부분만 보고 그 타입의 객체라고 취급하는 것이다. 즉 PaperBook 객체의 앞부분만 보면 Book 객체와 같은 모양과 데이터 종류를 가지므로 그 주소에 있는 인스턴스를 Book 객체라고 생각해도 문제가 없다. 또한 제일 앞에는 Object 클래스의 필드들이 있을 것이므로 Object 타입의 객체로 취급하는 것도 가능하다. 

  • Object 객체의 필드 : 클래스타입, 해시코드, vtable 등
  • Book 클래스 필드 : 제목, isbn, 저자, 분야, 출판일
  • PaperBook 클래스 필드 : 종이 종류, 책의 무게, 판형, 인쇄일

문제는 다운캐스팅이다. 앞쪽만 보고 Book 객체라고 생각하고 있던 b가 가리키는 객체를 이번에는 PaperBook이라고 보라고 강제로 변환하는 것이다. 그런데 만약 그 객체가 PaperBook 타입의 객체가 아니라면? 즉 앞 부분에 Book 뒤로 이어서 PaperBook이 가지는 필드들이 들어있는 것이 아니라면? 우리는 잘못된 타입으로 값을 접근하게 된다. 즉 스트링 데이터를 int로 또는 반대 방향으로 int 데이터를 스트링으로 읽으려고 하는 일이 생긴다. 이러한 문제가 생기는 것을 미리 막기 위해 자바 가상기계는 타입을 확인해 보고 맞지 않으면 TypeCastException을 발생시킨다. 다운캐스팅 자체도 이러한 타입 확인 과정 때문에 비싼 연산일 뿐 아니라 그것이 타입 오류로 인해 예외를 일으킬 가능성이 있다. 가능성이란 프로그래머가 실수를 해서 다른 타입의 객체로 캐스팅하는 가능성이다. 사람이란 실수할 수 있고 프로그램의 논리적 흐름에 대해 개발자가 착각해서 이 타입이 들어있으리라고 생각하고 다운캐스팅을 할 수 있는 가능성이다. 그럼 그것이 컴파일 오류를 일으키지 않고 그냥 넘어갔다가 실행시에 문제가 발생하게 된다. (운좋게 그 타입일 수도 있고 어쩌다 그 타입이 아닐 수도 있다.)

이러한 잠재적인 오류의 가능성 때문에 다운캐스팅은 위험하다고 얘기한다. 물론 타입 오류를 방지하기 위해 다음과 같이 instanceof 를 이용하여 검사할 수 있다. 그러나 이러한 검사는 비쌀 뿐 아니라 매번 어떤 타입인지 검사하고 그에 따라 다른 코드를 가지도록 프로그램을 작성한다면 매우 복잡하고 가독성이 떨어지는 코드가 된다.

if (b instanceof PaperBook) {
    pBook = (PaperBook)b;
    pBook.printPaperBookInfo();
}

업캐스팅은 우리가 프로그램을 작성할 때 공통적으로 처리할 수 있는 부분을 넓혀준다. 즉 다른 클래스인 PaperBook과 EBook에 대해 같은 일을 하는 경우는 Book 타입의 객체로 처리하면 그 메소드나 코드는 두 클래스 객체에 다 적용가능하다. 그런 장점은 가상함수 오버라이딩에 의해 더욱 높아질 수 있다. 코드의 활용도가 높아질 뿐 아니라 경우에 따라 다른 일을 하는 유연성까지 더해진다. 달라지는 부분을 상속으로 처리하되 사용하는 부분은 구별없이 하나의 코드로 사용 가능하다. 이것이 객체지향프로그래밍 언어가 가진 다형성이라는 강력함을 제공한다.

그러나 각자가 다른 일을 해야 하는 경우에는 해당 타입으로 다운캐스팅해야 하는 문제가 발생한다. 상속에 의해 코드의 재사용성을 최대한 높이되 이러한 개별 클래스에 대한 코드를 잘 구성하는 것이 상속을 이용한 코드를 잘 짜는 비결이다.

'자바 프로그래밍' 카테고리의 다른 글

Number 클래스와 오토박싱  (0) 2019.01.19
자바 this 용법  (2) 2019.01.18
자바 싱글톤 패턴  (0) 2019.01.18
객체의 참조와 객체 간의 관계  (0) 2019.01.17
자바 생성자와 초기화 이야기  (0) 2019.01.17
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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 31
글 보관함