오늘 수업시간에 상속에 대해 배울 수 있는 기회였습니다!! 음.. 까먹기 전에 인상 깊었던 내용 위주로 정리해보도록 하겠습니다.
상속이 뭘까?
제가 이해하는 상속은 어떤 클래스에 구현된 변수, 메서드를 좀 더 확장하여 사용하는 방법입니다. 이 때 이미 구현된 부분을 부모클래스, 이를 받아 확장하려는 클래스를 자식클래스라고 합니다. extends
예약어를 사용하면 부모클래스를 받으면 부모클래스에 선언된 모든 변수와 메서드를 사용할 수 있습니다. 부모클래스에서 선언된 변수와 메서드를 또 한 번 선언할 필요없이 자식클래스에서 사용이 가능하므로 불필요한 중복을 줄일 수 있습니다.
그럼 불필요한 중복을 줄이기 위한 방법으로 상속 말고는 다른 방법은 없을까?
수업시간에 이러한 질문에 대해 쉽게 대답하진 못했지만 사실 우리 모두가 많이 사용하고 있는 방법이 있습니다... 사실 공통된 부분에 대해 따로 클래스에 모아두고 이것에 대한 객체를 만들어서 사용해도 되는 것이죠. 이러한 방법은 조합(Composition)이라고 합니다.
상속과 조합의 구분과 어떤 상황에 각각 활용할 수 있을까?
사실 현업에서는 비율상으로 상속 보다는 조합을 많이 사용한다고 합니다. 설계 로직이 복잡하고 그만큼 버그 발생 위험도 높기 때문입니다. 하지만 분명 상속이 필요한 경우가 분명히 있고, 기본적으로 자바가 상속 구조로 설계되어 있기 때문에 상속을 잘 이해하는 것이 아주 중요하다고 합니다.
그럼 본론으로 들어와 이 두 개념을 어떻게 구분할 수 있을까요? 기본적으로 상속은 is a 관계(A는 B다), 조합은 has a 관계라고 말하는데요. 예를들어 '개'라는 부모클래스가 있고 자식클래스로 '골드리트리머', '풍산개' 등이 있다면 '풍산개는 개다'라는 is a관계가 성립되므로 상속을 사용하는게 좋다는 것입니다. 조합의 예로는 음.. 이런 상황이 되지 않을까요? '가족'이라는 부모클래스가 있다면 그 안에 '아버지', '어머니', '아들', '딸' 구성원이 있을 수 있는데요. 이 경우엔 조합을 사용할 수 있을 것 같습니다.
어떻게 동작할까?
기본적으로 자식클래스의 객체를 생성하여 특정 메서드를 호출하면 우선 자식클래스의 그 특정 메서드가 정의되어 있는지 확인을 합니다. 만약 자식클래스 내에 그 메서드가 없다면 부모클래스로 타고타고 올라가면서 그 메서드를 찾아갑니다. 만약 결국 찾을 수 없다면 오류가 발생하겠죠. 자바에서 자주 쓰고 보이는 toString()
메서드의 경우도 타고타고 올라가 모든 클래스가 기본적으로 상속받는 Object
클래스 내에서 불러오는 메서드입니다. 이러한 작동원리를 생각해보면 만약 자식클래스에서 Overriding한 메서드가 있다면 부모클래스까지 갈 필요없이 자식클래스에 정의된 메서드로 작동이 됩니다.
public class ParentClass {
public ParentClass() {
System.out.println("ParentClass 호출");
}
public void sayHello() {
System.out.println("안녕하세요");
}
}
public class ChildClass extends ParentClass {
public ChildClass() {
System.out.println("ChildClass 호출");
}
@Override
public void sayHello() {
System.out.println("니하오!");
}
}
위와 같은 클래스들이 정의되어 있다면 자식 객체를 생성하여 sayHello
메서드를 호출하면 아래와 같이 출력이 됩니다.
ParentClass 호출
ChildClass 호출
니하오!
자식클래스에서 Overriding된 sayHello
메서드가 호출된 것을 볼 수 있습니다. 이와 더불어 생성자의 경우는 반대로 부모클래스의 생성자가 먼저 호출되고 자식클래스의 생성자는 이후에 호출됨을 볼 수 있는데요. 이건 왜 부모클래스부터 호출이 될까요? 그 이유는 자식클래스의 생성자 제일 위에 super()
가 생략되어 있기 때문입니다.
super는 어떻게 동작하나?
super는 해당 클래스의 부모클래스를 지칭합니다. this가 해당 클래스를 지칭하기 때문에 반대의 의미이죠. 사실 상속을 받은 객체에서 부모클래스의 메서드는 앞에 super이 생략되어 있고, 해당 자식클래스의 메서드는 this가 생략되어 있을 뿐 알게 모르게 사용되고 있었습니다.
super는 대표적으로 다음과 같은 상황에 명시적으로 써 사용될 수 있습니다. 부모클래스에서 기본 생성자가 따로 정의안되어 있고 매개변수가 있는 생성자만 정의되어 있을 경우 자식클래스에서 오류가 뜹니다. 왜냐하면 기본 생성자 이외에 다른 생성자가 정의되어 있으면 따로 기본 생성자를 안 만드는데 그게 없기 때문이죠. 따라서 이와 같은 상황일 때 부모클래스의 생성자가 문자열을 매개변수로 받는다면 super("brad")
와 같이 기본 생성자 대신 명시적으로 '부모클래스에서 저 형태의 생성자를 호출하라'고 할 수 있습니다. 이때 위치도 중요한데 무조건 자식클래스 생성자의 제일 위에 선언되어야 합니다!!
추상 클래스와 상속
모든 자식클래스에서 공통적으로 처리하는 내용이라면 차라리 부모클래스에서 한번에 처리하는 것이 좋습니다. 그런데 구현하는 내용은 자식클래스에서 각각 해야한다면 어떻게 처리할 수 있을까요? 이 때 사용할 수 있는 것이 추상클래스 개념입니다. 우선 추상클래스를 사용하기 위해선 클래스명 앞에 abstract
라는 예약어가 필요하고 부모클래스 내에 abstract
예약어를 쓴 메서드를 만들면 됩니다. 이 메서드는 따로 {}
를 통해 정의할 필요는 없고 선언만 해둔 뒤 사용을 하고 구현은 자식클래스에서 하면 됩니다. 자식클래스에서 구현이 안되면 오류가 발생한다는 점에서 구현을 강제하는 효과도 있습니다.
부모클래스에 일련의 과정을 처리하려고 하는데 특정 자식클래스 내에 고유한 메서드가 같이 처리되어야 한다면?
이와 같은 경우는 앞서 상속 대신 사용될 수 있는 조합을 고려하는 것이 좋은 방법이 될 수 있습니다.
인스턴스 변수와 상속
자식 클래스에서 super
예약어를 사용하여 super.name = "brian"
과 같이 부모클래스의 인스턴스 변수의 상태값을 바꿀 수 있긴합니다. 하지만 객체지향의 관점과 지난 피드백을 통해 이러한 방법이 좋지 않은 것은 알고 있을 것입니다. 게다가 보통 인스턴스 변수를 private
로 접근 제한할 것이기 때문에 변경도 되지 않습니다.
이러한 경우 값은 전달하고 싶다면 생성자를 사용하는 것이 좋습니다. 앞서 값을 설정할때나 전달하고 싶을 때 차라리 생성자의 매개변수로 전달하는 방법을 배웠는데요. 상속에서도 같습니다!
Casting Down/Up
ParentClass parentClass = new ChildClass();
가 선언이 되면 부모의 타입으로 형변환(업캐스팅)이 됩니다.
이렇게 하면 ParentClass에서 ChildClass에만 있는 메서드 호출할 수 있나?
안됩니다. 이 경우 Casting을 통해((ChildClass)parentClass
) 자식클래스 타입으로 바꾸어주어야 합니다. 이렇게 Casting하여도 정상작동하는데요. 그 이유는 애초에 ChildClass
로 객체 생성되었을 때 형변환 되었다고해도 그대로 그 성질이 남아있기 때문입니다(겉으로는 부모클래스인척..). 반대로 부모클래스로 객체 생성되었다가 자식클래스로 형변환해도 컴파일에는 문제가 없지만 자식클래스 내의 메서드는 호출할 수 없습니다. 속 빈 강정이기 때문이죠. 결국 위의 상황을 보면 알 수 있듯이 결국 오른쪽 new 부분이 어떤 클래스로 정의되었는지가 중요합니다. 또 업캐스팅 되어도 Overriding된 메서드가 있다면 자식 클래스에서 Override된 메서드가 호출되는데요. 이 또한 오른쪽 new 부분이 왜 더 중요한지 말해줍니다.
캐스팅 되었을 때 애초 어떤 클래스로 객체 생성이 되었는지 어떻게 알 수 있을까?
일반적으로 여러 개의 값을 전달하거나 매개변수로 값을 전달할 때는 부모클래스로 전달한다고 하는데요. 이 경우 애초 어떤 클래스로 정의되었는지 구별하기 위해 사용되는 예약어가 instanceof
라는 예약어입니다. 예를들어 certainClass instanceof ChildClass
가 쓰인다고 한다면 certainClass
가 ChildClass
로 객체 생성되었는지 '참', '거짓' 여부를 반환합니다. 이때 주의할 점은 먼저 비교하는것이 자식클래스여야 한다는 점입니다. 그 이유는 모두 부모클래스로 형변환 되어있기 때문에 certainClass instanceof ParentClass
가 먼저 오게되면 이 부분에서 '참'으로 되어 자식클래스 검증이 제대로 안되기 때문입니다.
상속을 사용하다보면 Casting이 많이 발생한다고 하는데요. 상속 설계가 잘 되어 있을수록 그 Casting의 횟수가 적다고 합니다. 따라서 많은 Casting이 발생된다면 상속 구조를 변경하거나 다른 방식으로 풀어야할 필요성이 있음을 인지하고 새로운 방법을 고려해보는 것이 좋습니다.
다중상속
자식클래스에서 하나의 부모클래스만 상속받을 수 있습니다. 이러한 상황일 때는 상속 대신 조합을 고려해보아야 합니다. 조합이면 이러한 제한이 없기 때문이죠.
우아.. 상속의 개념을 잘 활용해보지 않고 몰랐던 부분이 많아서 정리할 내용이 정말 많았네요. 앞으로 다른 step에서 상속의 개념이 활용될텐데요. 오늘 배운 것 바탕으로 상속의 유용성을 느껴봤으면 좋겠습니다.
'Language > Java' 카테고리의 다른 글
제네릭(Generic) (0) | 2018.11.23 |
---|---|
예외처리 (0) | 2018.10.25 |
enum (2) | 2018.10.22 |
인터페이스(Interface) (0) | 2018.10.15 |
DTO와 Java Bean (2) | 2018.10.01 |