자바

다형성이란?

Blue_bull 2025. 2. 9. 20:44

다형성(Polymorphism)은 객체 지향 프로그래밍(OOP)의 핵심 개념 중 하나로, "하나의 인터페이스로 여러 형태의 동작을 수행할 수 있는 능력"을 의미해. Java를 기준으로 정리해볼게.


다형성의 개념

  1. 정의

    • 같은 메서드나 인터페이스를 통해 여러 객체가 서로 다른 동작을 수행할 수 있음.
    • 부모 클래스의 참조 변수가 자식 클래스의 객체를 가리킬 수 있음.
  2. 이점

    • 코드의 재사용성 증가
    • 유지보수 용이
    • 확장성이 좋아짐

다형성의 종류

  1. 컴파일 타임 다형성 (Compile-time Polymorphism) → 메서드 오버로딩(Method Overloading)

    • 같은 클래스 내에서 같은 이름의 메서드를 여러 개 정의할 수 있음.
    • 매개변수의 개수, 타입, 순서가 다르면 메서드를 구분할 수 있음.
    • 컴파일 시점에 어떤 메서드를 호출할지 결정됨.
    class MathUtil {
        int add(int a, int b) {
            return a + b;
        }
    
        double add(double a, double b) { // 오버로딩
            return a + b;
        }
    }
  2. 런타임 다형성 (Runtime Polymorphism) → 메서드 오버라이딩(Method Overriding)

    • 부모 클래스의 메서드를 자식 클래스에서 재정의(Override) 할 수 있음.
    • 실행 시점에서 어떤 메서드를 호출할지 결정됨.
    • @Override 애노테이션을 사용하여 재정의된 메서드를 명확하게 표시하는 것이 일반적임.
    class Animal {
        void makeSound() {
            System.out.println("동물이 소리를 냅니다.");
        }
    }
    
    class Dog extends Animal {
        @Override
        void makeSound() {
            System.out.println("멍멍!");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Animal myDog = new Dog(); // 부모 타입(Animal)으로 자식 객체(Dog) 참조
            myDog.makeSound(); // "멍멍!" 출력
        }
    }

다형성의 핵심 개념

  1. 업캐스팅(Upcasting) 가능

    • 부모 타입으로 자식 객체를 참조할 수 있음.
    • Animal myDog = new Dog(); (✅ 가능)
    • 하지만 부모 클래스에 정의된 메서드만 호출 가능함. (재정의된 메서드는 예외)
  2. 다운캐스팅(Downcasting) 필요

    • 부모 타입을 다시 자식 타입으로 변환할 때는 다운캐스팅이 필요함.
    • (Dog) myDog처럼 변환해야 하지만, 안전한 다운캐스팅을 위해 instanceof를 활용하는 것이 좋음.
    if (myDog instanceof Dog) {
        Dog realDog = (Dog) myDog;
        realDog.makeSound(); // "멍멍!"
    }

팩트 체크

✔ 다형성은 "하나의 인터페이스로 여러 형태를 수행하는 것"
오버로딩(컴파일 타임)과 오버라이딩(런타임) 두 가지 형태가 있음
✔ 부모 클래스 타입으로 자식 객체를 참조하는 것이 가능 (업캐스팅)
✔ 실행 시점에서 메서드가 결정되기 때문에 유연한 코드 작성이 가능함
✔ 다운캐스팅 시 instanceof를 활용하여 안전하게 변환해야 함



📌 부모와 자식 클래스의 멤버(변수, 메서드)는 언제 사용될까?

멤버(필드, 메서드)의 호출은 "참조 변수 타입"과 "실제 객체 타입"에 따라 달라짐.
특히, 메서드 오버라이딩 시 다형성이 적용되며, 업캐스팅(승급)과 다운캐스팅(강등)에 따라 접근 가능 여부가 달라짐.


1️⃣ 필드(멤버 변수)와 메서드(함수)의 사용 규칙

상황 필드(변수) 사용 메서드(함수) 사용
기본적인 호출 참조 변수 타입을 기준으로 결정됨 실제 객체 타입을 기준으로 결정됨
오버라이딩된 메서드 영향을 받지 않음 항상 실제 객체의 메서드가 호출됨
업캐스팅(승급, Upcasting) 부모의 필드만 접근 가능 자식이 오버라이딩한 메서드가 호출됨
다운캐스팅(강등, Downcasting) 강제 형변환 후 자식의 필드 접근 가능 강제 형변환 후 자식 메서드 호출 가능

📌 즉, 필드는 "참조 변수의 타입"에 따라 결정되고, 메서드는 "실제 객체 타입"을 따라간다.


2️⃣ 기본적인 멤버 변수 및 메서드 접근

class Parent {
    int x = 10;

    void show() {
        System.out.println("부모 클래스 show() 메서드");
    }
}

class Child extends Parent {
    int x = 20; // 부모의 x를 숨김 (변수 오버라이딩 아님, Shadowing)

    @Override
    void show() {
        System.out.println("자식 클래스 show() 메서드");
    }
}

public class Main {
    public static void main(String[] args) {
        Child c = new Child();
        System.out.println(c.x); // 20 (참조 변수 타입 기준 → Child의 x)
        c.show(); // "자식 클래스 show() 메서드" (메서드는 객체 타입 기준)
    }
}

📌 실행 결과:

20
자식 클래스 show() 메서드

변수는 참조 변수 타입 기준c.xChildx 값을 출력 (20).
메서드는 실제 객체 타입 기준c.show();Childshow()가 실행됨.


3️⃣ 업캐스팅(Upcasting) - 부모 타입으로 참조

public class Main {
    public static void main(String[] args) {
        Parent p = new Child(); // 업캐스팅 (승급)
        System.out.println(p.x); // 10 (참조 변수 타입 기준 → Parent의 x)
        p.show(); // "자식 클래스 show() 메서드" (메서드는 실제 객체 기준)
    }
}

📌 실행 결과:

10
자식 클래스 show() 메서드

변수 p.x는 Parent의 x (10) → 참조 변수의 타입을 따라감.
메서드 p.show();는 Child의 show() 실행 → 실제 객체 타입을 따라감.

📌 업캐스팅 시 부모의 변수만 보이고, 오버라이딩된 메서드는 자식 것이 호출됨.


4️⃣ 다운캐스팅(Downcasting) - 자식 타입으로 변환

public class Main {
    public static void main(String[] args) {
        Parent p = new Child(); // 업캐스팅
        // System.out.println(p.y); // 컴파일 오류 (Parent 타입에서는 Child의 멤버 접근 불가)

        Child c = (Child) p; // 다운캐스팅 (강등)
        System.out.println(c.x); // 20 (Child의 x)
        c.show(); // "자식 클래스 show() 메서드"
    }
}

📌 실행 결과:

20
자식 클래스 show() 메서드

다운캐스팅 후 c.x는 Child의 x(20) → 이제 접근 가능.
다운캐스팅 후 c.show();는 Child의 show() 실행 → 원래 가능했던 것.

📌 다운캐스팅을 하면 부모 클래스에서 접근할 수 없었던 자식의 멤버에도 접근할 수 있음.


5️⃣ 오버라이딩과 super를 활용한 부모 메서드 호출

class Parent {
    void show() {
        System.out.println("부모 클래스 show() 메서드");
    }
}

class Child extends Parent {
    @Override
    void show() {
        super.show(); // 부모의 메서드 호출
        System.out.println("자식 클래스 show() 메서드");
    }
}

public class Main {
    public static void main(String[] args) {
        Child c = new Child();
        c.show();
    }
}

📌 실행 결과:

부모 클래스 show() 메서드
자식 클래스 show() 메서드

super.show();를 사용하면 오버라이딩된 부모의 메서드도 실행 가능!


6️⃣ 정리 - 언제 부모/자식의 멤버가 사용될까?

상황 변수(필드) 메서드(함수)
기본 호출 참조 변수 타입을 따름 실제 객체 타입을 따름
업캐스팅(부모 타입으로 참조) 부모 클래스의 변수만 접근 가능 자식이 오버라이딩한 메서드가 호출됨
다운캐스팅(자식 타입으로 변환) 강제 형변환 후 자식 변수 접근 가능 강제 형변환 후 자식 메서드 호출 가능
오버라이딩된 메서드 X (변수는 오버라이딩되지 않음) 항상 실제 객체의 메서드가 실행됨

변수는 참조 변수의 타입을 따른다.
메서드는 실제 객체 타입을 따른다.
업캐스팅하면 부모의 변수만 보이지만, 오버라이딩된 메서드는 자식 것이 호출된다.
다운캐스팅하면 자식 클래스의 변수와 메서드 모두 접근 가능하다.

👉 "필드는 참조 타입을 따라가고, 메서드는 실제 객체를 따라간다!" 🚀

좋은 질문이야! 객체(Object)와 타입(Type)의 차이를 확실히 이해하면 다형성을 더 잘 활용할 수 있어.


객체와 타입의 차이

  1. 객체(Object)란?

    • 클래스의 인스턴스(실제 메모리에 생성된 것).
    • 생성된 객체는 클래스에서 정의한 필드(변수)와 메서드(동작)를 가짐.
    • 예) new Dog() → Dog 클래스의 객체 생성.
  2. 타입(Type)이란?

    • 변수가 참조할 객체의 데이터 유형을 결정.
    • 변수가 접근할 수 있는 필드와 메서드를 제한함.
    • 예) Animal myDog = new Dog();myDog 변수의 타입은 Animal이므로, Animal 클래스에서 정의된 필드와 메서드만 사용 가능.

객체 vs. 타입의 동작 차이

객체는 실제로 Dog 인스턴스이지만, 참조 타입(Animal)에 따라 사용할 수 있는 기능이 제한됨.

class Animal {
    String type = "동물"; // 필드
    void makeSound() {    // 메서드
        System.out.println("동물이 소리를 냅니다.");
    }
}

class Dog extends Animal {
    String breed = "불독"; // 자식 클래스에만 있는 필드
    @Override
    void makeSound() {
        System.out.println("멍멍!");
    }

    void wagTail() { // 자식 클래스에만 있는 메서드
        System.out.println("꼬리를 흔듭니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myDog = new Dog(); // 업캐스팅
        System.out.println(myDog.type); // ✅ "동물" (Animal에 정의된 필드 사용 가능)
        myDog.makeSound(); // ✅ "멍멍!" (오버라이딩된 메서드는 호출 가능)

        // System.out.println(myDog.breed); // ❌ 오류! (Animal에는 breed 필드 없음)
        // myDog.wagTail(); // ❌ 오류! (Animal에는 wagTail() 없음)

        Dog realDog = (Dog) myDog; // 다운캐스팅
        System.out.println(realDog.breed); // ✅ "불독"
        realDog.wagTail(); // ✅ "꼬리를 흔듭니다."
    }
}

정리

객체(Object) = 실제 생성된 인스턴스
타입(Type) = 변수가 참조할 수 있는 필드와 메서드를 결정
업캐스팅(Animal myDog = new Dog();)

  • 부모 타입 변수는 부모 클래스에 정의된 필드와 메서드만 접근 가능
  • 오버라이딩된 메서드는 호출 가능 (동적 바인딩)
    다운캐스팅(Dog realDog = (Dog) myDog;)
  • 자식 클래스의 필드와 메서드를 사용하려면 다운캐스팅 필요

이제 업캐스팅과 타입의 개념이 더 명확해졌을 거야! 😊

업캐스팅(Upcasting)의 핵심 이점코드의 유연성과 확장성이야. 부모 타입(Animal)을 사용하면, 자식 클래스(Dog, Cat, Bird 등)가 추가되어도 기존 코드를 변경하지 않고 사용할 수 있기 때문이야.


업캐스팅의 주요 이점

1️⃣ 유연성과 확장성 증가

  • 여러 자식 클래스가 추가되어도 공통된 인터페이스(부모 클래스)를 통해 다룰 수 있음.
  • 새로 추가되는 동물(Cat, Bird 등)이 있어도 Animal 타입으로 관리하면 기존 코드를 수정할 필요가 없음.
class Animal {
    void makeSound() {
        System.out.println("동물이 소리를 냅니다.");
    }
}

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

class Cat extends Animal {
    @Override
    void makeSound() {
        System.out.println("야옹!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Dog(); // 업캐스팅
        myAnimal.makeSound(); // "멍멍!" (동적 바인딩)

        myAnimal = new Cat(); // 업캐스팅 (유연성)
        myAnimal.makeSound(); // "야옹!" (동적 바인딩)
    }
}

myAnimal 변수의 타입이 Animal이므로, Dog → Cat으로 변경할 때 코드 수정이 필요 없음.


2️⃣ 코드 재사용성 증가 (다형성 활용)

  • 부모 클래스 타입을 사용하면, 같은 코드로 다양한 객체를 처리할 수 있음.
public class Main {
    public static void printAnimalSound(Animal animal) {
        animal.makeSound(); // 어떤 동물이든 같은 코드로 처리 가능
    }

    public static void main(String[] args) {
        printAnimalSound(new Dog()); // "멍멍!"
        printAnimalSound(new Cat()); // "야옹!"
    }
}

printAnimalSound() 메서드는 Animal 타입만 받기 때문에, Dog, Cat 등 어떤 객체든 동일한 코드로 처리 가능.


3️⃣ 컬렉션(List, 배열)에서의 활용

  • 업캐스팅을 사용하면 다양한 객체를 같은 리스트(List)나 배열(Array)에서 관리 가능.
import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<Animal> animals = new ArrayList<>();
        animals.add(new Dog()); // 업캐스팅
        animals.add(new Cat()); // 업캐스팅

        for (Animal animal : animals) {
            animal.makeSound(); // 각각 "멍멍!", "야옹!" 출력
        }
    }
}

ArrayList<Animal>을 사용하면 모든 동물 객체를 하나의 리스트에서 관리 가능.


4️⃣ 인터페이스와 조합하면 강력한 설계 가능

  • 업캐스팅을 활용하면 객체 간 결합도를 줄이고 인터페이스를 통한 유연한 설계 가능.
interface Animal {
    void makeSound();
}

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

class Cat implements Animal {
    @Override
    public void makeSound() {
        System.out.println("야옹!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myPet = new Dog(); // 인터페이스 타입으로 업캐스팅
        myPet.makeSound(); // "멍멍!"
    }
}

✔ 인터페이스를 사용하면 여러 개의 클래스를 유연하게 확장 가능.


결론

✔ 업캐스팅의 주요 이점

1️⃣ 유연성과 확장성 → 새 클래스 추가 시 기존 코드 수정 불필요
2️⃣ 코드 재사용성 → 하나의 메서드로 여러 객체 처리 가능
3️⃣ 컬렉션(List, 배열)에서 활용 → 여러 객체를 한 리스트에 저장 가능
4️⃣ 인터페이스와 조합 가능 → 유지보수와 확장성 향상

📌 정리하면, 업캐스팅을 활용하면 코드가 더 유연하고 확장 가능해지며, 유지보수가 쉬워진다! 🚀