[아이티센 부트캠프] (다형성)

이언덕·2026년 3월 25일

아이티센 부트캠프

목록 보기
18/115
post-thumbnail

다형성

1. 다형성이란 무엇인가

다형성은 자식 객체를 부모 타입으로도 다룰 수 있는 것이다.

쉽게말해
조상 타입의 참조변수로 자손 타입의 객체를 다룰 수 있다
라고 표현한다.

처음에는 이 문장이 어렵게 느껴질 수 있는데, 뜻은 단순하다.


예를 들어 CaptionTv가 Tv를 상속했다면, CaptionTv 객체는

  • CaptionTv 타입으로도 다룰 수 있고
  • Tv 타입으로도 다룰 수 있다

즉, 실제로는 자식 객체인데, 부모 타입의 참조변수에 담아서 사용할 수 있다는 뜻이다.


여기서 꼭 같이 기억해야 하는 문장이 있다.

조상 타입의 참조변수로 자손 타입의 객체는 참조할 수 있지만,
반대로 자손 타입의 참조변수로 조상 타입의 객체는 참조할 수 없다.


왜 이런 차이가 생기냐면, 참조변수가 알고 있는 기능 범위와 실제 객체가 가진 기능 범위가 맞아야 하기 때문이다.


자식 객체는 부모 기능을 이미 전부 가지고 있고, 거기에 자기 기능을 더 가지고 있다.
그래서 자식 객체를 부모 타입으로 보면, 부모가 아는 기능만 사용하면 되기 때문에 문제가 없다.


반대로 부모 객체를 자식 타입으로 보게 하면 문제가 생긴다.
자식 타입의 참조변수는 부모 기능뿐 아니라 자식이 추가한 기능도 사용할 수 있다고 생각한다.
그런데 실제 객체가 부모 객체라면, 자식 기능은 아예 존재하지 않는다.

즉, 참조변수는 있다고 믿고 접근하는데 실제 객체에는 없는 기능이 되어 버리므로, 이런 방향은 허용되지 않는다.




2. 가장 먼저 이해해야 하는 그림

다형성은 처음 보면 추상적으로 느껴질 수 있다.
그래서 가장 먼저 Tv와 CaptionTv 관계로 보는 것이 이해하기 쉽다.

CaptionTv는 Tv를 상속한 자식 클래스다.
그래서 CaptionTv 객체는 자식 타입으로도 볼 수 있고, 부모 타입으로도 볼 수 있다.

여기서 중요한 것은 실제 객체의 종류와 참조변수의 타입을 구분해서 보는 것이다.
객체가 바뀌는 것이 아니라, 어떤 타입의 참조변수로 바라보느냐에 따라 바로 사용할 수 있는 멤버의 범위가 달라진다.

핵심 특징

  • CaptionTv는 Tv를 상속했기 때문에 부모의 멤버를 물려받는다.
  • 자식 객체는 부모 기능도 사용할 수 있고, 자신이 추가한 기능도 사용할 수 있다.
  • 자식 객체를 부모 타입으로 참조하는 것은 가능하다.
  • 하지만 부모 타입으로 참조하면 부모가 가진 멤버까지만 바로 사용할 수 있다.
  • 반대로 부모 객체를 자식 타입으로 참조하는 것은 불가능하다.

예제: PolyBasic1

class Tv {
    boolean power;      // 전원 상태
    int channel;        // 현재 채널

    void power() {}      // 전원 켜기/끄기 기능
    void channelUp() {}  // 채널 증가
    void channelDown() {}// 채널 감소
}

class CaptionTv extends Tv {
    boolean caption;                    // 자막 기능 추가
    void displayCaption(String text) {} // 자막 출력 기능 추가
}

public class PolyBasic1 {
    public static void main(String[] args) {
        CaptionTv c = new CaptionTv(); // 자식 타입으로 자식 객체를 참조
        Tv t = new CaptionTv();        // 부모 타입으로 자식 객체를 참조 -> 다형성

        c.channel = 10;   // 가능: 부모에게서 물려받은 멤버
        c.caption = true; // 가능: 자식이 추가한 멤버

        t.channel = 10;   // 가능: Tv에 있는 멤버는 사용 가능
        // t.caption = true;         // 에러: Tv 타입으로는 자식 멤버가 안 보임
        // t.displayCaption("자막"); // 에러: Tv 타입으로는 자식 메서드가 안 보임
    }
}

/*
주석 처리한 두 줄은 컴파일 에러
*/

이 코드는 같은 CaptionTv 객체라도 어떤 타입의 참조변수로 잡느냐에 따라 사용 범위가 달라진다는 점을 보여준다.

CaptionTv c = new CaptionTv();는 자식 타입으로 자식 객체를 참조한 것이다.
그래서 부모에게서 물려받은 channel도 사용할 수 있고, 자식이 추가한 caption도 사용할 수 있다.

즉, c로는 아래 두 가지가 다 가능하다.

  • 부모에게서 물려받은 멤버 사용
  • 자식이 추가한 멤버 사용

반면 Tv t = new CaptionTv();는 실제로 만든 객체는 CaptionTv지만, 참조변수 타입이 Tv다.
그래서 Tv에 선언된 멤버까지만 바로 사용할 수 있다.


예를 들면 t로는 아래처럼 Tv에 있는 멤버만 사용할 수 있다.

  • power
  • channel
  • power()
  • channelUp()
  • channelDown()

하지만 아래처럼 CaptionTv에만 있는 멤버는 바로 사용할 수 없다.

  • caption
  • displayCaption()

즉, 실제 객체 안에는 자식이 추가한 기능까지 모두 들어 있다.
하지만 참조변수의 타입이 부모 타입이면 부모가 가진 범위까지만 바로 보인다.

그래서 다형성은 객체가 바뀌는 개념이 아니라,
하나의 자식 객체를 어떤 타입의 참조변수로 바라보느냐에 따라 바로 사용할 수 있는 멤버가 달라지는 개념이라고 이해하면 된다.


즉, 부모 타입으로 참조하면 부모 기능까지만 바로 보인다.




3. 부모 타입으로 받은 자식 객체는 형변환해서 다시 자식 기능을 사용할 수 있다

Tv t = new CaptionTv();처럼 부모 타입으로 자식 객체를 참조하면,
바로 사용할 수 있는 범위는 Tv에 있는 멤버까지만 제한된다.
그래서 caption이나 displayCaption() 같은 자식 멤버는 바로 사용할 수 없다.

하지만 여기서 중요한 점은 실제 객체가 CaptionTv라는 사실은 그대로라는 것이다.
즉, 지금은 부모 타입으로 보고 있어서 자식 기능이 안 보일 뿐이지,
객체 자체가 부모 객체로 바뀐 것은 아니다.

그래서 필요하면 형변환해서 다시 자식 타입으로 바라볼 수 있다.

예제: PolyBasic2

class Tv {
    boolean power;
    int channel;
}

class CaptionTv extends Tv {
    boolean caption;

    void displayCaption(String text) {
        if (caption) {
            System.out.println(text);
        }
    }
}

public class PolyBasic2 {
    public static void main(String[] args) {
        Tv t = new CaptionTv();   // 부모 타입으로 자식 객체를 참조

        t.channel = 10;           // 가능: Tv에 있는 멤버

        CaptionTv c = (CaptionTv)t; // 자식 타입으로 형변환
        c.caption = true;           // 가능: 자식 멤버 접근
        c.displayCaption("자막 출력"); // 가능: 자식 메서드 호출
    }
}

/*
출력결과
자막 출력
*/

이 코드는 부모 타입으로 받았던 자식 객체를 다시 자식 타입으로 바꿔서 사용하는 흐름을 보여준다.
코드를 순서대로 보면 이렇다.
1. Tv t = new CaptionTv();
// 실제 객체는 CaptionTv
// 하지만 Tv 타입으로 보고 있으므로 부모 멤버까지만 바로 사용 가능


2. CaptionTv c = (CaptionTv)t;
// t가 가리키는 객체를 다시 CaptionTv 타입으로 봄
// 객체가 바뀌는 것이 아니라, 보는 타입만 바뀜


3. c.caption = true;
// 이제 자식 타입으로 보고 있으므로 자식 멤버 사용 가능


4. c.displayCaption("자막 출력");
// 자식이 추가한 메서드도 호출 가능

즉, 부모 타입으로 받아서 공통으로 처리하다가, 필요할 때 다시 자식 타입으로 형변환해서 자식 기능을 꺼내 쓸 수 있다고 이해하면 된다.

헷갈리기 쉬운 부분

형변환은 아무 때나 할 수 있는 것이 아니다.
실제 객체가 정말 그 자식 객체일 때만 가능하다.

예를 들어 이 코드는 가능하다.

Tv t = new CaptionTv();
CaptionTv c = (CaptionTv)t;

하지만 이 코드는 안 된다.

Tv t = new Tv();
CaptionTv c = (CaptionTv)t;

왜냐하면 실제 객체가 Tv인데, CaptionTv라고 우겨서 보는 것이기 때문이다.


즉, 형변환은 “실제 자식 객체를 부모 타입으로 보고 있던 경우”에만 다시 되돌릴 수 있다.




4. 반대 방향은 왜 안 되는가

다형성을 배우면 여기서 꼭 한 번 걸린다.

  • 왜 Tv t = new CaptionTv();는 되는데, CaptionTv c = new Tv();는 안 될까?

답은 참조변수가 기대하는 기능 범위가 다르기 때문이다.

예제: PolyBasic3

class Tv {
    boolean power;
    int channel;
}

class CaptionTv extends Tv {
    boolean caption;
}

public class PolyBasic2 {
    public static void main(String[] args) {
        Tv t = new Tv();           // 부모 객체 생성
        // CaptionTv c = new Tv(); // 에러
    }
}

/*
주석 처리한 줄은 컴파일 에러
*/

이 코드에서 CaptionTv c = new Tv();가 허용되면, c는 자식 타입 참조변수이므로 caption 같은 자식 멤버도 사용할 수 있다고 생각하게 된다.

그런데 실제 객체는 그냥 Tv다.
즉, caption이라는 멤버는 아예 없다.

그래서 이런 식의 참조는 안전하지 않다.
즉, 참조변수가 알고 있는 기능 수보다 실제 객체가 가진 기능 수가 적으면 안 된다고 이해하면 된다.

헷갈리기 쉬운 부분

많이 헷갈리는 부분은 이거다.

  • “자식은 부모 기능을 다 쓰는데, 왜 부모는 자식 기능을 못 쓰는가?”

이건 단순히 부모-자식 관계 때문만이 아니라, 참조변수 타입이 무엇이냐가 더 중요하다.

  • CaptionTv 타입으로 보면 부모 기능 + 자식 기능이 모두 보인다.
  • Tv 타입으로 보면 Tv에 선언된 기능까지만 보인다.

즉, 실제 객체가 자식이라고 해도, 부모 타입으로 보고 있는 순간 자식이 추가한 기능은 바로 보이지 않는다.


정리하면 이렇게 된다.

  • Tv t = new CaptionTv(); → 가능
    → 부모가 아는 범위로만 사용하면 되기 때문

  • CaptionTv c = new Tv(); → 불가능
    → 자식 기능까지 있다고 생각하고 접근하게 되기 때문



5. 매개변수의 다형성

다형성은 단순히 부모 타입으로 자식 객체를 참조할 수 있다에서 끝나는 개념이 아니다.
진짜 중요한 이유는 서로 다른 객체들을 공통 부모 타입으로 묶어서, 같은 방식으로 처리할 수 있게 해 주기 때문이다.

예를 들어 Tv, Computer, Audio는 서로 다른 물건이지만, 모두 상품이라는 공통 성격을 가진다.
그리고 구매할 때 필요한 정보도 비슷하다.
대표적으로 가격과 보너스 점수가 있다.

즉, 물건 종류는 달라도 구매할 때 하는 일은 거의 같다.

  • 가격만큼 돈을 차감하고
  • 보너스 점수를 누적한다

이 공통 처리를 한 번에 묶기 위해 다형성이 필요하다.


먼저 흐름부터 보기

예를 들어 아래처럼 구매 기능을 만든다고 해 보자.

class Buyer {
    int money = 1000;
    int bonusPoint = 0;

    void buy(Tv t) {
        money -= t.price;
        bonusPoint += t.bonusPoint;
    }
}

여기서 Tv t는 매개변수다.
즉, buy() 메서드를 호출할 때 전달받은 Tv 객체를 임시로 담아 두는 변수라고 보면 된다.

예를 들어 아래처럼 호출하면,

Buyer b = new Buyer();
b.buy(new Tv());

new Tv()로 만든 객체가 buy() 안으로 들어오고,
그 객체를 메서드 안에서는 t라는 이름으로 부르게 된다.

즉,

b.buy(new Tv());

이 호출이 일어나면,
메서드 안에서는 전달된 Tv 객체를 t로 받아서 사용하는 것이다.


Tv만 받는 경우의 문제

처음에는 Tv만 산다고 생각하면 buy(Tv t)로 만들 수 있다.
하지만 나중에 Computer도 사고, Audio도 살 수 있게 하려면 문제가 생긴다.

왜냐하면 지금 메서드는 Tv 객체만 받을 수 있기 때문이다.
그래서 이런 식으로 메서드를 계속 늘려야 한다.

void buy(Tv t) { ... }
void buy(Computer c) { ... }
void buy(Audio a) { ... }

그런데 이 메서드들이 하는 일은 거의 똑같다.
받는 객체 이름만 다르고, 실제 처리 방식은 같다.
이렇게 되면 비슷한 코드가 계속 중복된다.


공통 부모로 묶기

그래서 Tv, Computer, Audio가 공통으로 가지는 부모 클래스를 만든다.

class Product {
    int price;
    int bonusPoint;

    Product(int price) {
        this.price = price;
        bonusPoint = price / 10;
    }
}

class Tv extends Product {
    Tv() {
        super(100);
    }
}

class Computer extends Product {
    Computer() {
        super(200);
    }
}

class Audio extends Product {
    Audio() {
        super(50);
    }
}

이제 Tv, Computer, Audio는 서로 다른 객체이지만, 전부 Product를 상속한 자식 클래스가 된다.
즉, 세 객체 모두 Product의 자식이라는 공통점이 생긴다.

buy(Product p)로 바꾸는 이유

이제 구매 메서드를 아래처럼 바꿀 수 있다.

class Buyer {
    int money = 1000;
    int bonusPoint = 0;

    void buy(Product p) {
        money -= p.price;
        bonusPoint += p.bonusPoint;
    }
}

여기서 Product p도 매개변수다.
차이는 이제 Tv만 받는 것이 아니라, Product를 상속한 모든 자식 객체를 받을 수 있다는 점이다.

즉, 아래 호출이 전부 가능해진다.

Buyer b = new Buyer();

b.buy(new Tv());
b.buy(new Computer());
b.buy(new Audio());

호출할 때 넘기는 객체는 각각 다르지만,
받는 쪽에서는 전부 Product 타입인 p 하나로 처리한다.

즉,

  • new Tv()를 넘기면 그 객체를 p가 받는다.
  • new Computer()를 넘기면 그 객체를 p가 받는다.
  • new Audio()를 넘기면 그 객체를 p가 받는다.

받은 객체의 실제 종류는 달라도,
공통 부모인 Product가 가진 price, bonusPoint를 사용할 수 있기 때문에 같은 방식으로 처리할 수 있다.


예제: 전체 흐름 보기

이제 실제로는 어떤 클래스에서 객체를 만들고, 어떤 흐름으로 buy()가 호출되는지 전체 예제를 보면 이해가 더 쉽다.

class Product {
    int price;
    int bonusPoint;

    Product(int price) {
        this.price = price;
        this.bonusPoint = price / 10;
    }
}

class Tv extends Product {
    Tv() {
        super(100);
    }
}

class Computer extends Product {
    Computer() {
        super(200);
    }
}

class Audio extends Product {
    Audio() {
        super(50);
    }
}

class Buyer {
    int money = 1000;
    int bonusPoint = 0;

    void buy(Product p) {
        money -= p.price;
        bonusPoint += p.bonusPoint;
        System.out.println(p.price + "원짜리 상품을 구매했습니다.");
    }
}

public class PolyTest {
    public static void main(String[] args) {
        Buyer b = new Buyer();
        // 1. Buyer 객체를 생성하고 b가 참조함

        b.buy(new Tv());
        // 2. Tv 객체를 생성해서 buy()에 전달함
        // 3. buy(Product p)에서 전달된 Tv 객체를 p가 받음
        // 4. p.price, p.bonusPoint를 사용해서 구매 처리함

        b.buy(new Computer());
        // 5. Computer 객체를 생성해서 buy()에 전달함
        // 6. buy(Product p)에서 전달된 Computer 객체를 p가 받음

        b.buy(new Audio());
        // 7. Audio 객체를 생성해서 buy()에 전달함
        // 8. buy(Product p)에서 전달된 Audio 객체를 p가 받음

        System.out.println("남은 돈은 " + b.money + "원입니다.");
        System.out.println("현재 보너스 점수는 " + b.bonusPoint + "점입니다.");

        // 출력결과
        // 100원짜리 상품을 구매했습니다.
        // 200원짜리 상품을 구매했습니다.
        // 50원짜리 상품을 구매했습니다.
        // 남은 돈은 650원입니다.
        // 현재 보너스 점수는 35점입니다.
    }
}

이 예제에서 중요한 점은 Buyer b = new Buyer();, b.buy(new Tv()); 같은 코드가
따로 튀어나온 것이 아니라 main() 메서드 안에서 실제로 객체를 만들고 호출하는 실행 흐름이라는 점이다.

즉,

  • Buyer 객체를 먼저 만들고
  • Tv, Computer, Audio 객체를 각각 생성해서
  • buy(Product p) 메서드에 전달하면
  • 메서드 안에서는 모두 Product 타입 하나로 받아서 같은 방식으로 처리한다

이렇게 보면 다형성이 왜 필요한지 더 자연스럽게 연결된다.


왜 다형성이 필요한가

결국 다형성이 필요한 이유는
객체 종류마다 메서드를 따로 만들지 않고, 공통된 부분을 기준으로 하나로 묶어 처리하기 위해서다.

즉, 다형성은

  • 공통 처리 구조를 만들기 쉽고
  • 코드 중복을 줄일 수 있고
  • 새로운 상품 종류가 늘어나도 기존 메서드를 크게 바꾸지 않아도 되는

더 유연한 구조를 만들게 해 준다.

그래서 다형성은 단순히 “부모 타입으로 자식을 참조한다”는 문법 개념이 아니라,
여러 자식 객체를 하나의 부모 타입으로 묶어서 같은 방식으로 처리하기 위한 핵심 개념이라고 이해하면 된다.

헷갈리기 쉬운 부분

buy(Product p)라고 해서 Product 객체만 받을 수 있는 것은 아니다.
Product를 상속한 Tv, Computer, Audio 객체도 모두 받을 수 있다.

또한 p는 새로운 특별한 객체가 아니라,
호출할 때 전달된 자식 객체를 부모 타입으로 받아서 가리키는 변수일 뿐이다.

즉, p가 받는 것은 Product 모양으로 보는 자식 객체라고 이해하면 된다.




6. 코드로 먼저 감 잡기

이제 실제 코드로 보면 더 쉽게 보인다.
PolyTest는 하나의 매개변수로 여러 종류의 객체를 받을 수 있다는 점을 보여주는 가장 기본적인 예제다.

예제: PolyTest

package day7;

import java.util.Date;

public class PolyTest {

    public static void main(String[] args) {
        printInfo("가나다");                         // String 객체 전달
        printInfo(new Date());                     // Date 객체 전달
        printInfo("ABC");                          // String 객체 전달
        printInfo(new int[10]);                    // int 배열 객체 전달
        printInfo(new A());                        // 사용자 정의 객체 전달
        printInfo(new java.io.File("/education")); // File 객체 전달
        printInfo(Integer.valueOf(100));           // Integer 객체 전달
        printInfo(new Object());                   // Object 객체 전달
    }

    static void printInfo(Object obj) {            // 모든 객체를 받을 수 있게 Object로 선언
        System.out.println(
            "obj변수에 전달된 객체의 클래스명: " + obj.getClass().getName()
        );
    }
}

/*
출력결과
obj변수에 전달된 객체의 클래스명: java.lang.String
obj변수에 전달된 객체의 클래스명: java.util.Date
obj변수에 전달된 객체의 클래스명: java.lang.String
obj변수에 전달된 객체의 클래스명: [I
obj변수에 전달된 객체의 클래스명: day7.A
obj변수에 전달된 객체의 클래스명: java.io.File
obj변수에 전달된 객체의 클래스명: java.lang.Integer
obj변수에 전달된 객체의 클래스명: java.lang.Object
*/

이 코드에서 가장 중요한 줄은 이것이다.

static void printInfo(Object obj)

매개변수 타입이 Object라서 문자열, 날짜, 배열, 파일, 일반 객체를 전부 받을 수 있다.
이게 가능한 이유는 모든 클래스의 최고 조상이 Object이기 때문이다.

즉, 서로 다른 객체들을 부모 하나로 묶어서 받을 수 있는 것이다.

코드 흐름

1) 호출하는 쪽

printInfo("가나다");
printInfo(new Date());
printInfo(new Object());

여기서는 서로 다른 종류의 객체를 같은 메서드에 전달하고 있다.
즉, 객체 종류는 달라도 메서드 이름은 하나다.



2) 받는 쪽

static void printInfo(Object obj)

받는 쪽에서는 종류가 다른 객체들을 전부 Object 타입 하나로 받는다.
즉, 여러 객체를 부모 하나로 묶어서 받는 구조다.



3) 확인하는 쪽

System.out.println(obj.getClass().getName());

이 코드는 “지금 실제로 들어온 객체가 무슨 클래스인지”를 출력한다.
그래서 같은 printInfo()를 호출해도 결과가 서로 다르게 나온다.

여기서 출력 결과 중 [I가 조금 낯설 수 있다.
배열은 클래스명이 조금 특이하게 출력되는데, 여기서 [I는 int 배열이라는 뜻이다.

즉, 받는 변수 타입은 하나지만 실제 객체는 여러 종류일 수 있다는 점을 확인하는 코드다.




7. 검사 후 형변환까지 보기

PolyTest가 “여러 객체를 하나로 받을 수 있다”를 보여주는 코드라면,
PolyTest_1은 그다음 단계인 검사 후 형변환까지 보여준다.

예제: PolyTest_1

package day7;

import java.util.Date;

public class PolyTest_1 {

    public static void main(String[] args) {
        printInfo("가나다");                         // String 객체 전달
        printInfo("ABC");                          // String 객체 전달
        printInfo(new Date());                     // Date 객체 전달
        printInfo(new int[10]);                    // 배열 객체 전달
        printInfo(new A());                        // 사용자 정의 객체 전달
        printInfo(new java.io.File("/education")); // File 객체 전달
        printInfo(Integer.valueOf(100));           // Integer 객체 전달
        printInfo(new Object());                   // Object 객체 전달
    }

    static void printInfo(Object obj) {            // 일단 모두 Object로 받음
        if (obj instanceof String) {               // 실제 객체가 String인지 먼저 확인
            System.out.println(
                "String 객체가 전달됨 -"
                + obj.toString()                   // Object에도 있는 공통 메서드
                + "-"
                + ((String)obj).length()           // String으로 형변환 후 length() 사용
            );
        } else {
            System.out.println(
                "obj변수에 전달된 객체의 클래스명: "
                + obj.getClass().getName()
            );
        }
    }
}

/*
출력결과
String 객체가 전달됨 -가나다-3
String 객체가 전달됨 -ABC-3
obj변수에 전달된 객체의 클래스명: java.util.Date
obj변수에 전달된 객체의 클래스명: [I
obj변수에 전달된 객체의 클래스명: day7.A
obj변수에 전달된 객체의 클래스명: java.io.File
obj변수에 전달된 객체의 클래스명: java.lang.Integer
obj변수에 전달된 객체의 클래스명: java.lang.Object
*/

이 코드에서 중요한 상황은 이것이다.

printInfo("가나다");

호출할 때 넘기는 "가나다"는 실제로 String 객체다.
그런데 메서드 안으로 들어오면 이 객체를 담는 변수 obj의 타입은 Object다.


즉, 지금 상태는 이렇게 된다.

  • 실제 객체: String
  • 참조변수 타입: Object

이 상태에서는 obj.length()를 바로 쓸 수 없다.
왜냐하면 length()는 String의 메서드이고, Object 타입 변수로는 Object에 있는 기능만 바로 쓸 수 있기 때문이다.


그래서 순서가 이렇게 간다.

1) 먼저 검사

if (obj instanceof String)

이건 “지금 obj 안에 들어 있는 실제 객체가 정말 String이냐”를 확인하는 것이다.



2) 맞으면 형변환

(String)obj

이제 Object로 보고 있던 값을 String으로 다시 보겠다는 뜻이다.
객체가 바뀌는 게 아니라, 보는 타입을 바꾸는 것이다.

이 검사가 필요한 이유는, 실제 객체가 그 타입이 아닐 때 무작정 형변환하면 오류가 날 수 있기 때문이다.
그래서 먼저 확인하고, 그다음에 형변환한다.



3) 그다음 자식 기능 사용

((String)obj).length()

이제야 String 전용 메서드인 length()를 사용할 수 있다.


즉, 부모 타입으로 받은 뒤 필요하면 검사 후 형변환해서 자식 기능을 쓴다.


반복문 안에서는 이렇게 이어진다

방금 본 예제는 obj 하나를 검사한 뒤 형변환해서 사용하는 흐름이었다.
이 원리는 객체가 여러 개 들어 있는 배열이나 리스트를 for문으로 처리할 때도 그대로 이어진다.

즉, 객체를 하나 다룰 때는
검사 → 형변환 → 그 타입의 기능 사용
순서로 갔다면,

여러 객체를 반복문으로 다룰 때도
각 원소를 하나씩 꺼내면서 같은 순서를 반복하게 된다.


예를 들어 부모 타입 배열 안에 여러 종류의 객체가 함께 들어 있으면,
반복문 안에서 꺼낸 값도 먼저는 부모 타입으로 보게 된다.
그래서 공통 기능은 바로 사용할 수 있지만,
특정 타입에만 있는 기능은 바로 사용할 수 없다.

이럴 때도 먼저 instanceof로 현재 꺼낸 객체가 정말 그 타입인지 확인하고, 맞을 때만 형변환해서 그 타입 전용 기능을 사용한다.

즉,
객체 하나에서 하던 검사 후 형변환이 반복문 안으로 그대로 들어온 형태라고 이해하면 된다.

예제: PolyTest_2

package day7;

public class PolyTest_2 {

    public static void main(String[] args) {
        Object[] arr = {"java", 100, "HTML", true};
        // 서로 다른 타입의 값을 Object 배열에 함께 저장함

        for (int i = 0; i < arr.length; i++) {
            // 배열 원소를 하나씩 꺼내 확인함

            if (arr[i] instanceof String) {
                // 현재 원소가 String인지 먼저 검사함

                String str = (String) arr[i];
                // String이 맞을 때만 형변환함

                System.out.println(str + " : " + str.length());
                // 형변환 후 String 전용 기능인 length()를 사용함
            }
        }
    }
}

/*
출력결과
java : 4
HTML : 4
*/

이 코드에서는 Object[] 배열 안에 문자열, 정수, 논리값이 함께 들어 있다.
즉, 여러 종류의 객체를 부모 타입 하나로 묶어서 저장한 상태다.

반복문이 돌면 배열 원소를 하나씩 꺼내는데,
꺼낼 때마다 바로 length()를 호출할 수는 없다.
왜냐하면 배열의 타입이 Object[]라서, 꺼낸 값도 먼저 Object로 보기 때문이다.

그래서 먼저 instanceof로 현재 원소가 String인지 확인한다.
그리고 String일 때만 (String)으로 형변환한 뒤
length()를 호출한다.

즉, 반복문 안에서도 흐름은 똑같다.

  • 먼저 검사하고
  • 맞으면 형변환하고
  • 그다음 그 타입의 기능을 사용한다

결국 instanceof는
여러 객체를 반복해서 처리하는 중에, 필요한 타입만 안전하게 골라내기 위해 사용하는 검사라고 이해하면 된다.




8. 여러 종류의 객체를 하나로 묶어서 다루기

다형성의 진짜 장점은 여기서 더 잘 보인다.
메서드 하나로 받는 것에서 끝나는 게 아니라, 여러 자식 객체를 한곳에 모아서 저장하고 관리할 수도 있다.

아까 본 Product 예시를 조금 더 실제처럼 바꿔 보면 이해가 더 쉽다.

예제: Product[]로 장바구니 만들기

class Product {
    int price;       // 제품 가격
    int bonusPoint;  // 제품 구매 시 적립되는 보너스 점수

    Product(int price) {
        this.price = price;
        bonusPoint = price / 10; // 가격의 10%를 보너스 점수로 저장
    }
}

class Tv extends Product {
    Tv() {
        super(300); // Tv 가격은 300
    }

    public String toString() {
        return "Tv";
    }
}

class Computer extends Product {
    Computer() {
        super(200); // Computer 가격은 200
    }

    public String toString() {
        return "Computer";
    }
}

class Audio extends Product {
    Audio() {
        super(100); // Audio 가격은 100
    }

    public String toString() {
        return "Audio";
    }
}

class Buyer {
    int money = 1000;
    int bonusPoint = 0;
    Product[] cart = new Product[3]; // Product 타입 배열
    int index = 0;                   // 현재 저장할 위치

    void buy(Product p) {            // Tv, Computer, Audio를 모두 받을 수 있음
        money -= p.price;
        bonusPoint += p.bonusPoint;
        cart[index++] = p;           // 구입한 제품을 장바구니에 저장
        System.out.println(p + "를 구입했습니다.");
    }
}

public class PolyProductTest {
    public static void main(String[] args) {
        Buyer b = new Buyer();

        b.buy(new Tv());
        b.buy(new Computer());
        b.buy(new Audio());
    }
}

/*
출력결과
Tv를 구입했습니다.
Computer를 구입했습니다.
Audio를 구입했습니다.
*/

이 코드에서 먼저 봐야 할 것은 배열 선언 부분이다.

Product[] cart = new Product[3];

이 뜻은 Product 타입의 데이터를 3개 저장할 수 있는 배열을 만들겠다는 뜻이다.
그런데 여기서 중요한 점은 Tv, Computer, Audio가 모두 Product를 상속한 자식 클래스라는 것이다.


즉,

  • Tv도 Product
  • Computer도 Product
  • Audio도 Product

라는 관계가 성립한다.

그래서 Product 타입 배열에는 Product 객체만 담는 것이 아니라,
Product의 자식 객체들도 함께 담을 수 있다.


이제 buy() 메서드를 보자.

void buy(Product p)

이 메서드는 겉으로 보면 Product 하나만 받는 것처럼 보인다.
하지만 실제로는 Product를 상속한 자식 객체들도 받을 수 있다.


그래서 아래 코드가 모두 가능하다.

b.buy(new Tv());
b.buy(new Computer());
b.buy(new Audio());

이렇게 하면 장바구니 배열 cart 안에는 순서대로 Tv, Computer, Audio 객체가 들어가게 된다.
배열 타입은 Product[] 하나지만, 실제로 담기는 객체의 종류는 서로 달라도 괜찮다.


또 이번 예제에서는 price와 bonusPoint도 같이 들어 있어서, 왜 buy(Product p)가 유용한지 더 잘 보인다.

money -= p.price;
bonusPoint += p.bonusPoint;

이 코드는 Tv, Computer, Audio 각각에 대해 따로 작성한 것이 아니다.
공통 부모인 Product에 price, bonusPoint가 있기 때문에, 부모 타입 하나로 공통 처리할 수 있는 것이다.


즉, 다형성은 단순히
“부모 타입으로 자식 객체를 참조할 수 있다”에서 끝나는 개념이 아니라,
공통된 기준으로 여러 객체를 한 번에 처리하고 저장할 수 있게 해 주는 구조라고 볼 수 있다.


즉, 다형성은 메서드와 배열을 훨씬 깔끔하게 만들어 준다.




9. 헷갈리기 쉬운 부분

1) 객체와 참조변수 타입은 다를 수 있다

다형성에서는 이 둘을 꼭 구분해야 한다.

  • 실제 객체 = new로 만든 것
  • 참조변수 타입 = 그 객체를 어떤 타입으로 보고 있는가

객체는 그대로인데, 참조변수 타입 때문에 보이는 기능 범위가 달라진다.
예를 들어

Tv t = new CaptionTv();
Object obj = "가나다";

이 두 문장은 같은 원리로 볼 수 있다.

  • 첫 번째는 CaptionTv 객체를 Tv 타입으로 받고 있는 것
  • 두 번째는 String 객체를 Object 타입으로 받고 있는 것

즉, 실제 객체는 자식일 수 있지만, 부모 타입으로 보고 있는 동안에는 부모가 가진 기능까지만 바로 사용할 수 있다.

2) 부모 타입으로 받았다고 해서 객체가 부모로 바뀌는 것은 아니다

Tv t = new CaptionTv();라고 해도 실제 객체는 여전히 CaptionTv다.
다만 Tv 타입으로 보고 있기 때문에 Tv에 있는 기능까지만 바로 사용할 수 있을 뿐이다.

즉, 조상 타입으로 받았다고 해서 객체가 조상으로 바뀌는 것은 아니다.
객체는 그대로이고, 참조변수 타입 때문에 보이는 범위만 달라지는 것이다.

3) 자식 기능을 쓰고 싶으면 형변환이 필요할 수 있다

부모 타입으로 넓게 받은 뒤, 실제 객체가 자식임이 확인되면 형변환해서 자식 기능을 사용할 수 있다.

4) 형변환은 아무 때나 하는 것이 아니다

무작정 형변환하면 안 된다.
실제 객체가 그 타입이 맞는지 먼저 확인해야 한다.

그래서 instanceof로 검사한 뒤 형변환하는 흐름이 중요하다.

5) 다형성은 메서드와 배열에서 특히 강력하다

  • 매개변수를 부모 타입으로 두면 여러 자식 객체를 같은 메서드로 처리할 수 있다.
  • 부모 타입 배열을 만들면 여러 자식 객체를 같은 배열에 저장할 수 있다.



핵심만 다시 정리하면

  • 다형성은 자식 객체를 부모 타입으로도 다룰 수 있는 것이다.
  • 책에서는 이것을 조상 타입의 참조변수로 자손 타입의 객체를 다룬다고 표현한다.
  • 반대로 자손 타입의 참조변수로 조상 타입의 객체를 다루는 것은 불가능하다.
  • 부모 타입으로 보면 부모가 가진 기능까지만 바로 사용할 수 있다.
  • 실제 객체가 자식 객체라면, 필요할 때 검사 후 형변환해서 자식 기능을 사용할 수 있다.
  • 다형성의 진짜 장점은 여러 객체를 공통 부모 하나로 묶어서 같은 방식으로 처리할 수 있다는 점이다.
  • 그래서 메서드도 줄이고, 배열 저장도 통일하고, 처리 흐름도 훨씬 깔끔하게 만들 수 있다.

0개의 댓글