Java 26일 코스 - Day 12: 다형성과 캐스팅

Day 12: 다형성과 캐스팅

다형성(Polymorphism)은 같은 타입의 변수가 실제로는 다양한 형태의 객체를 참조할 수 있는 성질입니다. 마치 “동물”이라는 한 단어로 강아지, 고양이, 새 등 다양한 동물을 가리킬 수 있는 것과 같습니다. 이를 통해 유연하고 확장 가능한 코드를 작성할 수 있습니다.

업캐스팅과 다형성

자식 객체를 부모 타입 변수에 담는 것이 업캐스팅입니다. 자동으로 이루어집니다.

class Animal {
    String name;

    Animal(String name) {
        this.name = name;
    }

    void speak() {
        System.out.println(name + "이(가) 소리를 냅니다.");
    }

    void eat() {
        System.out.println(name + "이(가) 먹이를 먹습니다.");
    }
}

class Dog extends Animal {
    Dog(String name) { super(name); }

    @Override
    void speak() {
        System.out.println(name + ": 멍멍!");
    }

    void fetch() {
        System.out.println(name + "이(가) 공을 물어옵니다.");
    }
}

class Cat extends Animal {
    Cat(String name) { super(name); }

    @Override
    void speak() {
        System.out.println(name + ": 야옹~");
    }

    void scratch() {
        System.out.println(name + "이(가) 발톱을 세웁니다.");
    }
}

class Bird extends Animal {
    Bird(String name) { super(name); }

    @Override
    void speak() {
        System.out.println(name + ": 짹짹!");
    }

    void fly() {
        System.out.println(name + "이(가) 하늘을 날아갑니다.");
    }
}

public class PolymorphismBasic {
    public static void main(String[] args) {
        // 업캐스팅: 자식 객체를 부모 타입으로 참조
        Animal animal1 = new Dog("바둑이");
        Animal animal2 = new Cat("나비");
        Animal animal3 = new Bird("짹짹이");

        // 다형성: 같은 메서드 호출이지만 실제 객체에 따라 다른 동작
        animal1.speak(); // 바둑이: 멍멍!
        animal2.speak(); // 나비: 야옹~
        animal3.speak(); // 짹짹이: 짹짹!

        // 배열로 다형성 활용
        Animal[] animals = {animal1, animal2, animal3};
        for (Animal animal : animals) {
            animal.speak(); // 각 동물의 고유 소리
            animal.eat();   // 공통 메서드
        }

        // animal1.fetch(); // 컴파일 에러! (Animal 타입에는 fetch가 없음)
    }
}

다운캐스팅과 instanceof

부모 타입 변수를 자식 타입으로 변환할 때는 명시적 캐스팅이 필요합니다.

public class DowncastingExample {
    static void handleAnimal(Animal animal) {
        // 공통 동작
        animal.speak();

        // instanceof로 실제 타입 확인 후 다운캐스팅
        if (animal instanceof Dog) {
            Dog dog = (Dog) animal;
            dog.fetch(); // Dog 고유 메서드 사용 가능
        } else if (animal instanceof Cat) {
            Cat cat = (Cat) animal;
            cat.scratch();
        } else if (animal instanceof Bird) {
            Bird bird = (Bird) animal;
            bird.fly();
        }
    }

    // Java 16+: 패턴 매칭 instanceof
    static void handleAnimalModern(Animal animal) {
        animal.speak();

        // 타입 확인과 변수 선언을 한 번에
        if (animal instanceof Dog dog) {
            dog.fetch();
        } else if (animal instanceof Cat cat) {
            cat.scratch();
        } else if (animal instanceof Bird bird) {
            bird.fly();
        }
    }

    public static void main(String[] args) {
        Animal[] animals = {
            new Dog("바둑이"),
            new Cat("나비"),
            new Bird("짹짹이")
        };

        for (Animal animal : animals) {
            System.out.println("--- " + animal.name + " ---");
            handleAnimalModern(animal);
        }

        // 잘못된 다운캐스팅은 ClassCastException 발생
        // Animal a = new Cat("나비");
        // Dog d = (Dog) a; // ClassCastException!
    }
}

다형성을 활용한 설계

결제 시스템을 다형성으로 설계하는 실전 예제입니다.

class Payment {
    String payerName;
    long amount;

    Payment(String payerName, long amount) {
        this.payerName = payerName;
        this.amount = amount;
    }

    boolean process() {
        System.out.println("기본 결제 처리");
        return true;
    }

    void printReceipt() {
        System.out.println("--- 영수증 ---");
        System.out.println("결제자: " + payerName);
        System.out.println("금액: " + String.format("%,d", amount) + "원");
    }
}

class CreditCardPayment extends Payment {
    String cardNumber;

    CreditCardPayment(String payerName, long amount, String cardNumber) {
        super(payerName, amount);
        this.cardNumber = cardNumber;
    }

    @Override
    boolean process() {
        String masked = "****-****-****-" + cardNumber.substring(cardNumber.length() - 4);
        System.out.println("신용카드 결제: " + masked);
        return true;
    }
}

class BankTransfer extends Payment {
    String bankName;

    BankTransfer(String payerName, long amount, String bankName) {
        super(payerName, amount);
        this.bankName = bankName;
    }

    @Override
    boolean process() {
        System.out.println(bankName + " 계좌이체 처리 중...");
        return true;
    }
}

class MobilePayment extends Payment {
    String phoneNumber;

    MobilePayment(String payerName, long amount, String phoneNumber) {
        super(payerName, amount);
        this.phoneNumber = phoneNumber;
    }

    @Override
    boolean process() {
        System.out.println("모바일 결제 (전화번호: " + phoneNumber + ")");
        return true;
    }
}

public class PaymentSystem {
    // 다형성 덕분에 결제 수단에 관계없이 동일한 코드로 처리
    static void processPayment(Payment payment) {
        if (payment.process()) {
            payment.printReceipt();
            System.out.println("결제 완료!\n");
        }
    }

    public static void main(String[] args) {
        Payment[] payments = {
            new CreditCardPayment("홍길동", 50000, "1234-5678-9012-3456"),
            new BankTransfer("김영희", 100000, "국민은행"),
            new MobilePayment("이철수", 15000, "010-1234-5678")
        };

        for (Payment payment : payments) {
            processPayment(payment); // 모두 같은 메서드로 처리
        }
    }
}

sealed 클래스 (Java 17+)

상속할 수 있는 클래스를 제한하는 기능입니다.

// sealed: 허용된 자식만 상속 가능
sealed class Notification permits EmailNotification, SmsNotification, PushNotification {
    String recipient;
    String message;

    Notification(String recipient, String message) {
        this.recipient = recipient;
        this.message = message;
    }

    void send() {
        System.out.println("알림 전송: " + message);
    }
}

final class EmailNotification extends Notification {
    String subject;

    EmailNotification(String recipient, String message, String subject) {
        super(recipient, message);
        this.subject = subject;
    }

    @Override
    void send() {
        System.out.println("이메일 발송 -> " + recipient + " [" + subject + "]");
    }
}

final class SmsNotification extends Notification {
    SmsNotification(String recipient, String message) {
        super(recipient, message);
    }

    @Override
    void send() {
        System.out.println("SMS 발송 -> " + recipient);
    }
}

non-sealed class PushNotification extends Notification {
    PushNotification(String recipient, String message) {
        super(recipient, message);
    }

    @Override
    void send() {
        System.out.println("Push 알림 -> " + recipient);
    }
}

오늘의 연습문제

  1. 도형 계산기: Shape 배열에 Circle, Rectangle, Triangle 객체를 담고, 반복문으로 각 도형의 이름과 넓이를 출력하세요. 다형성을 활용하여 타입별 분기 없이 처리하세요.

  2. 할인 정책 시스템: DiscountPolicy 부모 클래스를 만들고, PercentDiscount(비율 할인), FixedDiscount(정액 할인), NoDiscount를 자식으로 구현하세요. applyDiscount(long price) 메서드를 다형성으로 처리하세요.

  3. 동물원 시뮬레이션: 다양한 동물 객체를 배열에 담고, instanceof(패턴 매칭)를 사용하여 동물 종류별로 특수 행동을 실행하는 프로그램을 작성하세요. 각 동물의 먹이 종류와 양도 출력하세요.

이 글이 도움이 되었나요?