웹 개발 기초/자바 문법

다형성 활용, 상속을 사용하는 경우, IS-A 관계, HAS-A 관계

sungw00 2022. 12. 7. 20:23
728x90

다형성을 활용하여 프로그램을 작성하게 되면 유지보수할 때 매우 편리하다는 이점이 있다.

배열을 사용하여 여러 하위 클래스 자료형을 상위 클래스 자료형으로 한꺼번에 관리할 수도 있다.

객체 배열 ArrayList 자료형을 지정하여 선언하고, Customer과 GoldCustomer과 VIPCustomer 클래스 모두 상위 클래스인 Customer 자료형으로 지정하고 선언하게 되면 이 배열에는 Customer, GoldCustomer, VIPCustomer를 모두 사용할 수 있다. 

그리고 이 배열에 Customer 하위 클래스의 인스턴스가 추가될 때 모두 Customer 형으로 묵시적 형변환이 된다.

 

그러면 테스트 프로그램을 구현해보자. Customer 클래스와 VIPCustomer 클래스는 전부 이전 예제와 코드가 동일하다.

import java.util.ArrayList;
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 customer : customerList) {
			System.out.println(customer.getCustomerInfo());
		}
		
		System.out.println("====== 할인율과 보너스 포인트 계산 ======");
		int price = 10000;
		for(Customer customer : customerList) {
			int cost = customer.calcPrice(price);
			System.out.println(customer.getCustomerName() + "님이 " + cost + "원 지불하셨습니다.");
		}
		
		System.out.println("====== 계산 후 고객 정보 출력 ======");
		for(Customer customer : customerList) {
			System.out.println(customer.getCustomerInfo());
		}
	}
}
// 출력 결과
====== 고객 정보 출력 ======
이순신 님의 등급은 SILVER이며, 보너스 포인트는 0입니다.
신사임당 님의 등급은 SILVER이며, 보너스 포인트는 0입니다.
홍길동 님의 등급은 SILVER이며, 보너스 포인트는 0입니다.
이율곡 님의 등급은 SILVER이며, 보너스 포인트는 0입니다.
김유신 님의 등급은 VIP이며, 보너스 포인트는 0입니다.
====== 할인율과 보너스 포인트 계산 ======
이순신님이 10000원 지불하셨습니다.
신사임당님이 10000원 지불하셨습니다.
홍길동님이 10000원 지불하셨습니다.
이율곡님이 10000원 지불하셨습니다.
김유신님이 9000원 지불하셨습니다.
====== 계산 후 고객 정보 출력 ======
이순신 님의 등급은 SILVER이며, 보너스 포인트는 100입니다.
신사임당 님의 등급은 SILVER이며, 보너스 포인트는 100입니다.
홍길동 님의 등급은 SILVER이며, 보너스 포인트는 300입니다.
이율곡 님의 등급은 SILVER이며, 보너스 포인트는 300입니다.
김유신 님의 등급은 VIP이며, 보너스 포인트는 500입니다.

상속은 언제 사용할까?

가장 처음 VIP 고객 등급을 추가하게 된 이유는 이미 Customer 클래스가 구현되어 있는데 추가 요구 사항이 생긴 것이다. 

사실 가장 간단하게 프로그램을 구현하고자 한다면, 이미 Customer 클래스가 존재하기 때문에 여기에 추가 내용을 함께 구현할 수 있다.

Customer 클래스에 VIP 고객의 내용도 함께 구현하면 된다. 하지만 추가 기능을 이렇게 구현하면 요구사항이 점점 늘어남에 따라 코드가 굉장히 복잡해 질 것이다.

이유는 일반 고객 등급이 사용하지 않는 속성(상담원 ID, 할인율 등) 뿐만 아니라 VIP 고객만을 위한 서비스 내용까지 추가해야 하기 때문이다. 만약 Customer 클래스에 모든 등급의 내용을 함께 넣어 구현하면 다음과 같이 될 것이다.

if(customerGrade == "VIP") { // 할인 많이 + 적립 많이
}
else if(customerGrade == "GOLD") { // 할인 많이 + 적립 조금
}
else if(customerGrade == "SILVER") { // 적립만
}

고객 등급에 따라 다르게 구현해야 하기 때문에 if-else if-else 문을 사용한다. calcPrice( ) 메서드뿐 아니라 여러 다른 메서드에서도 등급에 따라 다른 구현이 필요하다면 클래스 전체에서 이러한 if-else if-else 문이 많이 사용될 것이다.

바로 이러한 경우에 고객의 등급이 하나라도 추가되거나 삭제되면 유지보수가 매우 복잡해질 것이다.

 

상속을 사용하게 되면 모든 등급에서 공통으로 사용하는 코드 부분은 상위 클래스인 Customer 클래스에 구현하고, 각 등급별 고객의 내용은 각각의 하위 클래스에 구현한다. 또한 새로운 등급의 고객이 추가되더라도 기존의 코드를 거의 수정하지 않고 새로운 클래스를 추가할 수 있다. 따라서 프로그램이 확장성 있고 유지보수하기 좋다.

 

상속은 항상 사용하는 것이 좋을까?

'IS-A 관계(is a relationship; inheritance)'라는 용어가 있다.

IS-A 관계란 일반적인 개념과 구체적인 개념의 관계이다. 즉 '사람은 포유류이다'와 같은 관계이다.

상속은 IS-A 관계에서 사용하는 것이 가장 효율적이다. 일반 클래스를 점차 구체화하는 상황에서 상속을 사용하는 것이다.

상속을 사용하게 되면 많은 장점이 있겠지만, 하위 클래스가 상위 클래스형에 종속되기 때문에 이질적인 클래스 간에는 상속을 사용하지 않는 것이 좋다. 단순히 코드를 재사용할 목적으로 서로 관련이 없는 개념의 클래스들을 상속 관계로 사용하는 것도 좋지 않은 코드 작성법이다.

 

다음과 같은 경우를 생각해보자. 과목을 나타내는 Subject 클래스가 있고, 과목아이디와 이름을 멤버 변수로 가지고 get( ), set( ) 메서드를 제공한다.

public class Subject {
    private int subjectID;
    private int subjectName;
    
    public int getSubejctID() {
        return subjectID;
    }
    
    public void setSubjectID(int subjectID) {
        this.subjectID = subjectID;
    }
    
    public int getSubjectName() {
        return subjectName;
    }
    
    public void setSubjectName() {
        this.subjectName = subjectName;
    }
    
    public void showSubjectInfo() {
        System.out.println(subejctID + "," + subjectName)
    }
}

이제 Student(학생) 클래스를 생성하고자 할 때, 모든 학생은 전공 과목(Subject)을 가지고 있다. 그러므로 Subject 클래스에서 제공하는 여러 메서드를 활용하면 좋을 것 같다고 생각할 수 있다.

이런 경우 다음과 같이 Student 클래스가 Subject 클래스를 상속받으면 되는 것일까?

public Student extends Subject {  }

이런 경우는 상속을 사용하지 않는 것이 좋다. 왜냐면 Subject가 Student를 포괄하는 개념의 클래스가 아니기 때문이다.

또한 Student 클래스를 상속받는 다른 클래스가 있을 수도 있다. 이런 경우에는 'HAS-A 관계(has a relationship; association'로 표현한다.

HAS-A 관계란 한 클래스가 다른 클래스를 소유한 관계이다. Subject는 Student에 포함되어 Student의 멤버 변수로 사용하는 것이 적절하다.

 

상속을 코드 재사용 개념으로 이해하면 안 되는 이유가 여기에 있다.

재사용 할 수 있는 코드가 있다고 해서 무조건 상속을 받는 것은 아니다.

상속을 사용하면 클래스 간의 결합도가 높아지게 되고 상위 클래스의 변화가 하위 클래스에 미치는 영향이 크다.

따라서 상속은 '일반적인 클래스''구체적인(확장되는) 클래스'의 관계에서 구현하는 것이 맞다.

 

* 자바는 한번에 여러 클래스를 상속받는 다중상속이라는 기능을 지원하지 않는다. C++은 다중 상속을 지원하지만, 자바에서 지원하지 않는 이유는 두 개 이상의 상위 클래스에 같은 이름의 메서드가 정의되어 있다면, 다중 상속을 받는 하위 클래스는 어떤 메서드를 상속받을 지 모호해지기 때문이다. 객체 지향에서 다중 상속의 모호성에 대한 예가 다이아몬드 문제이다. 

따라서 extends 예약어 뒤에 오는 클래스는 반드시 한 개여야 한다.

728x90