상속과 다형성
객체 지향 프로그래밍에서 상속과 다형성은 코드의 재사용성과 유지 보수성을 높이는 핵심적인 개념입니다.
상속은 한 클래스가 다른 클래스의 속성과 메소드를 물려받는 것을 의미하며, 다형성은 인터페이스나 부모 클래스를 통해 하나의 객체에 여러 가지 타입을 대입할 수 있다는 것을 의미합니다.
상속은 왜 하는데?
- 상속을 통해 기존의 코드를 재사용하고 확장할 수 있으며, 상속을 통해 중복 코드를 줄이고, 다형성을 통해 다양한 기능을 쉽게 추가할 수 있기 때문입니다.
- 중복 코드를 줄여 코드 유지보수가 용이해집니다.
- 클래스 간 계층 구조를 만들어 체계적으로 관리할 수 있다.
등의 장점이 존재합니다.
실제 코드를 통해서 상속이 필요한 이유에 대해서 알아보겠습니다.
class Dog {
String name = "강아지";
void bark() {
System.out.println("멍멍!");
}
void eat() {
System.out.println(this.name + "는 음식을 먹습니다.");
}
}
class Cat {
String name = "고양이";
void meow() {
System.out.println("야옹!");
}
void eat() {
System.out.println(this.name + "는 음식을 먹습니다.");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat(); // "강아지는 음식을 먹습니다."
dog.bark(); // "멍멍!"
Cat cat = new Cat();
cat.eat(); // "고양이는 음식을 먹습니다."
cat.meow(); // "야옹!"
}
}
위와 같은 코드가 있다고 가정해보겠습니다.
지금이야 ‘강아지’, ‘고양이' 클래스만 존재하고, 해당 클래스의 함수가 적기 때문에 별 문제가 없지만, 나중에 중복되는 함수가 많아지고, 동물의 수가 1000, 10000마리 늘어나게 된다면 매번 중복 함수를 정의하기 힘들어집니다.
이럴 때 상속을 이용하면 코드의 중복을 줄일 수 있습니다.
class Animal {
String name;
Animal(String name) {
this.name = name;
}
void eat() {
System.out.println(this.name + "는 음식을 먹습니다.");
}
}
class Dog extends Animal {
Dog() {
super("강아지");
}
void bark() {
System.out.println("멍멍!");
}
}
class Cat extends Animal {
Cat() {
super("고양이");
}
void meow() {
System.out.println("야옹!");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat(); // "강아지는 음식을 먹습니다."
dog.bark(); // "멍멍!"
Cat cat = new Cat();
cat.eat(); // "고양이는 음식을 먹습니다."
cat.meow(); // "야옹!"
}
}
위 코드에서 Animal 클래스는 공통적인 eat 메소드를 포함하고 있으며, Dog와 Cat 클래스는 Animal 클래스를 상속받아 eat 메소드를 재사용 합니다. 이를 통해 코드의 중복을 줄이고, 새로운 동물 클래스를 추가할 때 eat 메소드와 같이 중복되는 함수를 작성할 필요 없게 됩니다.
Java에서의 상속과 다형성
자바에서는 참조 타입을 통해서 다형성을 구현할 수 있습니다.
즉, 부모 Class를 참조 타입으로 선언하고 실제 객체는 자식 Class 객체를 생성할 수 있습니다.
아래의 그림을 참고해서 선언을 간단하게 한다면,
Dog a = new Dog()
Animal b = a
이는 자바에서 상속 관계에 있는 클래스들 간에 자동 타입 변환을 허용하기 때문입니다.
즉, 부모 클래스 변수에 자식 클래스 객체를 대입하면, 자식 타입이 부모 타입으로 자동 변환됩니다.
위의 경우에도 Dog 클래스가 Animal 클래스의 자식 클래스라고 할 때, Dog 객체를 생성하고, 이것을 Animal변수에 대입하면 자동 타입 변환이 일어나게 됩니다.
이 때, a과 b 변수는 참조 타입만 다를 뿐, 동일한 Dog 객체를 참조하게 됩니다.
자식타입이 부모타입으로 자동타입변환된 이후에는, 비록 변수는 자식객체를 참조하지만, 변수로 접근가능한 멤버는 부모 클래스 멤버로만 한정됩니다. 따라서 부모클래스에 없고 자식클래스에만 있는 멤버에 접근할 수 없습니다.
단, 메소드가 자식클래스에서 오버라이딩되었다면 오버라이딩된 메소드가 대신 호출되게 됩니다.
이걸 코드로 한번 확인해보겠습니다!
public static void main(String[] args) {
Poodle a = new Poodle();
Dog b = a;
Animal c = b;
// Poodle a
System.out.println("Poodle a가 메소드를 호출합니다");
a.a();
a.aa();
a.aaa();
a.bark(); // "Poodle이 짖습니다"
// Dog b
System.out.println("Dog b가 메소드를 호출합니다");
b.a();
b.aa();
// b.aaa(); // 얘는 오류를 발생시킴
b.bark(); //"Dog가 짖습니다"
// Animal c
System.out.println("Animal c가 메소드를 호출합니다");
c.a();
// c.aa(); // 얘는 오류를 발생시킴.
// c.aaa(); // 얘는 오류를 발생시킴
c.bark(); //"Dog가 짖습니다" <- Dog의 bark()를 호출
}
위와 같은 사진과 코드가 있다고 가정할 때, Poodle 객체가 어떻게 생겼는지 제가 생각한(?) 그림을 그려보겠습니다.
위의 그림을 보면서 각 Poodle a, Dog b, Animal c 라는 변수가 Poodle 객체를 어떻게 바라보고 있는지 하나씩 살펴보겠습니다.
Poodle a 변수
Poodle a의 경우에 접근할 수 있는 Class 메소드의 경우에는 상속 받은 bark(), aa(), a() 메소드와 Poodle에서 정의한 aaa() 메소드에 접근할 수 있습니다.
따라서 위의 코드에 Poodle a 부분에 작성된 코드들의 실행 결과는 아래와 같습니다.
aaa 함수가 호출되었습니다.
aa 함수가 호출되었습니다.
a 함수가 호출되었습니다.
Dog가 짖습니다
Dog b 변수
Dog b의 경우에 접근할 수 있는 Class 메소드의 경우에는 상속 받은 a() 메소드와 Dog에서 새롭게 정의한 aa() 메소드, 그리고 오버라이딩을 통해 새롭게 정의한 bark() 메소드에 접근할 수 있습니다.
aa 함수가 호출되었습니다.
a 함수가 호출되었습니다.
Dog가 짖습니다
Animal c 변수
Animal c의 경우에 접근할 수 있는 Class 메소드는 a() 와 bark() 메소드에 접근할 수 있습니다.
a 함수가 호출되었습니다.
Dog가 짖습니다
그런데 주의해야할 점은 여기서 c.bark()를 호출했을 때 Animal에서 정의한 bark()가 아닌 Dog(자식 클래스)에서의 bark()가 호출되는 것을 알 수 있습니다.
메소드가 자식클래스에서 오버라이딩되었다면 오버라이딩된 메소드가 대신 호출되기 때문에 Dog class의 bark()가 호출된 것입니다.
이렇듯 Poodle이라는 하나의 객체를 Poodle, Dog, Animal과 같이 여러가지 타입을 가질 수 있게 하는 것을
다형성(polymorphism) 이라고 합니다.
다형성 왜 필요한데?
다형성이 필요한 이유는 코드의 재사용성과 유연성이 향상되어, 확장성 있는 프로그램 개발이 가능하기 때문입니다.
다형성이 왜 필요한지에 대해 설명하기 위해 스마트폰에 들어가는 시스템을 개발한다고 가정하겠습니다.
Widget이라는 Class에는 여러가지 위젯이 포함됩니다. 예를 들어 퀵 설정 패널에 있는 블루투스 위젯, 와이파이 위젯부터 홈 화면에 있는 전화 위젯, 카메라 위젯 등 다양한 위젯들로 스마트폰이 구성됩니다.
만약 Widget 버튼을 눌러 동작하는 함수를 작성한다고 가정하겠습니다.
다형성이 허용되기 때문에 Widget Class를 참조 타입으로 push_button이라는 함수를 정의하면 모든 버튼의 동작을 정의할 수 있습니다.
위젯의 버튼을 눌렀을 때 발생하는 동작을 정의하는 push_button 함수는 Widget 타입을 매개변수로 받아들이며, 실제 객체가 어떤 클래스에 속하는지에 따라 동적으로 다른 동작을 수행합니다.
WifiWidget 객체가 전달되면 WifiWidget 클래스에서 오버라이딩한 check()와 action() 메소드가 호출되고, CallWidget 객체가 전달되면 CallWidget 클래스에서 오버라이딩된 메소드가 호출됩니다.
이러한 방식 덕분에, 각 버튼에 대해 개별적인 함수를 작성할 필요 없이 하나의 함수로 모든 동작을 처리할 수 있게 됩니다.
void push_button(Widget widget){
// 위젯과 관련된 기기 상태를 확인하는 함수
widget.check();
// 위젯을 누르면 동작하는 작업을 정의하는 함수
widget.action();
}
Widget wifiWidget = new WifiWidget();
Widget callWidget = new CallWidget();
push_button(wifiWidget);
push_button(callWidget);
... (생략) ...
만약 다형성이 허용되지 않는다면, 각 버튼마다 별도의 함수를 작성해야 하고, 코드가 길어지고 복잡해지며, 유지보수가 어려워집니다.
예를 들어, push_button 함수가 WifiWidget을 처리하는 방법, CallWidget을 처리하는 방법 등 각각을 분리해서 작성해야 할 것입니다.
이는 코드 중복을 초래하고, 새로운 위젯이 추가될 때마다 수정해야 할 부분이 많아져 확장성이 떨어집니다.
이러한 이유로 다형성은 객체지향프로그래밍(OOP)에서의 꽃이라고 불리는 것 같습니다.
'Java' 카테고리의 다른 글
[Java] Spring이란? (0) | 2025.03.02 |
---|---|
[Java] Static 변수 & 메서드 (0) | 2024.11.28 |
[Java] 데이터 타입 - 기본형 타입, 참조형 타입 (0) | 2024.11.27 |
[Java] 클래스, 인스턴스, 레퍼런스에 대해 알아보자 (0) | 2024.11.27 |
[Java] Maven이란? (2) | 2024.11.26 |