[Java] 다형성

7 분 소요

다형성이란?

지금까지 설명한 묵시적 클래스 형 변환과 가상 메서드를 바탕으로 객체 지향의 중요한 특성인 다형성(polymorphism) 을 알아보자. 다형성이란, 하나의 코드가 여러 자료형으로 구현되어 실행되는 것을 말한다. 쉽게 말해 같은 코드에서 여러 실행결과가 나오는 것으로, 예제를 통해 살펴보자.

만약 Animal 클래스가 있고 이를 상속받아 Human, Tiger, Eagle 클래스를 만들어본다고 가정해보자.
Animal 클래스에 메서드를 하나 정의하고 이를 상속받은 클래스에서 재정의 해보자.

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
32
33
34
35
36
37
package polymorphism;

class Animal{
    public void move(){
        System.out.println("동물이 움직입니다.");
    }
}
class Human extends Animal{
    public void move(){
        System.out.println("사람이 두 발로 걷습니다.");
    }
}

class Eagle extends Animal{
    public void move(){
        System.out.println("독수리가 하늘을 납니다.");
    }

}
class Tiger extends Animal{
    public void move(){
        System.out.println("호랑이가 네발로 뜁니다.");
    }

}
public class AnimalTest{
    public static void main(String[] args){
        AnimalTest aTest = new AnimalTest();
        aTest.moveAnimal(new Human());
        aTest.moveAnimal(new Eagle());
        aTest.moveAnimal(new Tiger());

    }
    public void moveAnimal(Animal animal){
        animal.move();
    }
}

위 코드는 Animal 클래스를 하나 만들고, 이를 상속하는 클래스 Human, Tiger, Eagle 을 만들어준 뒤 Animal의 메서드인 move를 오버라이딩하여 이를 main 함수에서 moveAnimal 메서드를 통해 사용되도록 했다.

여기서 moveAnimal 메서드는 어떤 인스턴스가 매개변수로 넘어와도 모두 Animal 형으로 변환한다. 예를 들어 매개변수 부분에 Human 인스턴스가 전달되었다면, 다음코드처럼 형 변환된다.

1
Animal ani = new Human();

Animal에서 상속받은 클래스가 매개변수로 넘어오면 모두 Animal형으로 변환되므로 animal.move() 메서드를 호출할 수 있다. 가상 메서드 원리에 따라 animal.move() 메서드가 호출하는 메서드는 Animal의 move가 아닌 매개변수로 넘어온 실제 인스턴스의 메서드이다. 따라서 animal.move() 메서드는 변함이 없지만 어떤 메서드가 넘어왔느냐에 따라 출력문이 달라지는데,
이를 다향성이라 한다.

다향성의 장점

다른 동물이 새로 추가되는 경우를 생각해보자. 새로운 동물도 Animal 클래스를 상속받아서 구현하면 모든 클래스를 Animal 자료형 하나로 쉽게 관리할 수 있을것이다. 이것이 바로 다형성을 활용한 프로그램의 확장성이다. 각 자료형에 따라 코드를 다르게 구현하면 코드는 훨씬 복잡해지고 내용도 길어지겠지만, 상위클래스에서 공통 부분의 메서드를 제공하고, 하위 클래스에서는 그에 기반한 추가요소만 덧붙여 구현하면 코드 양도 줄어들며, 유지보수도 편리하다.

그럼 다형성을 이용해서 상속에서 다뤘던 VIP 클래스를 구현해보자.

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
package polymorphism;

public class Customer {
    protected int customerID;
    protected String customerName;
    protected String customerGrade;
    int bonusPoint;
    double bonusRatio;

    public Customer(){
        initCustomer();
    }
    public Customer(int customerID, String customerName){
        this.customerID = customerID;
        this.customerName = customerName;
        initCustomer();
    }
    private void initCustomer(){
        customerGrade = "SILVER";
        bonusRatio = 0.01;
    }

    public int calcPrice(int price){
        bonusPoint += price * bonusRatio;
        return price;
    }
    public String showCustomerInfo(){
        return customerName+"님 의 등급은 " + customerGrade+"이며, 보너스 포인트는 "+bonusPoint+"입니다.";
    }
}

먼저 Customer 클래스를 만들어 주었는데, 등급과 보너스 비율을 기본으로 설정하는 메서드를 생성자 두곳에서 모두 사용할 수 있도록 initCustomer() 메서드로 만들어주었다. 그럼 이를 상속하여 VIPCustomer 클래스를 만들어보자.

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
package polymorphism;

public class VIPCustomer extends Customer {
    private int agentID;
    double saleRatio;
    
    public VIPCustomer(int customerID, String customerName, int agentID){
        super(customerID, customerName);
        customerGrade = "VIP";
        bonusRatio = 0.05;
        saleRatio = 0.1;
        this.agentID = agentID;
    }
    public int calcPrice(int price){
        bonusPoint += price * bonusRatio;
        return price - (int)(price * saleRatio);
    }
    public String showCustomerInfo(){
        return super.showCustomerInfo()+ "담당 상담원 번호는" + agentID+"입니다.";
    }
    public int getAgentID(){
        return agentID;
    }
    
}

Customer 클래스를 상속하여 VIPCustomer 클래스를 만들어 주었다. VIPCustomer 클래스만의 멤버변수를 선언해 준뒤, calcPrice()와 showCustomerInfo() 메서드도 오버라이딩 해주었다.

이제 이렇게 생성해준 Customer 클래스와 VIPCustomer 클래스를 사용해서 각각의 인스턴스를 선언해 돈을 지불하고 계산해보면서 다형성에 대해 알아보자.

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
32
33
34
package polymorphism;

public class CustomerTest {
    public static void main(String[] args){
        Customer customerLee = new Customer(10010, "이순신");
        customerLee.bonusPoint = 1000;

        System.out.println(customerLee.showCustomerInfo());

        Customer customerKim = new VIPCustomer(10020,"김유신",12345);
        customerKim.bonusPoint = 1000;

        System.out.println(customerKim.showCustomerInfo());
        System.out.println("====================");

        int price = 10000;
        int leePrice = customerLee.calcPrice(price);
        int kimPrice = customerKim.calcPrice(price);

        System.out.println(customerLee.customerName + "님이" + leePrice+"원 을 지불했습니다.");
        System.out.println(customerLee.showCustomerInfo());
        System.out.println(customerKim.customerName+ "님이" + kimPrice+"원을 지불 하셨습니다.");
        System.out.println(customerKim.showCustomerInfo());
    }
}

>>> 이순신님  등급은 SILVER이며, 보너스 포인트는 1000입니다.
>>> 김유신님  등급은 VIP이며, 보너스 포인트는 1000입니다.담당 상담원 번호는12345입니다.
>>> ====================
>>> 이순신님이10000원  지불했습니다.
>>> 이순신님  등급은 SILVER이며, 보너스 포인트는 1100입니다.
>>> 김유신님이9000원을 지불 하셨습니다.
김유신님  등급은 VIP이며, 보너스 포인트는 1500입니다.담당 상담원 번호는12345입니다.

위에는 인스턴스로 customerLee와 customerKim 두개를 생성해주었는데, customerKim 은 형변환을 하여 Customer형으로 선언해주었다. 고객의 자료형은 Customer형으로 동일하지만, 할인율과 보너스 포인트는 각 인스턴스에 맞게 계산된 것을 알 수 있다.

이처럼 상속관계에 있는 상위 클래스와 하위 클래스는 같은 상위 클래스 자료형으로 선언되어 생성 할 수 있지만, 재정의된 메서드는 각각 호출될뿐만 아니라 이름만 같을 뿐, 서로 다른 역할을 구현하고 있음을 알 수 있다.

다형성 활용하기

앞에서 배운 상속과 다형성을 이용하여 어떻게 이 둘을 활용하여 프로그램을 간편하게 만들 수 있는지 알아보자.
다음과 같은 요구 사항이 발생했다고 하자.

고객이 늘어 VIP 고객만큼 물건을 많이 구매하지는 않지만, 그래도 단골인 분들에게 혜택을 주고싶다. 그래서 고객등급에 GOLD 고객등급을 하나 추가 하려한다. GOLD 회원의 혜택은 다음과같다.

  • 제품을 살 때는 항상 10% 할인해준다.
  • 보너스 포인트를 2% 적립해준다.
  • 담당 전문 상담원은 없다.

이것을 기반으로 Customer 클래스를 상속받아 GOLDCustomer 클래스를 만들어보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package polymorphism;

public class GOLDCustomer extends Customer{
    double saleRatio;

    public GOLDCustomer(int customerID, String customerName){
        super(customerID, customerName);
        customerGrade = "GOLD";
        bonusRatio = 0.02;
        saleRatio = 0.1;
    }
    @Override
    public int calcPrice(int price){
        bonusPoint += price * bonusRatio;
        return price - (int)(price * saleRatio);
    }
}

그럼 다시 CustomerTest 클래스에 main 함수를 구현하여 고객관리 프로그램을 구현해보자.

다음과 같은 상황을 구현해보도록 할 것 이다.

이 회사의 고객은 현재 5명이다. 5명 중 VIP 1명, GOLD 2명, SILVER 2명이다. 이 고객들이 각각 10000원 짜리 상품을 구매했을 때의 결과를 출력해보자.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package polymorphism;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicReferenceArray;

public class CustomerTest {
    public static void main(String[] args){
        ArrayList<Customer> customerList = new ArrayList<Customer>();

        Customer customerLee = new Customer(10010, "이순신");
        Customer customerShin = new Customer(10020, "신사임당");
        Customer customerHong= new GOLDCustomer(10030, "홍길동");
        Customer customerYoul = new GOLDCustomer(10040, "이율곡");
        Customer customerKim = new VIPCustomer(10050, "김유신",12345);

        customerList.add(customerLee);
        customerList.add(customerShin);
        customerList.add(customerHong);
        customerList.add(customerYoul);
        customerList.add(customerKim);

        System.out.println("==== 고객 정보 출력 ====");
        for(Customer c : customerList){
            System.out.println(c.showCustomerInfo());
        }

        System.out.println("==== 할인율과 보너스 포인트 계산 ====");
        int price = 10000;
        for (Customer c : customerList){
            int cost = c.calcPrice(price);
            System.out.println(c.customerName+ "남이 "+cost+"원 지불하셨습니다.");
            System.out.println("현재 남아있는 포인트는"+c.bonusPoint+ "입니다.");
        }
    }
}

>>> 이순신님  등급은 SILVER이며, 보너스 포인트는 0입니다.
>>> 신사임당님  등급은 SILVER이며, 보너스 포인트는 0입니다.
>>> 홍길동님  등급은 GOLD이며, 보너스 포인트는 0입니다.
>>> 이율곡님  등급은 GOLD이며, 보너스 포인트는 0입니다.
>>> 김유신님  등급은 VIP이며, 보너스 포인트는 0입니다.담당 상담원 번호는12345입니다.
>>> ==== 할인율과 보너스 포인트 계산 ====
>>> 이순신님이 10000 지불하셨습니다.
>>> 현재 남아있는 포인트는100입니다.
>>> 신사임당님이 10000 지불하셨습니다.
>>> 현재 남아있는 포인트는100입니다.
>>> 홍길동남이 9000 지불하셨습니다.
>>> 현재 남아있는 포인트는200입니다.
>>> 이율곡님이 9000 지불하셨습니다.
>>> 현재 남아있는 포인트는200입니다.
>>> 김유신님이 9000 지불하셨습니다.
>>> 현재 남아있는 포인트는500입니다.

5명의 고객의 정보를 담기 위해 객체 배열 ArrayList를 선언해주었다. 그리고 Customer 클래스로 5개의 인스턴스를 선언했는데, 이순신, 신사임당은 SILVERCustomer 객체로, 홍길동, 이율곡은 GOLDCustomer, 김유신은 VIPCustomer 객체로 선언하였다. 그리고 for 문을 사용하여 ArrayList에 있는 요소를 하나씩 가져와 c변수에 넣게되어 메서드를 실행하게된다. 그리고 각 인스턴스가 메서드를 호출하면 현재 이 변수의 실제 인스턴스가 무엇이냐에 따라 재정의한 메서드를 각각 호출하여 계산한다. 이것이 다형성이다.

만약 재정의한 메서드가 가상 메서드 방식에 의해 자동으로 호출되지 않는다면 if-else if문을 사용하여 각 자료형에 적합한 코드를 따로 구현해야 할 것이다. 게다가 새로운 등급의 고객이 필요할 때 마다 또 다른 조건을 구현해야 하므로 코드의 유지보수가 어려워진다. 이련경우에 상속과 다형성을 활용하면 복잡한 코드를 간결하게 하고 확장성있는 프로그램을 구현할 수 있게된다.

상속의 사용 조건

이렇게 상속에 대해 알아봤는데, 상속은 코드의 재사용 개념이 아니라는 것을 알아야한다.
예를 들어 과목을 나타내는 Subject 클래스가 있다고 하자. 과목 아이디, 이름을 멤버변수로 가지고 get(), set() 메서드도 제공한다. 이제 Student 클래스를 만들고자 하는데, 모든 학생은 전공 과목을 가지고 있기 때문에, Subject 클래스에서 제공하는 여러 메서드를 활용할 수 있도록 상속 해주면 좋을 것 같다는 생각이 든다.

하지만 이런 경우에는 상속을 사용하지 않는게 좋다. Subject가 Student를 포괄하는 개념의 클래스가 아니기 때문이다. 또한 Student 클래스를 상속받는 다른 클래스가 있을 수도 있기 때문에 상속을 사용하지 않는 것이 좋다. 이런경우를 HAS-A 관계(has a relationship; association) 라고 한다.

이 관계는 한 클래스가 다른 클래스를 소유한 관계로 상속이 아닌 멤버변수로 사용하는 것이 좋다.

1
2
3
class Student(){
    Subject majorSubject;
}

그렇다면 어떤 경우에 상속을 사용해주어야 좋을까?

바로 두 개념이 상위와 하위에 위치한, 일반적인 개념과 구체적인 개념의 관계를 갖고있는 상황에서 사용해주어야 한다. 즉 ‘사람은 포유류이다’ 와 같은 관계이다. 이러한 관계를 IS-A 관계(is a relationship; inheritance) 라고 한다.
상속은 이러한 관계를 갖고있는 개념에서 사용해주어야 올바르고 효율적인 코드를 작성할 수 있다.

카테고리:

업데이트:

댓글남기기