JAVA Basic ) 상속과 다형성_상속, 형 변환
Programming/Java 기초

JAVA Basic ) 상속과 다형성_상속, 형 변환

반응형

 

목차


     

    <상속이란?>

    객체 지향 프로그래밍의 중요한 특징 중 하나가 '상속(inheritance)'이다. 
    B클래스가 A클래스를 상속받으면
    -> B클래스는 A클래스의 멤버 변수와 메서드를 사용할 수 있다.
    객체지향 프로그램은 유지 보수하기 편하고 프로그램을 수정하거나 새로운 내용을 추가하는 것이 유연한데, 그 기반이 되는 기술이 바로 이 '상속'이다.


    <클래스의 상속>

    B클래스가 A클래스에서 상속을 받는다고 할 때 그림으로 다음처럼 나타낼 수 있다.

    우리가 생각하기에 상속을 하는 클래스에서 상속을 받는 클래스로 화살표가 갈 것 같지만,
    클래스 간 상속을 표현할 때는 다음 그림처럼 상속 받는 클래스에서 상속하는 클래스로 화살표가 가므로 햇갈리면 안된다.

    '상위 클래스'를 super class, base clase로,
    '하위 클래스'
    를 subclass, derived class라고 표현하기도 한다.

     

     

    클래스 상속 문법

    자바 문법으로 상속을 구현할 때는 'extends예약어'를 사용한다.
    이때 사용하는 extends예약어는 '연장, 확장하다'의 의미.
    A가 가지고 있는 속성이나 기능을 추가로 확장하여 B클래스를 구현한다는 뜻... 
    따라서 일반적인 A클래스에서 더 구체적인 B클래스가 구현 되는 것.

    class B extends A{	//"B클래스가 A클래스를 상속받는다."
    }

    다음 그림을 보자.

    포유류는 사람보다 더 일반적 개념이다.
    사람 포유류의 특징과 기능을 포함해 더 많거나 다른 특징과 기능을 갖고있다.

    이렇게 상속 관계에서는 상위 클래스가 하위 클래스보다 더 일반적인 개념이고, 하위 클래스는 상위 클래스보다 구체적인 클래스가 된다.

     

    <상속을 사용한 고객 관리 프로그램 구현>

    1. customer 클래스 생성

    (클래스 속성을 멤버 변수(아이디, 이름, 고객 등급, 보너스포인트, 보너스 포인트 적립 비율)를 속성으로 선언)

    package inheritance;
    
    public class Customer {
    	private int customerID;
    	private String customerName;
    	private String customerGrade;
    	int bonusPoint;
    	double bonusRatio;
    	
    	public Customer() {	// 디폴트 생성자
    		customerGrade = "SILVER";	// 실버 등급
    		bonusRatio = 0.01;	// 보너스 적립 비율
    	}
    	public int calcPrice(int price) {	
    		bonusPoint += bonusRatio;	// 보너스 포인트를 계산하는 메서드
    		return price;
    	}
    	public String showCustomerInfo() {	//고객정보를 반환하는 메서드
    		return customerName + "님의 등급은 " + customerGrade + "이며, 보너스 포인트는 " + bonusPoint + "입니다."; 
    	}
    }

    모든 멤버 변수를 반드시 private으로 선언할 필요 없다. 필요에 따라 멤버 변수나 메서드를 외부에 노출하지 않을 목절일 때 private로 선언한다.

    접근 제어자 가시성 표

    2. 새로운 고객 등급이 필요한 경우

    [예제 시나리오]
    고객이 점점 늘어나고 판매도 많아지니 우수 고객에게 보다 좋은 혜택을 주고 싶습니다.
    우수 고객 등급은 VIP이고
    제품을 살 때는 항상 10% 할인
    보너스 포인트를 5% 적립
    담당 전문 상담원 배정

    가장 간단하게 생각해 보면, 이미 Customer클래스가 존재하므로 여기에 VIP고객에게 필요한 변수와 메서드까지 함께 포함시켜 구현하는 방법이 있다.
    그러나 이렇게 구현하면 Customer클래스의 코드가 복잡해지고, 가독성이 굉장히 낮아지게 된다. 게다가 일반 고객의 인스턴스를 생성할 때는 VIP고객과 관련된 기능은 전혀 필요 없는데 VIP고객의 내용까지 같이 생성되어 낭비가 발생한다.

    이럴 땐 다음처럼 VIPCustomer클래스를 따로 생성한다.

    3. VIPCustomer 클래스 생성

    package inheritance;
    
    public class VIPCustomer extends Customer{
    //VIPCustomer 클래스는 Customer를 상속받음
    	// Customer 클래스와 겹치는 멤버 변수 제거
    	private int agentID;	// 담당 전문 상담원
    	double saleRatio;	// 할인율
    	
    	public VIPCustomer() {	// 디폴트 생성자
    		customerGrade = "VIP";	
    		bonusRatio = 0.05;	// 보너스 5% 적립
    		saleRatio = 0.1;	// 할인율 10%
    	}
    	
    	public int getAgentID() {	// VIP고객에게만 필요한 메서드
    		return agentID;
    	}
    	
    	//calcPrice메서드 제거
    	
    	// 고객 정보 반환 메서드 제거
    }

    비교적 간결한 VIPCustomer 클래스

    앞에서 만든 Customer클래스에 이미 선언되어 있는 멤버 변수(id,이름 등)와 메서드(calcPrice(),showInfo())를 제거하고,
    Customer클래스에게서 상속을 시킨다. (extents Customer)

    즉 Customer클래스에 일반 고객의 속성과 기능이 이미 구현되어 있기 때문에, VIPCustomer클래스는 Customer클래스를 상속받고 VIP고객에게 필요한 추가 속성과 기능을 구현.

    그러나, 위 코드는 두 가지 문제점이 있다. 
    1. customerGrade 변수에서 오류가 발생한다.
    상위 클래스(Customer)에 선언된 변수인데 private으로 선언되어 있다. -> 클래스 외부에서는 사용X (Cutsomer 클래스의 멤버 변수를 protected로 선언하자.) 그리고 다른 하위 클래스에서도 사용할 수 있도록 get(),set()메서드를 추가하자.
    2. VIP고객에게 제공하는 혜택인 할인율과 세일 가격을 어떻게 적용할지 구현하지 않았다. (메서드 오버라이딩에서 설명)

    4. Customer 클래스 수정 

    package inheritance;
    
    public class Customer {
    	protected int customerID;	//protected로 변경
    	protected String customerName;
    	protected String customerGrade;	
    	int bonusPoint;
    	double bonusRatio;
    	
    	public Customer() {	// 디폴트 생성자
    		customerGrade = "SILVER";	// 실버 등급
    		bonusRatio = 0.01;	// 보너스 적립 비율
    	}
    	public int calcPrice(int price) {	// 보너스 포인트를 계산하는 메서드
    		bonusPoint += bonusRatio;	
    		return price;
    	}
    	public String showCustomerInfo() {	//고객정보를 반환하는 메서드
    		return customerName + "님의 등급은 " + customerGrade + "이며, 보너스 포인트는 " + bonusPoint + "입니다."; 
    	}
    	public int getCustomerID() {
    		return customerID;
    	}
    	public void setCustomerID(int customerID) {
    		this.customerID = customerID;
    	}
    	public String getCustomerName() {
    		return customerName;
    	}
    	public void setCustomerName(String customerName) {
    		this.customerName = customerName;
    	}
    	public int getBonusPoint() {
    		return bonusPoint;
    	}
    	public void setBonusPoint(int bonusPoint) {
    		this.bonusPoint = bonusPoint;
    	}
    	
    	// get(),set() 메서드 추가
    }

    protected예약어로 선언한 변수는 외부 클래스에는 private 변수처럼 get() 메서드를 사용해서 값을 가져올 수 있고, set() 메서드를 사용해 값을 지정할 수 있다. Customer 클래스를 상속받은 VIPCustomer 클래스는 protected로 선언한 변수를 상속받게 되고, 나머지 public메서드도 상속받아 사용할 수 있다.

     

    5. 테스트 프로그램 실행

    간단한 테스트 프로그램을 만들어 확인해보자.
    [예제 시나리오]
    일반 고객 1명과 VIP고객 1명이 있다.
    일반 고객의 이름은 이순신, 아이디는 10010, 보너스 포인트 1000보유.
    VIP고객의 이름은 김유신, 아이디는 10020, 이 고객의 보너스 포인트는 10000점.

    package inheritance;
    
    public class CustomerTest {
    
    	public static void main(String[] args) {
    		Customer customerLee = new Customer();
    		customerLee.setCustomerName("이순신");
    		customerLee.setCustomerID(10010);
    		//customerID와Name은 protected변수이므로 set()메서드로 호출
    		customerLee.bonusPoint = 1000;
    		System.out.println(customerLee.showCustomerInfo());
    		
    		VIPCustomer customerKim = new VIPCustomer();
    		customerKim.setCustomerName("김유신");
    		customerKim.setCustomerID(10020);
    		//customerID와Name은 protected변수이므로 set()메서드로 호출
    		//Customer를 상속했기 때문에 메서드 사용 가능
    		customerKim.bonusPoint = 10000;
    		System.out.println(customerKim.showCustomerInfo());
    	}
    }

    run:

    새로운 Customer클래스를 생성하고 customerLee 참조 변수에 대입.
    그리고 VIPCustomer클래스를 생성하고 customerKim 참조변수에 대입했다.
    VIPCustomer가 Customer를 상속했기 때문에 고객 아이디, 고객 이름의 메서드를 사용할 수 있다.

     

    <상속에서 클래스 생성과 형 변환>

    하위 클래스가 생성될 때는 상위 클래스의 생성자가 먼저 호출된다.
    상속 관계에서 클래스의 생성 과정을 살펴보면, 하위 클래스의 변수와 메서드를 사용할 수 있는 이유하위 클래스가 상위 클래스의 자료형으로 형 변환을 할 수 있는 이유를 이해할 수 있다.

     

    하위 클래스의 생성과정

    CustomerTest예제를 살펴보면,
    VIPCustomer클래스로 선언한 CustomerKim인스턴스는 상속받은 상위 클래스의 변수를 자기 것처럼 사용할 수 있다.
    변수를 사용할 수 있다는 것은 그 변수를 저장하고 있는 메모리가 존재한다는 뜻.
    그런데 VIPCustomer클래스의 코드를 보면 해당 변수가 존재하지 않는다. Customer클래스를 상속받았을 뿐이다.
    여기서 우리는 상속된 하위 클래스가 생성되는 과정을 다시 생각해 봐야한다. 

    package inheritance;
    
    public class Customer {
    	protected int customerID;
    	protected String customerName;
    	protected String customerGrade;	
    	int bonusPoint;
    	double bonusRatio;
    	
    	public Customer() {	// 디폴트 생성자
    		customerGrade = "SILVER";	// 실버 등급
    		bonusRatio = 0.01;	// 보너스 적립 비율
    		System.out.println("Customer() 생성자호출");	// 상위 클래스 생성할 때 콘솔 출력문
    	}
        ...

    Customer()생성자에 출력문을 넣었습니다. Customer 인스턴스가 생성되면 이 호출문이 출력될 것이다.


    그리고 VIPCustomer()생성자에도 다음과 같이 출력문을 추가한다.

    package inheritance;
    
    public class VIPCustomer extends Customer{
    //VIPCustomer 클래스는 Customer를 상속받음
    	private int agentID;
    	double saleRatio;
    	
    	public VIPCustomer() {
    		customerGrade = "VIP";	
    		bonusRatio = 0.05;	// 보너스 5% 적립
    		saleRatio = 0.1;	// 할인율 10%
    		System.out.println("VIPCustomer() 생성자 호출");
    	}
    ...

    CustomerTest클래스를 실행하여 출력 결과를 보자. (CustomerLee 인스턴스 지우고)

    package inheritance;
    
    public class CustomerTest {
    
    	public static void main(String[] args) {
    		/*Customer customerLee = new Customer();
    		customerLee.setCustomerName("이순신");
    		customerLee.setCustomerID(10010);
    		customerLee.bonusPoint = 1000;
    		System.out.println(customerLee.showCustomerInfo());
    		*/
    		VIPCustomer customerKim = new VIPCustomer();
    		customerKim.setCustomerName("김유신");
    		customerKim.setCustomerID(10020);
    		customerKim.bonusPoint = 10000;
    		System.out.println(customerKim.showCustomerInfo());
    	}
    }

    run:

    출력 화면을 보면 상위 클래스의 Customer()생성자가 먼저 호출되고 그다음 VIP()생성자가 호출되는 것을 볼 수 있다.
    정리하면 상위 클래스를 상속받은 하위 클래스가 생성될 때는 반드시 상위 클래스의 생성자가 먼저 호출, 그리고 상위 클래스 생성자가 호출될 때 상위 클래스의 멤버 변수가 메모리에 생성.

    VIPCustomer가 생성될 때 메모리 구조 그림

    그림처럼 상위 클래스의 변수가 메모리에 먼저 생성되기 때문에 하위 클래스에서도 이 값들을 모두 사용할 수 있다.

     

    <부모를 부르는 예약어, super>

    'super 예약어'는 '하위 클래스에서 상위 클래스로 접근할 때 사용'.
    하위 클래스는 상위 클래스의 주소, 즉 '참조 값'을 알고있다. 이 참조 값을 super예약어가 알고있는 것이다.
    (this가 자기 자신의 참조 값을 알고 있는 것과 같다고 생각하면 됨.)
    또한 super는 상위 클래스의 생성자를 호출하는 데도 쓰임.

     

    상위 클래스 생성자 호출

    CustomerTest 예제를 보면 VIPCustoemr만 생성했는데, Customer 상위 클래스도 생성된 것을 알 수 있었다.
    하위 클래스 생성자만 호출 했는데 상위 클래스 생성자가 호출된 이유는 생성자에서 super()를 '자동'으로 호출하기 때문. 
    super()를 호출하면 상위 클래스의 디폴트 생성자가 호출된다. 하위 클래스의 디폴트 생성자는 바이트 코드로 변환되기 전에 다음과 같이 코드가 자동으로 변경된다.

    	public VIPCustomer() {	// 디폴트 생성자
    		super();	
            	// 컴파일러가 자동으로 추가하는 코드. 상위 클래스의 Customer()가 호출됨.
    		customerGrade = "VIP";	
    		bonusRatio = 0.05;
    		saleRatio = 0.1;
    		System.out.println("VIPCustomer() 생성자 호출");
    	}

     

    super예약어로 매개변수가 있는 생성자 호출

    한 가지 경우를 생각해보자. Customer클래스를 생성할 때 고객 ID와 이름을 반드시 지정해야 한다고 가정하자.
    이런 경우에 set()메서드로 값을 지정하는 것이 아닌, 새로운 생성자를 만들어 매개변수로 값을 전달받아야 한다.
    *** *(Customer customerLee = new Customer("...")) // 이런식으로...
    즉 디폴트 생성자가 아닌 매개변수가 있는 생성자를 직접 구현해야 함.

    다음처럼 Customer클래스에 새로운 생성자를 추가. 기존의 디폴트 생성자는 주석처리하자.

    ...
        public Customer(int customerID, String customerName) {
    		this.customerID = customerID;
    		this.customerName = customerName;
    		customerGrade = "SILVER";
    		bonusRatio = 0.01;
    		System.out.println("Customer(int, String) 생성자 호출");
    	}
    ...

    이렇게 Customer()를 없애고 새로운 생성자를 작성하면 Customer클래스를 상속받은 VIPCustomer클래스에서 오류가 발생한다.
    이 오류는 묵시적으로 호출될 디폴트 생성자 Customer()가 정의되지 않았기 때문에 반드시 명시적으로 다른 생성자를 호출해야 한다.(※하위 클래스가 생성될 때는 상위 클래스의 디폴트 생성자를 호출하는 super()가 자동으로 생성된다.)

    Customer클래스를 새로 생성할 때 고객ID와 고객 이름을 반드시 지정하여 생성하기로 했으니 VIPCustomer클래스를 생성할 때도 이 값이 필요하게 된다.

    기존 VIPCusotmer클래스의 디폴트 생성자도 주석처리하여 수정해보자.
    그리고 VIP고객만을 위한 상담원ID도 함께 지정해보자.

    ...	
        public VIPCustomer(int customerID, String customerName, int agentID) {
    		super(customerID,customerName);	// 상위 클래스 생성자 호출
    		customerGrade = "VIP";
    		bonusRatio = 0.05;
    		saleRatio = 0.1;
    		System.out.println("VIPCustomer(int, String, int) 생성자 호출");
    	}
    ...

    새로운 생성자는 고객ID, 고객 이름, 상담원ID를 매개변수로 받는다. super예약어 super(customerID, customerName); 문장으로 상위 클래스 생성자를 호출한다. 

    super()를 통해 Customer(int customerID, String customerName)상위 클래스 생성자를 호출하고, 코드 순서대로 멤버 변수가 초기화된다. 상위 클래스 호출이 끝나면 VIPCustomer하위 클래스 생성자의 내부 코드 수행이 마무리된다.

     

    [ super() 복습 ]

    1. 하위 클래스가 생성될 때는 상위 클래스의 [ 생성자 ]가 먼저 호출된다.
    2. 상위 클래스에 생성자 코드가 따로 없으면 [ super() ]로 상위 클래스의 디폴트 생성자가 자동으로 호출된다.
    3. 상위 클래스에 디폴트 생성자가 없고 매개변수가 있는 생성자만 있을 경우 [ super() ]에 매개변수를 추가하여, 매개변수가 있는 상위 클래스의 생성자를 직접 호출해야 한다.

     

    상위 클래스의 멤버 변수나 메서드를 참조하는 super

    상위 클래스에 선언한 멤버 변수나 메서드를 하위 클래스에서 참조할 때도 super를 사용. this를 사용하여 자신의 멤버에 접근했던 것과 비슷하다.

    public String showVIPInfo(){
    	return super.showCustomerInfo() + "담당 상담원 아이디는" + agentID + 입니다.";
    }

    super예약어는 상위 클래스의 참조 값을 가지고 있으므로 위 코드처럼 사용하여 고객 정보를 출력하는 showCustomerInfo()를 활용할 수 있다.
    물론 위 코드의 showVIPInfo()메서드에서는 굳이 super.showCustomerInfo()라고 호출하지 않아도 상위 클래스의 메서드가 잘 호출되지만, 나중에 하위 클래스가 상위 클래스와 동일한 이름의 메서드를 구현하는 경우가 있는데, 이러한 경우 super예약어를 사용해야한다.

     

    <상위 클래스로 묵시적 클래스 형 변환>

    상속에서 가장 중요한 관계가 '클래스 간 형 변환'이다.
    Customer와 VIPCustomer의 관계를 생각해보자.
    개념 면에서 보면 상위 클래스인 Customer가 VIPCustomer보다 일반적인 개념이고, 기능 면에서 보면 VIPCustomer가 더 기능이 많다. 왜냐하면 상속받은 클래스는 상위 클래스 기능을 모두 사용하며 추가적인 기능이 더 구현되어있기 때문.

    따라서 VIPCustomer는 VIPCustomer형이면서 동시에 Customer형이기도 한다. 즉 VIPCustomer 클래스로 인스턴스를 생성할 때 이 인스턴스의 자료형을 Customer형으로 클래스 형 변환하여 선언할 수 있다.
    왜냐하면 VIPCustomer클래스는 Customer클래스를 상속받았기 때문이다.
    ※ 클래스형과 클래스 자료형, 인스턴스형과 인스턴스 자료형은 모두 비슷한 의미로 사용하는 용어.
    ※ 이러한 클래스 형 변환을 업캐스팅(upcasting)이라고도 한다.

    	Customer vc = new VIPCustomer();
    선언된 클래스형	생성된 인스턴스의 클래스형
    (상위 클래스형)		(하위 클래스형)

    그럼 반대로 Customer로 인스턴스를 생성할 때 VIPCustomer형으로 선언할 수 있을까?
    놉.
    상위 클래스인 Customer가 VIPCustomer클래스의 기능을 다 가지고 있지 않기 때문이다.

    정리하면, 모든 하위 클래스는 상위 클래스 자료형으로 변환될 수 있지만, 역으로는 성립이 불가능하다.
    따라서 하위 클래스인 VIPCustomer는 상위 클래스Customer형을 내포하기 있기 때문에 Customer형으로 형 변환이 가능한 것이다.

    형 변환된 vc가 가리키는 것은?

    Customer vc = new VIPCustomer();

    에서 형 변환된 vc가 가리키는 것은 무엇일까?

    VIPCustomer가 생성될 때 메모리 구조 그림

    Customer vc = new VIPCustomer(); 문장이 실행되면 VIPCustomer 생성자가 호출되므로 클래스 변수가 위와같이 메모리에서 만들어진다. 그런데 클래스 자료형이 Customer로 한정된다. 클래스 형 변환이 되었을 때는 선언한 클래스형에 기반하여 멤버변수와 메서드에 접근할 수 있다.
    따라서 이 vc 참조변수가 가리킬 수 있는 변수와 메서드는 Customer클래스의 멤버뿐이다.
    이클립스에서 vc변수에서 [ctrl+spacebar]를 눌러보면 다음과 같이 vc 참조변수가 접근할 수 있는 변수와 메서드가 나타난다.

    vc변수는 Customer클래스(상위 클래스)의 멤버 변수와 메서드만 사용이 가능하다.

     

     

    이렇게 클래스 형 변환을 하는 이유는 다음 포스팅(메서드 오버라이딩)에서 다루게 된다.

    여기에선 하위 클래스의 인스턴스가 상위 클래스로 형 변환되는 과정이 묵시적으로 이루어 진다는 것만 이해하면 된다.
    ※ vc 참조변수를 다시 VIPCustomer형으로 변환할 수도 있다. 이것을 '다운캐스팅(downcasting)'이라고 한다.


    [Do it! 자바 프로그래밍 입문] 도서로 공부하며 정리한 글입니다.

    반응형