Notice
Recent Posts
Recent Comments
Link
«   2026/03   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Tags more
Archives
Today
Total
관리 메뉴

프로그래밍 공부

[자바] #01. 업캐스팅(Upcasting)과 다형성(Polymorphism) 본문

자바

[자바] #01. 업캐스팅(Upcasting)과 다형성(Polymorphism)

하 냥 2025. 2. 14. 09:01

MobilePhone 클래스가 있고, 그 MobilePhone 클래스를 상속하는 SmartPhone 클래스가 있다.

MobilePhone 클래스는 인스턴스 변수로 String number를,

SmartPhone 클래스는 인스턴스 변수로 String androidVer가 있다.

 

SmartPhone ph1 = new SmartPhone("010-1111-2222", "Kitcat");
MobilePhone ph2 = new SmartPhone("010-3333-4444", "Lollipop");

 

여기서 ph2는 MobilePhone 타입의 참조 변수지만, SmartPhone 객체를 가리키고 있다.

이게 가능한 이유가 바로 업캐스팅(upcasting) 때문.

 

업캐스팅이란?

서브클래스의 객체를 부모 타입의 참조 변수에 저장하는 것이다.

즉, ph2는 SmartPhone 객체를 참조하지만 MobilePhone 타입으로 다루는 것.

자식 클래스는 부모 클래스의 모든 속성을 포함하기 때문에, 부모 클래스처럼 취급할 수 있다.

 

즉, SmartPhone은 MobilePhone의 확장된 버전이므로, MobilePhone 타입 변수로 SmartPhone 객체를 저장할 수 있다.

(스마트폰은 모바일폰이다. IS-A 관계)

하지만 반대로는 안 된다!

MobilePhone에는 SmartPhone에만 있는 인스턴스 변수나 메소드가 없기 때문에 SmartPhone처럼 다룰 수 없다.

( SmartPhone ph1 = new MobilePhone()이 안 된다.)

 

여기서 의문이 생긴다.

자식 클래스에는 부모 클래스에 없는 내용도 포함되는데, 왜 자식 객체를 부모 객체에 대입할 수 있는가?

업캐스팅이 안전한 이유

class MobilePhone {
    String number;

    public MobilePhone(String number) {
        this.number = number;
    }

    public void call() {
        System.out.println("전화 걸기: " + number);
    }
}

class SmartPhone extends MobilePhone {
    String os;

    public SmartPhone(String number, String os) {
        super(number);
        this.os = os;
    }

    public void browseInternet() {
        System.out.println("인터넷 서핑 중...");
    }
}

업캐스팅 적용

MobilePhone ph = new SmartPhone("010-1111-2222", "Kitkat");
ph.call();  // 정상 실행
ph.browseInternet();  // 오류! MobilePhone 타입에는 없는 메서드

이 코드에서 ph의 실제 객체는 SmartPhone이지만, MobilePhone 타입으로 참조되기 때문에

MobilePhone 클래스에 정의된 멤버만 사용 가능하다.

즉, 업캐스팅을 하면 자식 클래스에 추가된 기능(예: browseInternet())을 사용할 수 없도록 제한되므로, 안전한 방식이다.

 

다시 돌아와서, 즉 위 예제에서 ph2는 SmartPhone 객체를 가리키지만 MobilePhone의 멤버(변수와 메서드)만 직접 호출할 수 있다는 말이다.

하지만 SmartPhone에서 MobilePhone의 메서드를 오버라이딩했다면, 실제 호출되는 것은 SmartPhone의 메서드이다. (동적 바인딩 때문이다. 이건 밑에서 설명)

 

#그래서, 업캐스팅을 하면 뭐가 좋은건데?

그럼 이 의문도 생긴다. 업캐스팅을 하면 뭐가 좋을까?

업캐스팅을 하면 코드의 유연성, 확장성, 유지보수성이 좋아지고, 다형성(Polymorphism)을 활용할 수 있다.
즉, 부모 클래스를 기준으로 코드를 작성하면, 자식 클래스의 종류가 늘어나도 코드 수정 없이 사용할 수 있다는 점이 가장 큰 장점이다.

 

1. 다형성(Polymorphism) 활용 가능 -> 코드의 재사용성과 유지보수성이 증가한다

같은 부모 클래스를 공유하는 다양한 자식 클래스를 하나의 타입으로 다룰 수 있음.

 

📌 다형성이 없을 때 (중복 코드 문제)

class SmartPhone {
    void call() {
        System.out.println("스마트폰에서 전화");
    }
}

class FeaturePhone {
    void call() {
        System.out.println("피처폰에서 전화");
    }
}

public class Main {
    public static void main(String[] args) {
        SmartPhone sp = new SmartPhone();
        FeaturePhone fp = new FeaturePhone();
        
        sp.call();
        fp.call();
    }
}

위 코드에서는 기기 종류가 늘어날 때마다 새로운 클래스를 추가하고, 객체를 생성하고, 별도로 호출해야 함.

✅ 다형성을 활용하면?

class MobilePhone {
    String number;

    public MobilePhone(String number) {
        this.number = number;
    }

    public void call() {
        System.out.println("전화를 겁니다: " + number);
    }
}

class SmartPhone extends MobilePhone {
    String os;

    public SmartPhone(String number, String os) {
        super(number);
        this.os = os;
    }

    @Override
    public void call() {
        System.out.println("스마트폰에서 전화를 겁니다: " + number);
    }
}

class FeaturePhone extends MobilePhone {
    public FeaturePhone(String number) {
        super(number);
    }

    @Override
    public void call() {
        System.out.println("피처폰에서 전화를 겁니다: " + number);
    }
}

 

이제 부모 클래스 타입으로 여러 종류의 객체를 관리할 수 있음.

public class Main {
    public static void main(String[] args) {
        MobilePhone[] phones = {
            new SmartPhone("010-1234-5678", "Android"),
            new FeaturePhone("010-9876-5432")
        };

        for (MobilePhone phone : phones) {
            phone.call();  // 각각의 실제 객체에 맞는 call()이 실행됨! (동적 바인딩)
        }
    }
}
스마트폰에서 전화를 겁니다: 010-1234-5678
피처폰에서 전화를 겁니다: 010-9876-5432

부모 클래스 타입(MobilePhone)을 사용했기 때문에, 새로운 기기가 추가되더라도 기존 코드를 수정할 필요 없음!
즉, 유지보수성과 확장성이 좋아진다.

 

 

2. 객체의 종류가 바뀌어도 같은 방식으로 사용할 수 있다 (유연성 증가)

다형성이 없으면, 객체의 종류가 바뀔 때마다 if-else문이나 switch문을 사용해야 하지만,
다형성을 활용하면 같은 인터페이스나 부모 클래스를 통해 일관된 방식으로 객체를 다룰 수 있다.

📌 다형성이 없으면? (if-else로 해결해야 함)

class Payment {
    void pay(String method) {
        if (method.equals("CreditCard")) {
            System.out.println("신용카드 결제");
        } else if (method.equals("Paypal")) {
            System.out.println("페이팔 결제");
        }
    }
}

✅ 만약 새로운 결제 방식(Bitcoin)을 추가하면? → 코드를 계속 수정해야 함 (유지보수 어려움)

 

✅ 다형성을 활용하면?

interface Payment {
    void pay();
}

// 기존 결제 방식
class CreditCard implements Payment {
    public void pay() {
        System.out.println("신용카드 결제");
    }
}

class Paypal implements Payment {
    public void pay() {
        System.out.println("페이팔 결제");
    }
}

// 새롭게 추가된 Bitcoin 결제 방식
class Bitcoin implements Payment {
    public void pay() {
        System.out.println("비트코인 결제");
    }
}

public class Main {
    public static void main(String[] args) {
        Payment[] payments = {
            new CreditCard(),
            new Paypal(),
            new Bitcoin() // 새롭게 추가된 결제 방식
        };

        for (Payment payment : payments) {
            payment.pay();  // 다형성을 활용하여 모든 결제 방식 호출
        }
    }
}

Bitcoin 결제 방식이 추가되었지만, 기존 코드(인터페이스 Payment)는 그대로 유지됨.
즉, 기존 for 루프를 수정하지 않아도 새로운 결제 방식이 자동으로 적용됨.
이게 바로 다형성을 활용한 유지보수성 & 확장성 증가의 핵심 장점이다.

 

📌 다형성이 없었다면?

다형성을 사용하지 않고 if-else 방식으로 구현했다면,

새로운 결제 방식이 추가될 때마다 if-else 문을 수정해야 하는 문제가 생겼을 것이다.

밑의 코드는 객체 지향적인 설계 또한 아니다.

class Payment {
    void pay(String method) {
        if (method.equals("CreditCard")) {
            System.out.println("신용카드 결제");
        } else if (method.equals("Paypal")) {
            System.out.println("페이팔 결제");
        } else if (method.equals("Bitcoin")) {  // 새로운 방식 추가될 때마다 수정 필요!
            System.out.println("비트코인 결제");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Payment payment = new Payment();
        payment.pay("CreditCard");
        payment.pay("Paypal");
        payment.pay("Bitcoin");  // 새로운 방식 추가됨
    }
}

다형성을 활용하면 이런 문제 없이 새로운 결제 방식을 쉽게 추가할 수 있다!

 

 

3. 실행 시점(런타임)에서 적절한 메서드가 자동으로 실행된다 (동적 바인딩)

 '동적 바인딩'이란? 컴파일 시점이 아니라 실행 시점에 실제 객체의 메서드가 호출되는 것을 의미한다.

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();  // 멍멍! (Dog의 메서드가 실행됨)

        myAnimal = new Cat();
        myAnimal.makeSound();  // 야옹! (Cat의 메서드가 실행됨)
    }
}

변수 타입이 Animal이어도, 실제 객체가 Dog이면 Dog의 makeSound()가 실행된다.
즉, 컴파일 시점이 아니라 실행 시점에 실제 객체의 메서드가 호출됨(동적 바인딩).
이 덕분에 더 유연하고 확장성 있는 코드를 작성할 수 있다!

 

 

4. 인터페이스를 활용한 유연한 설계 가능

다형성을 활용하면 인터페이스(interface)를 사용해서 다양한 객체를 동일한 방식으로 다룰 수 있다.

interface Vehicle {
    void move();
}

class Car implements Vehicle {
    public void move() {
        System.out.println("자동차가 달립니다.");
    }
}

class Bicycle implements Vehicle {
    public void move() {
        System.out.println("자전거가 달립니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Vehicle v = new Car();
        v.move();  // 자동차가 달립니다.

        v = new Bicycle();
        v.move();  // 자전거가 달립니다.
    }
}

인터페이스를 사용하면 다양한 객체를 '같은 방식'으로 다룰 수 있어, 코드가 더 깔끔하고 확장성이 좋아진다.
추후 새로운 Motorcycle 클래스가 추가되더라도 기존 코드를 수정할 필요가 없다.

// 새로운 Vehicle 추가
class Motorcycle implements Vehicle {
    public void move() {
        System.out.println("오토바이가 달립니다.");
    }
}

기존 코드 수정 없이 Motorcycle 클래스를 추가하고 바로 사용할 수 있다!

 

 

주의할 점

맨 위 코드블럭 예제에서, SmartPhone 클래스는 os라는 멤버변수를 따로 가지고 있다고 가정해보자.

SmartPhone ph1 = new SmartPhone("010-1111-2222", "Kitcat");
MobilePhone ph2 = new SmartPhone("010-3333-4444", "Lollipop");
System.out.println(ph2.os);  // 오류 발생!

 

여기서 ph2의 타입은 MobilePhone이므로, MobilePhone에 없는 멤버(os)에는 직접 접근할 수 없다.

SmartPhone의 기능을 쓰려면 아래와 같이 다운캐스팅(downcasting)이 필요하다.

SmartPhone realPhone = (SmartPhone) ph2;
System.out.println(realPhone.os);  // 정상 동작