다형성은 객체 지향 프로그래밍의 4대 핵심 원칙 중 하나이다.
여러 형태를 가질 수 있는 능력을 의미하는데, 다형성은 같은 이름의 메서드나 속성이 클래스에 따라 다르게 동작하도록
하여 유연하고 확장 가능한 코드를 작성할 수 있게 한다.
이번 글에선 다형성의 개념과 본질을 살펴보고 자바 코드 예제를 통해 실제로 어떻게 활용할 수 있는지 간단하게 설명하는
시간을 가져보겠다.
다형성이란?
다형성은 크게 두 가지로 구분되는데,
1. 컴파일 시간 다형성 (정적 바인딩) : 메서드 오버로딩처럼 컴파일 시점에 호출될 메서드를 결정하는 방식이다.
2. 런타임 다형성(동적 바인딩) : 메서드 오버라이딩처럼 실행 시점에 호출될 메서드를 결정하는 방식이다.
이번 글에서는 런타임 다형성을 중점적으로 다룰 것이다.
런타임 다형성은 부모 클래스를 통해 자식 클래스의 메서드를 호출하거나 하나의 부모 타입으로 여러 자식 객체를 다룰 수 있는 특징이다.
다형성의 본질은 뭘까?
다형성의 본질은 코드의 유연성과 재사용성이다.
1. 코드의 확장성:
- 새로운 자식 클래스를 추가하더라도 기존 코드를 수정하지 않고 동작시킬 수 있다.
- 부모 클래스를 기준으로 작성된 코드는 다양한 자식 클래스와 함께 동작할 수 있다.
2. 유연한 객체 관리:
- 하나의 부모 타입 변수를 사용해 여러 자식 객체를 처리할 수 있다.
- 배열이나 컬렉션에서 부모 타입을 사용하면 다양한 자식 객체를 저장하고 관리 가능.
3. 재정의된 메서드의 동적 호출:
- 부모 타입으로 선언된 변수더라도 참조하는 객체가 자식 클래스라면 오버라이딩(재정의)된 메서드가 호출된다.
- 객체의 실제 동작을 유연하게 결정 가능.
다형성을 활용하기 좋은 예제인 동물과 개, 고양이를 예시로 들어보자.
package polymorphismEx;
//부모
public class Animal {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void makeSound() {
System.out.println("Animal Sound");
}
}
package polymorphismEx;
//자식
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Bark");
}
}
package polymorphismEx;
//자식
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Meow");
}
}
package polymorphismEx;
//메인
public class Zoo {
public static void main(String[] args) {
Animal myDog = new Dog();
Animal myCat = new Cat();
myDog.setName("Angkko");
myCat.setName("Baduk");
System.out.println(myDog.getName());
myDog.makeSound();
System.out.println(myCat.getName());
myCat.makeSound();
}
}
이제 메인 클래스 분석을 해보자.
Animal myDog = new Dog();
Animal myCat = new Cat();
- Animal 타입의 변수로 Dog와 Cat을 참조했다.
- 이 참조를 통해 다양한 자식 클래스를 하나의 부모 타입으로 다룰 수 있는 유연성을 제공할 수가 있다.
myDog.makeSound(); // 출력: Bark
myCat.makeSound(); // 출력: Meow
- 부모 클래스인 Animal 클래스의 메서드 makeSound는 자식 클래스에서 오버라이딩되었다.
- Animal 타입으로 선언되었지만 Override 되어 덮어씌워진 참조하는 실제 객체(Dog, Cat)의 makeSound 메서드가 호출된다.
다형성을 통해 부모 클래스 타입으로 자식 클래스 객체를 다룰 수가 있다.
다양한 객체를 하나의 부모 타입으로 다룰 수 있고 새로운 자식 클래스를 추가해도 기존 코드를 수정할 필요가 사라진다.
업캐스팅과 다운캐스팅
업캐스팅
위에서 간단하게 접근했지만 좀 더 자세히 접근해보겠다.
업캐스팅은 우리가 위에서 했던 것처럼 자식 클래스 객체를 부모 클래스 타입으로 변환하는 것을 말한다.
이 과정에서 객체 자체는 변하지 않고 여전히 자식 클래스 객체로 유지가 된다.
그러나 객체가 그대로라고 해서 모두 접근 가능한 것이 아니다. 참조 변수의 타입이 부모 타입으로 바뀌기 때문에 객체는
그대로더라도 부모 클래스에서 정의된 멤버(필드와 메서드)만 접근할 수 있다.
즉, 자식 클래스에서 추가된 메서드나 필드는 참조 변수로 볼 수 없게 된다.
class Animal {
public void makeSound() {
System.out.println("Animal sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Bark");
}
public void wagTail() {
System.out.println("Wagging tail");
}
}
public class Zoo {
public static void main(String[] args) {
Animal myDog = new Dog(); // 업캐스팅
myDog.makeSound(); // 출력: Bark (재정의된 메서드 호출)
// myDog.wagTail(); // 오류: 부모 타입으로 참조했기 때문에 Dog 고유 메서드 접근 불가
}
}
업캐스팅의 핵심
- Animal myDog = new Dog()를 통해서 Dog 객체를 생성했지만 참조 변수의 타입이 Animal인 것을 확인할 수 있다.
- 객체 자체는 여전히 Dog지만 참조 변수는 부모 타입인 Animal로 선언되었기 때문에 부모 클래스의 범위만 볼 수 있는 것이다.
- makeSound 메서드는 Animal에서 선언되었지만 Dog에서 재정의(Override)했으므로 자식 클래스 구현이 가능하다.
- 자식 클래스의 고유 멤버는 참조 변수로 접근이 불가능하다. 재정의(Override)된 것만!
다운캐스팅
다운캐스팅은 업캐스팅된 객체를 다시 자식 클래스 타입으로 변환하는 것이다.
부모 타입으로 참조 시 접근할 수 없었던 자식 클래스의 고유 멤버에 다시 접근할 수 있게 된다.
중요한 점은 객체가 실제로 자식 클래스의 인스턴스인 경우만 가능하다! 업캐스팅된 객체만 다운캐스팅이 가능하다는 뜻이다.
업캐스팅된 객체만 다운캐스팅이 가능한 이유는?
객체의 타입과 참조의 타입은 다를 수 있기 때문이다.
- 업캐스팅된 객체는 실제로 자식 클래스 객체이기 때문에 다운캐스팅 시 자식 클래스의 내용을 참조할 수 있다.
- 하지만 부모 클래스 객체를 다운캐스팅하려고 하면 자식 클래스의 내용이 존재하지 않기 때문에 불가능하다.
비유를 들어보자.
A라는 부모 클래스와 B라는 자식 클래스가 있다.
B 객체는 부모와 자식의 모든 내용을 담고 있는 책이다.
업캐스팅은 이 책을 부모(A)의 내용만 볼 수 있는 작은 창으로 보는 것이고 다운캐스팅은 다시 자식(B)의 내용까지 볼 수 있는 창으로 바꾸는 것이다.
하지만 A책(부모만 존재하는 책)을 B책으로 바꾸는 것은 불가능하다.
코드 예제를 보자.
public class Zoo {
public static void main(String[] args) {
Animal myAnimal = new Dog(); // 업캐스팅
myAnimal.makeSound(); // 출력: Bark
// 다운캐스팅
if (myAnimal instanceof Dog) {
Dog myDog = (Dog) myAnimal;
myDog.wagTail(); // 출력: Wagging tail
}
}
}
먼저 업캐스팅 부분을 간단히 설명하자면 실제 객체는 Dog이지만 참조 타입을 Animal 타입으로 설정한 것을 볼 수 있다.
myAnimal이 Animal 타입으로 선언되었으나 다형성에 의해서 오버라이딩된 makeSoung() 메서드가 호출된다.
다운 캐스팅 부분을 보면
if (myAnimal instanceof Dog) {
Dog myDog = (Dog) myAnimal;
myDog.wagTail(); // 출력: Wagging tail
}
instanceof 연산자를 통해 myAnimal 객체가 실제로 자식 클래스인 Dog 클래스의 인스턴스인지 확인한다.
myAnimal이 Dog의 객체이므로 이 조건은 true가 되고 if 문이 충족되니 myAnimal을 Dog 타입으로 다운캐스팅한다.
Dog 타입으로 변환하려면 명시적으로 다운캐스팅을 해야 하므로 (Dog) 라는 타입 캐스팅을 사용하여 변환이 가능한 것이다.
이제 myDog는 Dog 타입이므로 Dog 클래스의 wagTail() 메서드를 호출할 수 있다.
그래서 다형성의 본질은?
다형성은 부모 타입으로 참조하되, 실제 객체의 자식 타입 구현에 따라 동작한다.
업캐스팅은 유연하고 일관된 코드를 작성하는 데 사용하고 부모 타입으로 참조하여 구현에 독립적인 코드를 만들 수 잇다.
다운캐스팅은 특정 상황에서 자식 클래스의 고유 기능을 사용할 때 필요하며 안전하게 처리해야 한다.
다형성의 본질은
상속 관계에 있는 클래스들이 공통된 부모 타입을 통해 일관된 인터페이스를 제공하면서, 실제 구현은 자식 클래스의 구체적인 방식으로 동작하도록 하는 것이다.
'Language > Java' 카테고리의 다른 글
[Java] toString을 왜 쓸까? (0) | 2025.01.07 |
---|---|
[Java] 예외처리 코드 연습 (0) | 2025.01.05 |
[Java] String / StringBuffer / StringBuilder 차이점 (0) | 2024.10.04 |
[Java] 인터페이스 (48) | 2024.06.22 |
[Java] 멤버 변수 초기화에 관하여 (52) | 2024.06.21 |