강좌 Course 1. Part 4. ch2, ch3 요약
지금까지는 데이터의 상태정보에 초점을 맞추었기 때문에 이제는 클래스의 동작, 행위정보에 초점을 맞추어 설계 및 상속하는 법을 알아보자.
동작적인 측면에서도 수평적 구조와 수직적 구조를 살펴볼 수 있다. 수평적인 구조에서 비슷한 동작이 중복될 수 있기 때문에 중복을 최소화하고자 수직적인 구조가 필요하다.
// 수평적 구조(동작)
// 예시: Dog 클래스
public class Dog {
public void eat(){
System.out.println("Eat like a dog");
}
}
// 예시: Cat 클래스
public class Cat {
public void eat(){
System.out.println("Eat like a cat");
}
public void night(){
System.out.println("Their eyes shine in the night");
}
}
// 예시: CatDogTest
import fc.java.model.*;
public class CatDogTest {
public static void main(String[] args) {
Dog d = new Dog();
Cat c = new Cat();
d.eat();
c.eat();
c.night();
}
}
코드의 중복을 줄이기 위해 중복된 동작을 모아 부모 클래스를 만들고, 자식 클래스들이 부모 클래스를 상속하게끔 하여 수직적 구조를 설계할 수 있다. 예시에서는 Dog 클래스와 Cat 클래스가 eat() 동작을 공유하고 있으므로, eat() 동작을 가진 부모 클래스 Animal을 만들어본다.
// 수직적 구조
// 예시: Animal 클래스
public class Animal {
public void eat(){
System.out.println("Eat like an animal"); // 추상적
}
}
// 예시: Dog 클래스
public class Dog extends Animal{}
// 예시: Cat 클래스
public class Cat extends Animal{
public void night(){
System.out.println("Their eyes shine in the night");
}
}
상속을 사용하면, Dog 클래스와 Cat 클래스가 Animal 클래스의 eat()을 실행하는 것을 볼 수 있다.
개인이 소스코드를 작성하고 실행하면, .java 파일과 .class 파일을 전부 가지고 있어 어떤 클래스가 어떤 동작을 가지고 있는지 파악할 수 있다. 하지만 다른 사람이 만든 .class를 전달받아 사용할 경우, 소스코드가 없어 어떤 동작이 구현되어 있는지 파악하기 힘들다. 이러한 문제점을 해결하기 위해 실행 파일과 .class 파일 사이에 인터페이스 역할을 하는 단계를 추가하여 설계 및 배포해야 한다. 이 인터페이스 단계를 상속 관계인 부모 클래스가 담당하기 때문에 때문에, 상속은 필수로 익혀야 한다.
API를 받아 사용하는 경우, 이렇게 부모 클래스를 보고 하위 클래스에 어떤 동작이 있는지 파악한다.
Animal x = new Dog();
x.eat();
생성하는 객체는 Dog, Cat같은 하위 클래스여도 상위 클래스로 받을 수 있다. 더 큰 타입이 더 작은 타입을 받기 때문에 자동으로 형변환이 일어나며, 부모가 자식을 가리키는 이 객체생성방법을 업캐스팅이라고 한다(=부모를 통해 자식을 구동할 수 있다). 업캐스팅으로 객체를 생성하면 하위 클래스의 동작은 사용할 수 없으며, 상위 클래스의 메모리만을 사용할 수 있기 때문에
Animal x = new Dog();
x.eat(); // "Eat like an animal"
Animal y = new Cat();
y.eat(); // "Eat like an animal"
원래 Dog, Cat의 eat() 동작 결과인 "Eat like a dog", "Eat like a cat"을 얻지 못한다.
이 문제를 해결하려면 다음 파트로 넘어간다.
상속 체이닝은 맨 위 부모 클래스부터 객체가 생성되어 자식까지 연결되는 구조를 말한다. 위의 Animal은 Dog의 부모 클래스이지만, Animal은 동시에 최상위 클래스인 Object의 자식이기도 한 것이다. 자바는 처음 생성자가 호출된 자식 클래스에서 시작하여, 더 이상 부모 클래스가 존재하지 않을 때까지 상속관계를 타고 올라가 가장 위에 있는 부모 클래스 객체부터 생성을 한다.
super는 상속(1)에서 살펴보았던 것처럼 상위 클래스의 생성자 메서드를 호출한다. 생성자 메서드에서 가장 첫 문장(First Statement)에 사용해야 하며, 상위 클래스의 기본생성자를 호출하는 super()은 생략되어 있다.
업캐스팅을 하면서도 하위 클래스의 동작을 사용하려면, 하위클래스가 상위 클래스와 공유하는 동작을 재정의하는 과정이 필요하다. 이 과정을 Override라고도 한다. (@Override라는 애너테이션을 붙여도 되고 안붙여도 된다.)
업캐스팅을 사용한 상태에서 동작을 실행하면 하위 클래스에서 동작을 재정의하였는지 체크하고, 만일 오버라이드가 되어 있다면 재정의된 동작을 실행한다. 실행 시점에서 사용될 메서드가 결정되는 이 바인딩 방법을 동적 바인딩이라고 한다. 컴파일 시점에서는 부모클래스의 동작을 실행할지, 자식클래스의 동작을 실행할지 정해져 있지 않고, 실행을 해야 알 수 있다.
재정의는 상속관계에서 하위 클래스가 상위 클래스의 동작에 기능을 추가하거나 변경하는 행위를 말한다. 동작 재정의는 자식 클래스에서 재정의할 동작을 작성하면 된다.
// 예시: Dog eat() 재정의
public class Dog extends Animal{
public void eat(){
System.out.println("Eat like a dog");
}
}
// 예시: Cat eat() 재정의
public class Cat extends Animal{
public void eat(){
System.out.println("Eat like a cat");
}
}
재정의한 후 업캐스팅하여 eat()을 실행하면, 더이상 Eat like an animal이라는 문자열이 출력되지 않는 것을 볼 수 있다.
요약하면,
1. 상속관계일 때 부모 클래스로 자식 클래스를 받는 업캐스팅이 가능하다.
2. 자식 클래스에서 부모 클래스의 동작을 재정의할 수 있다.
3. 어떤 동작이 실행될지는 동적 바인딩으로 실행 시점에 결정된다.
앞에서 업캐스팅을 알아보았다. 업캐스팅 말고도 다운캐스팅이 있다. 부모 클래스는 여러 자식 클래스를 둘 수 있으며, 다운캐스팅은 반대로 상위클래스의 타입을 하위클래스의 타입으로 바꾸는 행위로, 더 작은 클래스에 더 큰 클래스가 들어가기 때문에 강제 형변환이 일어난다.
이렇게 말하면 사실 처음에는 잘 와닿지 않아서, 다운캐스팅은 서로 다른 특성을 가진 자식에게 맞추기 위하여 부모가 각기 다른 태세를 취한다고 생각할 수 있을 것 같다.
강제 형변환은 객체 앞에 자료형을 명시해주어야 한다.
// 예시: 다운캐스팅
Dog a = (Dog)Animal;
Cat b = (Cat)Animal;
예시에서 Dog와 Cat은 eat() 메서드를 재정의해 업캐스팅으로 각기 다른 문자열을 출력할 수 있다. 하지만 Cat의 night()는 Animal에 없기 때문에 Animal로 호출했을 때 이 메서드의 실행이 불가능하다.
다운캐스팅을 했을 때는 night()를 호출할 수 있다.
public class ObjectCasting {
public static void main(String[] args) {
Animal a = new Cat();
Cat c = (Cat)a;
c.night();
}
}
그냥 Animal 객체를 만들어서 다운캐스팅하면 되는 거 아닌가? 하고 만들었더니 오류가 났다. 다운캐스팅을 하려면 업캐스팅을 해서 만든 객체에 적용해야 한다.
저 세 줄을 한 줄로 줄일 수 있는데, 이때 주의해야 할 것은 참조 연산자 .의 우선순위가 형변환보다 높다는 것이다. 그래서 이를 시정해주지 않으면 캐스팅이 되기 전에 참조연산자가 실행되기 때문에 오류가 날 수 있다.
// 한 줄로!
((Cat).a).night();
괄호로 한번 더 묶어서 캐스팅이 먼저 일어나도록 바꾸어주어야 한다.