Java - 다형성

develkkm·2025년 11월 6일

0부터 시작하는 Java

목록 보기
19/27
post-thumbnail

다형성

다형성(Polymorphism)은 객체지향 프로그래밍의 핵심 개념 중 하나다.
하나의 객체가 여러 형태로 동작할 수 있는 성질을 의미하며 자바에서는 부모 타입의 참조 변수로 자식 객체를 참조할 수 있는 것을 뜻한다.
이 기능을 통해 코드는 더 유연하고 확장 가능하게 된다.


다형성의 기본

다형성은 상속 관계에서 성립한다.
부모 클래스가 존재하고 그 클래스를 상속받은 자식 클래스가 있을 때 부모 타입의 참조 변수에 자식 객체를 담을 수 있다.

class Parent {
    public void greet() {
        System.out.println("부모의 인사입니다.");
    }
}

class Child extends Parent {
    @Override
    public void greet() {
        System.out.println("자식의 인사입니다.");
    }
}

public class PolyMain {
    public static void main(String[] args) {
        Parent parent = new Child(); // 업캐스팅
        parent.greet(); // "자식의 인사입니다."
    }
}

변수의 타입은 Parent이지만 실제 인스턴스는 Child다.
이처럼 실행 시점에 실제 객체의 타입에 따라 호출되는 메서드가 결정되는 것을 동적 바인딩(Dynamic Binding)이라 한다.


캐스팅

부모 타입으로 자식을 참조하면 부모가 가진 기능만 사용할 수 있다.
자식 클래스의 고유한 기능을 사용하려면 다운캐스팅(Downcasting)이 필요하다.

Parent parent = new Child();
parent.greet(); // 부모 타입으로 접근 가능

// parent.childMethod(); // 불가능

Child child = (Child) parent; // 다운캐스팅
child.greet(); // 가능

반대로 자식을 부모로 바꾸는 것은 업캐스팅(Upcasting)이라 하며 자동으로 수행된다.
그러나 다운캐스팅은 명시적 형변환이 필요하다.


instanceof

다운캐스팅을 수행하기 전에는 객체의 실제 타입을 확인해야 한다.
이를 위해 instanceof 연산자를 사용한다.

Parent parent = new Child();

if (parent instanceof Child) {
    Child child = (Child) parent;
    child.greet();
}

instanceof는 해당 객체가 특정 타입의 인스턴스인지 여부를 검사한다.
잘못된 타입으로 캐스팅하면 ClassCastException이 발생하므로 안전한 캐스팅을 위해 꼭 사용해야 한다.


오버라이딩

오버라이딩은 부모 클래스의 메서드를 자식 클래스가 재정의하는 것이다.
다형성은 오버라이딩을 기반으로 동작한다.

class Animal {
    public void sound() {
        System.out.println("동물이 소리를 냅니다.");
    }
}

class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}

class Cat extends Animal {
    @Override
    public void sound() {
        System.out.println("냐옹");
    }
}
public class SoundMain {
    public static void main(String[] args) {
        Animal[] animals = {new Dog(), new Cat()};

        for (Animal animal : animals) {
            animal.sound(); // 실제 객체의 메서드가 실행됨
        }
    }
}

부모 타입으로 선언된 배열이나 컬렉션을 통해 여러 자식 객체를 동일한 방식으로 다룰 수 있다.
이것이 다형성이 제공하는 가장 큰 장점이다.


다형성 활용

다형성은 중복을 제거하고 코드 변경 없이 확장을 가능하게 한다.
새로운 동물이 추가되더라도 기존 로직을 수정할 필요가 없다.

class Cow extends Animal {
    @Override
    public void sound() {
        System.out.println("음매");
    }
}

SoundMain의 코드는 그대로 유지된다.
이처럼 다형성은 새로운 요구사항이 추가될 때 수정보다 확장을 가능하게 만든다.
이는 OCP(개방-폐쇄 원칙)을 실현하는 핵심 원리다.


추상 클래스

모든 자식 클래스가 sound()를 반드시 구현해야 한다면 Animal을 추상 클래스로 만들어 강제할 수 있다.

public abstract class Animal {
    public abstract void sound();

    public void move() {
        System.out.println("동물이 움직입니다.");
    }
}
public class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}
public class Cat extends Animal {
    @Override
    public void sound() {
        System.out.println("냐옹");
    }
}

추상 클래스의 특징은 다음과 같다.

  • 인스턴스를 생성할 수 없다.
  • 자식 클래스가 반드시 구현해야 할 메서드를 정의한다.
  • 공통된 로직은 그대로 물려줄 수 있다.

인터페이스

추상 클래스보다 더 순수한 형태의 규약이 필요할 때 인터페이스를 사용한다.
인터페이스는 “무엇을 할 수 있는가”에 대한 약속을 정의한다.

public interface Animal {
    void sound();
}
public class Dog implements Animal {
    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}

public class Cat implements Animal {
    @Override
    public void sound() {
        System.out.println("냐옹");
    }
}
public class AnimalMain {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new Cat();

        Animal[] animals = {dog, cat};
        for (Animal a : animals) {
            a.sound();
        }
    }
}

인터페이스는 구현체와의 결합을 줄이고 다양한 객체를 동일한 방식으로 다룰 수 있게 한다.
이로써 코드의 의존성이 낮아지고 확장성이 높아진다.


클래스와 인터페이스 활용

추상 클래스는 공통된 성질을 상속하기 위한 수단이다.
인터페이스는 공통된 행위를 정의하기 위한 수단이다.

public interface Flyable {
    void fly();
}

public abstract class Animal {
    public abstract void sound();
}
public class Bird extends Animal implements Flyable {
    @Override
    public void sound() {
        System.out.println("짹짹");
    }

    @Override
    public void fly() {
        System.out.println("새가 하늘을 납니다.");
    }
}
public class AnimalMain {
    public static void main(String[] args) {
        Bird bird = new Bird();
        bird.sound();
        bird.fly();
    }
}

클래스는 공통된 속성을 물려주고 인터페이스는 공통된 행동을 강제한다.
두 개념을 함께 사용하면 객체 간 협력 관계를 더욱 유연하게 설계할 수 있다.

profile
알던것을 더 확실하게

0개의 댓글