자바 9. 상속과 다형성, 다운캐스팅, 추상 클래스, 인터페이스 (extend, abstract, implements)

Bluewiz_YSH·2022년 8월 7일
0

1. 상속

자바에서의 상속은 아주 중요한 기능이자 꼭 설명하고 넘어가야하는 개념이다. 상속은 말 그대로 어떤 특정 클래스가 가진 변수나 기능들을 다른 클래스에게 넘겨줌과 동시에 거기서 더 나아가 그 변수와 기능을 더 확장시키는 개념이다.

예를 들어, A라는 클래스와 B라는 클래스가 있다고 가정해보자. A 클래스는 멤버 변수 c와 메소드 d를 가지고 있고 B라는 클래스는 멤버 변수 e와 메소드 f를 가지고 있다. 그런데 나는 B 클래스에서 A 클래스의 멤버 변수와 메소드도 같이 쓰고 싶다. 그럴 경우엔 간단히 extends 키워드로 B클래스가 A 클래스를 '상속 받으면 된다.'

즉, B 클래스 선언문에서 Class B extends Class A로 수정만 해도 Class B는 메인 메소드 안에서 Class A가 가졌던 멤버 변수 c와 메소드 f를 쓸 수 있는것이다.

아래 고객의 보너스 포인트와 할인율 그리고 ID를 관리하는 프로그램의 코드 예시를 한번 보자.

A마트에서 고객 관리 프로그램을 만들고 싶어한다. 일반 고객은 'SILVER' 등급으로 보너스 포인트는 1%, 할인율은 없고 VIP 고객은 'VIP'등급으로 보너스 포인트는 5%, 할인율은 10%로 하고 싶고 전문 상담원을 배정한다고 가정할때 입력해야하는 코드단은 어떻게 되는지 구현해보시오.

public class Customer {
	protected int customerID;
	protected String customerName;
	protected String customerGrade;
	int bonusPoint;
	double bonusRatio;
    
    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 String getCustomerGrade() {
		return CustomerGrade;
        }

	public void setCustomerGrade(String customerGrade) {
		this.customerGrade = customerGrade;
        }
        
	public Customer() {
		cutomerGrade = "SILVER";
		bonusRatio = 0.01;
		}

	public int calcPrice(int price) {
		bonusPoint += price * bonusRatio;
    	return price;
    	}

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

일단 각각의 고객들을 숫자 int형 customerID로 관리하고 이름과 등급은 문자열 String형 customerName과 customerGrade로 두면서 이 셋은 모두 개인정보이기에 외부에서 보여지면 안되기에 접근 제어자 protected으로 선언해 상속받지 않으면 외부에서 함부로 접근하지 못하게 했다. (public 외의 접근 제어자의 종류에 대해선 나중에 후술하겠다) 그리고 보너스 포인트는 숫자 int형 bonusPoint로, 보너스율은 숫자 실수 double형 bonusRatio로 두었다.

그리고 이대로는 고객ID와 고객이름, 고객 등급을 (상속받지 않은 클래스가 아니면) 외부에서 접근하지 못하기 때문에 무언가를 해줘야 하는데,

원래 외부 클래스나 패키지에서 접근하지 못하게 하는 접근 제어자 private이나 protected가 붙은 경우 외부에서 접근이 가능하게 해 변수 값을 대입하거나 반환하게 하는 getter 게터,setter 세터 메소드를 만들어주는게 관례이다. 그래서 만든것이 public으로 선언해 외부에서도 접근이 가능한 getCustomerID/setCustomerID, getCustomerName/setCustomerName, getCustomerGrade/setCustomerGrade 메소드들이다. (이 게터/세터 메소드는 나중에 접근제어자 할때 같이 후술하겠으며 this가 붙은 것들은 클래스의 멤버변수들을 뜻한다. 이 부분도 나중에 this와 super 키워드를 설명할때 후술하겠다)

그 다음 디폴트 생성자로 고객 customer 객체가 생성될때 마다 초기화로 기본 등급은 SILVER로, 보너스율은 기본 0.01, 1%로 되게끔 설정해놓았다.

그런 뒤 메서드는 총 두 개를 해놓았는데, 하나는 가격을 계산하는 calcPrice 메서드, (안에는 매개변수로 주어진 물품들의 총 가격 price에 보너스율 bonusRatio를 곱해서 이것을 bonusPoint에 가산하고 일단 일반 SILVER 등급은 할인율이 없으므로 그대로 리턴값은 주어진 매개변수 price를 다시 반환하도록 구성했다)

그리고 고객의 등급과 보너스 포인트를 보여주는 showCustomerInfo 메서드 (각 개체마다 저장된 값 customerName과 customerGrade, bonusPoint를 문자열과 조합해 그대로 String으로 반환하도록 했다) 이렇게 두 가지가 존재한다.

그런데 여기서 VIP 고객에 대한 클래스를 만들어야 하는데 유심히 보고 생각해보면 VIP 고객 클래스와 일반 고객 Customer 클래스와 내용이 겹치지만, 일부 다른 것이 있다. (할인율 존재 10%, 보너스 포인트 5배 5%, 등급 VIP)

이럴때는 상속 extends 키워드를 사용해 일반 고객 Customer 클래스와 겹치는것은 끌어오고 다른 부분만 달리 입력해 VIP 고객 customer 클래스를 만들면 된다. 이러는 편이 코드의 편의성과 중복성을 방지하고 나중에 관리하는것을 더 쉽게 만들어준다.

VIPCustomer 클래스는 아래와 같다:

public class VIPCustomer extends Customer {
	private int agentID;
	double saleRatio;
    
    public VIPCustomer() {
    	customerGrade = "VIP";
    	bonusRatio = 0.05;
    	saleRatio = 0.1;
    	}
    
    public int getAgentID() {
    	return agentID;
        }
}

멤버 변수 선언 부분부터 보면 VIPCustomer 클래스 선언문 다음에 extends Customer를 적어준것만으로도 Customer 클래스의 멤버 변수 (customerID, customerName, customerGrade, bonusPoint, bonusRatio)를 이용할수 있어서 중복 입력을 안해도 되고 VIPCustomer 클래스에서만 들어가는 전문 상담원 배정 agentID, 할인율 saleRatio만 멤버 변수 선언을 먼저 했다.

그리고 디폴트 생성자에서 등급은 VIP이니 customerGrade는 "VIP"로, 보너스율 bonusRation는 5% 0.05로, 할인율 saleRatio는 10% 0.1로 하게끔 해놓았고 마지막으로 전문 상담원 배정을 알 수 있게 해주는 메서드 getAgentID를 추가해준것이다.

그 외 Customer 클래스에 있는 calcPrice, showCustomerInfo 메서드 또한 여기 VIPCustomer클래스에는 안 적혀져 있지만 extends 키워드로 상속을 받았으므로 객체 생성후 그대로 쓸 수 있다. (이것도 중복 입력을 피한것이다.)

이처럼 상속은 기존 클래스(앞으로 이런 클래스를 부모/상위/super/base/기본 클래스라고 하자)에서 '상속을 받은' (<= 표현은 항상 상속을 받는다고 하자) 다른 클래스 (앞으로 이런 클래스를 자식/하위/sub/derived/파생 클래스라고 하자)를 만들면서 코드의 중복 입력은 피하고 클래스들의 연결성은 높여주면서 동시에 상속으로 파생되는 클래스를 여러개 쉽게 만들수 있는 이로운 상황을 만들어준다.

하지만, 아직 다 끝난게 아니다. 아직 ustomer 부모 클래스와 VIPCustomer 클래스 간 생성자나 형 변환은 어떻게 되는지 등 관계와 VIPcustomer 클래스에서 할인율을 이용해 할인된 가격을 돌려주는 부분을 메소드로 구현하지 못한 부분을 구현해야하는 등 아직 풀어야 할게 많다.먼저 상속 관계에서 클래스 생성과 형 변환에 대해 알아보겠다.

일단, Customer 부모 클래스와 VIPCustomer 자식 클래스 생성자에 다음과 같은 출력문들을 각각 넣어보자.

//Customer 클래스 디폴트 생성자 안 아래 출력문 입력
System.out.println("Customer() 생성자 호출");
//VIPcustomer 클래스 디폴트 생성자 안 아래 출력문 입력
System.out.println("VIPCustomer() 생성자 호출");

위처럼 해놓고 VIPCustomer 클래스 각각 개체를 만들면 결과가 어떻게 나오는지 한번 보자.

public class CustomerTest {

	public static void main (String[] args) {
   
   	VIPCustomer customerKim = new VIPCustomer();
    customerKim.setCustomerID(2022071501);
    customerKim.setCustomerName("김미래");
    customerKim.bonusPoint = 1000;
    System.out.println(customerKim.showCustomerInfo());
    
    }
    
}

결과에서 알수 있다시피 부모 클래스 Customer 클래스를 상속받은 자식 클래스 VIPCustomer 클래스의 객체를 만들게 되면 부모 클래스가 메모리(힙 메모리)에 올라가게 되어 부모 생성자가 호출이 되고 그 다음 자식 클래스가 메모리에 올라가 자식 생성자가 호출이 된다. 이 말은 즉, 우리가 자식 클래스에선 코드를 쓰지않고 당연히 부모 클래스에 있는 변수와 메소드들을 쓸수 있다고 알고 있지만 안보이는 뒤쪽 부분인 프로그램 내부와 메모리에선 부모 클래스 전체를 메모리에 올려놓고 그것을 그런 뒤에 메모리에 올려지는 자식 클래스와 연동시켜 진행되는 부분임을 깨달을수 있다.

여기서 더 깊게 들어가서 어떻게 자식 클래스 객체를 생성할때 부모 클래스 생성자를 호출하는지, 그리고 이에 깊게 관여하는 super 키워드에 대해 다음 코드문을 통해 알아보자.

Public VIPCustomer() {
	super();
    customerGrade = "VIP";
    bonusRatio = 0.05;
    saleRatio = 0.1;
    System.out.println("VIPCustomer() 생성자 호출");

원래 사실은 VIPCustomer 클래스 생성자는 super();라는 코드문을 담고 있다. 이 super라는 키워드는 부모 클래스 자체를 가리키는 단어이며 예약어이다. 이 예약어에 ()를 달면 당연히 디폴트 생성자라는 뜻이 되고 이 디폴트 생성자는 자식 클래스 디폴트 생성자에서 명시적으로 보이지 않더라도 자동적으로 호출된다.

그렇다면, 부모 클래스에서 디폴트 생성자가 아닌 매개변수가 있는 다른 생성자를 선언하면 어떻게 될까? 아래 코드문처럼 Customer 클래스에 원래 있던 디폴트 생성자를 삭제하고 매개변수가 있는 생성자를 선언해보자.

public Customer(int customerID, String customerName) {

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

그러면 VIPCustomer 클래스에서 아래와 같이 에러가 뜰것이다.

에러의 뜻은 Customer 클래스에 디폴트 생성자가 선언, 정의되어있지 않으므로 다른 생성자로 바꿔주어야 한다는 뜻이다.

그래서 VIPCustomer 클래스도 디폴트 생성자를 주석처리하거나 삭제한 뒤 매개변수가 있는 생성자를 아래 코드문과 같이 선언해보자.

public VIPCustomer(int customerID, String customerName, int agentID) {
	super(customerID, customerName);
    customerGrade = "VIP";
    bonusRatio = 0.05;
    saleRatio = 0.1;
    this.agentID = agentID;
    System.out.println("VIPCustomer(int, String, int) 생성자 호출");

그리고 CustomerTest 클래스 main 메소드 안을 다음과 같이 바꿔서 실행해보자.

		VIPCustomer customerKim = new VIPCustomer(22071501, "김미래", 999);
//	    customerKim.setCustomerID(2022071501);
//	    customerKim.setCustomerName("김미래");
	    customerKim.bonusPoint = 1000;
	    System.out.println(customerKim.showCustomerInfo());

위 사진처럼 아까와 같이 생성자 호출이 부모 클래스 먼저 그 다음 자식 클래스 먼저 되는것을 알수 있다.

즉, 여기서 알아야할건 만약 부모 클래스의 생성자를 따로 추가하거나 변경할때 자식 클래스도 이에 맞게 생성자를 따로 추가하거나 변경해줘야 한다는것이다. 그리고 그때 쓸수 있는것이 부모 클래스 자체를 지칭하는 키워드, super이다.

그러면 좀 더 super 키워드를 다르게 사용하는 법에 대해 알아보자. super 키워드는 부모 클래스 자체를 지칭하기 때문에 부모 클래스의 변수나 메소드를 자식 클래스에서 끌어올때도 쓸 수 있다. 한번 아래 코드문과 같이 자식 클래스에서 showVIPInfo 메소드를 작성해보자.

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

위처럼 코드를 입력하면 자식 클래스 메서드 showVIPInfo 안에서도 부모 클래스의 메서드, showCustomerInfo 메서드를 호출하여 사용할수 있다. 물론 Customer 클래스와 VIPCustomer 클래스는 부모-자식 관계이기 때문에 super.를 달지 않아도 showCustomerInfo 메서드를 가져와서 쓸 수 있지만, 이번에는 super 키워드 사용에 있어서 이런 예시도 있다는걸 보여주기 위해 썼다. 또한 바로 후술할 내용이지만 부모 클래스와 동일한 이름, 반환형, 매개변수 등 동일한 메서드메서드를 자식 클래스에서 구현할수 있는데 이를 메서드 오버라이드라고 한다. 이 형 변환까지 마치면 바로 후술할 것이다.

이제 상속관계의 형 변환에서 중요하고 마지막 부분인 자식 클래스에서 부모 클래스로 묵시적 형 변환이 일어나는것에 대해 알아보겠다.

여기까지 보면 알겠지만 자식 클래스는 자식 클래스 본인임과 동시에 부모 클래스에도 호환할수 있는 성격을 지녔다. 개념적으로 보면 부모 클래스가 자식 클래스보다 더 포괄적이고 기능적으로는 자식 클래스가 부모 클래스보다 더 활용적이라고 볼 수 있다. 그래서 자식 클래스에서 만든 객체는 부모 클래스 자료형으로 형 변환될수 있는 여지가 있다. 왜냐하면 자식 클래스는 부모 클래스를 상속'받았기' 때문이다.

Customer (부모 클래스) cc = new VIPCustomer (자식 클래스) ();

그렇다면 반대로 부모 클래스에서 자식 클래스로 형 변환할수 있을까? 답은 할수 없다가 맞다. 왜냐하면 부모 클래스는 자식 클래스처럼 기능이 확장된 클래스가 아니기 때문이다. 단순하게 생각하면 큰 상자와 작은 상자가 있는데 (개념적 크기 부모>자식) 작은 상자(자식 클래스)는 큰 상자(부모 클래스)에 들어갈수 있지만, 큰 상자는 작은 상자에 들어갈수 없다고 보면 된다.

정리하자면 자식/하위 클래스의 객체는 부모/상위 클래스로 형 변환이 가능하지만 그 역은 성립하지 않는다고 보면 된다.

또한, 이렇게 묵시적 형 변환이 일어나면 일반적으로 동일한 메서드가 없다는 가정 하에 원래 자식 클래스에서 사용했던 변수나 메서드는 더이상 사용할수 없다. 이는 이클립스에서 확인이 가능하다.

앞에서 보았던 cc 객체 생성 코드문을 입력해보고 cc.을 입력하고 위에 마우스를 올려 ctrl + spacebar를 눌러보자. 그럼 아래 사진과 같이 쓸수 있는 변수와 메서드 목록이 나온다. (객체 생성 하기전에 Customer와 VIPCustomer 클래스의 디폴트 생성자를 다시 복구 시켜줘야 한다)

보면 Customer 클래스에 선언된 멤버 변수나 메서드밖에 나오질 않는다. 즉, 인스턴스는 VIPCustomer 클래스로 처음에 했지만 마지막에 담긴건 Customer 클래스기에 그런것이다.
(일반적으로 이렇고 다음에 나올 메서드 오버라이딩은 예외로 보면 된다.)

이렇게 형 변환은 끝났고 앞에서 살짝 언급했던, 상속 관계일때 동일한 이름의 메소드를 사용할때 가능한 메소드 오버라이딩에 대해 먼저 설명하겠다.

메소드 오버라이딩은 extends를 이용하거나 그 외 상속관계 그리고 앞으로 배울, implement를 통한 인터페이스 상속관계에서 발생하는데 중요하게 기억해야할것은 상속받은 자식 클래스와 부모 클래스 서로 동일한 메소드가 있다고 할때 자식 클래스의 메소드가 부모 클래스의 메소드를 덮어쓰기(재정의)하는 현상이 바로 메소드 오버라이딩이다. (이때 동일하다고 하는 의미는 반환 자료형, 메서드명, 매개 변수 개수, 매개변수 자료형이 모두 같다는 의미이다. 이 점에서 오버로딩과 다르다.)

위의 Customer 클래스와 VIPCustomer 클래스 사례를 이어서 아래 예시 코드를 보자.

// VIPCustomer 클래스
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;
    }
    
    //메서드 오버라이딩 된 부분 calcPrice (부모 클래스인 Customer 클래스에도 존재)
    //@Override (생략가능)
    //VIP고객이 할인율을 제공받는 부분을 구현함
	public int calcPrice(int price) {
		bonusPoint += price * bonusRatio;
    	return price - (int)(price * saleRatio);
    	}
    
    public int getAgentID() {
    	return agentID;
        }
}

calcPrice 메소드 위에 적힌 주석 부분을 보면 이해가 갈텐데 calcPrice는 부모 클래스인 Customer에도, 자식 클래스인 VIPCustomer 클래스에도 존재하는, 동일한 이름이면서 매개변수 자료형과 개수, 반환 자료형까지 같은 메소드이다. 다만, VIPCustomer 클래스는 VIP 고객에 대한 부분을 구현하는 만큼 위의 사례에서 구현하지 못한 부분인 할인율 10%로 제공을 구현하기 위해 물건 가격을 계산하는 메소드 calcPrice를 자식 클래스를 오버라이드, 재정의해서 마지막 return하는것이 price 그대로가 아닌, price에 price * saleRatio 값을 int로 형 변환한 값을 뺀 나머지를 리턴하는 메소드로 바꾼것이다.(@Override는 이 메소드가 오버라이드 된다는 표시인데 @ 표시는 애노테이션 Annotation 주석 표시이다. 이 표시는 컴파일러에게 따라오는 키워드에 따라 어떤 정보를 전달하는 역할을 한다)

그렇다면, 메소드 오버라이딩을 한 이 상태에서 customer 클래스로 생성한 객체와 VIPcustomer 클래스로 생성한 객체에서 모두 calcPrice를 실행시켰을때 어떤 결과가 나올까?

public class OverridingTest1 {
	public static void main(String [] args) {
    
    Customer customerLee = new Customer(2022071801, "이성찬");
    customerLee.bonusPoint = 1000;
    
    VIPCustomer customerKim = new VIPCustomer(2022071802, "김수하", 12345);
    customerKim.bonusPoint = 10000;
    
    int price = 10000;
	
    System.out.println(customerLee.getCustomerName() + " 님이 지불해야 하는 금액은 " + customerLee.calcPrice(price) + "원입니다.");
    
    System.out.println(customerKim.getCustomerName() + " 님이 지불해야 하는 금액은 " + customerKim.calcPrice(price) + "원입니다.");

위 사진처럼 Customer 클래스로 생성했던 customerLee 이성찬은 calcPrice 메서드를 실행했을때 처음부터 받은 price 값 10000 그대로 반환했고,(return price;) VIPCustomer 클래스로 생성했던 customerKim 김수하는 calcPrice 메서드를 실행했을때 처음 받은 price 값 10000에 price * saleRatio를 뺀 값을 반환하는데, (return price - (int)(price saleRatio);) saleRatio는 객체를 생성할때 초기값으로 0.1로 지정되어 있으므로 10000 - 10000 * 0.1 = 9000을 price 값으로 최종 반환한다.

즉, 자식 클래스에서 부모 클래스와 동일한 메서드를 오버라이드 재정의했을때 자식 클래스에서 객체 생성후 그 메서드를 호출하면 재정의된 자식 클래스의 메서드가 호출된다.

덧붙이자면, 자식 클래스에서 오버라이드된 메서드가 부모 클래스의 원래 메서드를 '가리므로' 이를 가리키는 용어로 오버라이드와 동시에 부모 클래스의 원래 메서드가 '하이딩' 된다고들 말한다.

그렇다면 우리가 앞의 형 변환에서 봤었던, 객체 생성은 자식 클래스 VIPCustomer에서 했지만 마지막 부모 클래스 Customer에 담기는 형 변환에서는 오버라이딩이 어떻게 작용하는지 한번 보자.

public class OverridingTest2
{

	public static void main(String[] args)
	{
		Customer cc = new VIPCustomer(2022071802, "테스트", 56789);
		cc.bonusPoint**텍스트** = 1000;
		
		System.out.println(cc.getCustomerName() + " 님이 지불해야 하는 금액은 " + cc.calcPrice(10000) + "원입니다.");
	}

}

앞에서 보았던 흐름에서는 당연히 Customer 클래스의 원래 메서드, price값을 그대로 반환하는 calcPric 메서드를 호출해서 결과값이 10000원이 나와야할거 같았지만 사실상 그게 아니고 9000원이 나왔다. 이 말은 즉슨, 처음 객체가 생성되었던 VIPCustomer 클래스 안 오버라이드된 calcPrice 메서드를 끌고 온것이다. 이렇게 부모-자식 클래스의 동일한 메서드를 호출할때 마지막에 선언된 클래스형이 아니라 객체 인스턴스가 생성된 클래스형을 기준으로 따르는것을 가상 메서드(Virtual method)라고 한다.

먼저 우리가 앞서 배웠던 메모리 영역, 특히 멤버 변수와 메서드가 저장되는 영역에 대해서 다시 한번 짚어보자. 멤버 변수 자체의 값은 우리가 객체를 생성할때 힙 메모리에 저장된다. 그러나, 메서드는 멤버 변수와 다르게 스택 영역, 메서드 영역이라 불리는 다른 메모리 영역에 저장된다. 여기서 가상 메서드가 작용하는데,

쉽게 말해서 객체를 생성할때마다 그 객체 값이 위치한 메모리 주소가 달리 생성되는것과 같이 메서드 또한 부모 클래스의 원래 메서드와 자식 클래스의 오버라이딩 한 메서드 둘 다 다른 메서드로서 테이블에 존재하면서 서로 다른 주소를 내포하고 있다.

그래서 calcPrice 메서드는 두가지 주소를 가지고 있는데 하나는 Customer 클래스의 원래 메서드를 가리키는 주소, 또 다른 하나는 VIPCustomer 클래스의 재정의된 메서드를 가리키는 주소이다. 여기서 어떤 주소를 가지고 어떤 메서드를 호출할지 결정하는 것은 calcPrice 메서드를 호출하는 객체가 처음에 어떤 클래스로 생성되었는지에 달려있다.

즉, Customer 클래스로 생성된 객체에서 calcPrice 메서드를 호출하면 Customer 클래스에서 선언한 원래 calcPrice 메서드 주소를 참조해 원래 calcPrice 메서드를 불러오고, VIPCustomer 클래스로 생성된 객체에서는 마지막에 그대로 VIPCustomer형이거나 혹은 부모 클래스 Customer로 형 변환이 되든 calcPrice 메소드를 호출하면 생성된 객체의 뿌리인 VIPCustomer 클래스에서 오버라이드 된 calcPrice 메소드를 불러온다.

마지막으로 정리를 위해 아래 코드문을 입력해보고 실행해보자.

public class OverridingTest3
{

	public static void main(String[] args)
	{
		int price = 10000;
		
		Customer customerLee = new Customer(22071801, "이채림"); //Customer 인스턴스 생성
		System.out.println(customerLee.getCustomerName() + " 님이 지불해야하는 금액은 " + customerLee.calcPrice(price) + "원 입니다.");
		
		VIPCustomer customerKim = new VIPCustomer(22071802, "김미래", 200); //VIPCustomer 인스턴스 생성
		System.out.println(customerKim.getCustomerName() + " 님이 지불해야하는 금액은 " + customerKim.calcPrice(price) + "원 입니다.");
		
		Customer cc = new VIPCustomer(22071803, "테스트", 300); //VIPCustomer 인스턴스 객체를 Customer형으로 변환
		System.out.println(cc.getCustomerName() + " 님이 지불해야하는 금액은 " + cc.calcPrice(price) + "원 입니다.");
	}

}

그렇게 처음 Customer형으로 생성한 객체 customerLee 이채림은 calcPrice 호출하면 price 그대로 10000원, 처음 VIPCustomer형으로 객체를 생성후 VIPCustomer형으로 선언한 customerKim 김미래와 부모 클래스 Customer형으로 형 변환한 cc 테스트 둘은 calcPrice 호출하면 재정의된 calcPrice가 호출되어 price에 할인율 saleRatio를 적용한 9000원이 결과값으로 나온다.

즉, 자식 클래스에 재정의된, 부모 클래스와 동일한 메서드가 있고 자식 클래스에서 객체 생성후 부모 클래스로 형 변환을 했더라도 처음 생성된 인스턴스가 자식 클래스이기 때문에 그 메서드를 호출하면 재정의된 메서드 주소를 따라 그것을 호출한다.

2. 다형성

상속의 일반적인 설명은 끝났고 이제 이 상속으로 우리가 뭘 얻을수 있는지 생각해봐야한다. 그게 바로 다형성이다. 말그대로 다형성은 수많은 모양, 상태를 띄는 성질인데 이걸 상속만으로도 구현이 가능하다는것이다. 우리가 여태까지 해온 고객 관리 프로그램을 다시 보면서 생각해보자.

처음에는 일반 고객들을 위한 Customer 클래스만 생성했지만 그 뒤에 VIP 고객을 별도로 취급하기 위한 VIPCustomer 클래스를 추가적으로 만들고, Customer 클래스를 부모 클래스-VIPCustomer 클래스를 자식 클래스로 연결지어 특히 일반 고객이면 가격을 그대로 계산하지만 VIP 고객이면 고객의 물품 가격을 할인해주고 계산하는 calcPrice 메서드는 두 가지의 모양, 상태를 가지도록 만든것이다.

즉, 부모 클래스의 공통된 메서드를 자식 클래스가 받으면서 코드 중복을 줄이고 이를 활용해 메소드에 추가적인 내용이 필요하다면 오버라이드를 하면서 기능적으로 확장도 하면서 마지막엔 자식클래스에서 생성한 객체를 부모 클래스형으로 바꾸면서 오버라이드된 메서드는 유지하되 한가지 클래스형으로 통일하는, 결국 나중에 유지보수도 쉬워지는 결과를 낳는다.

이것이 다형성이고 자바의 핵심 성질이며 앞으로 많이 언급될것이다. 꼭 기억해둬야한다.

그럼, 이제 지금까지 작업해왔던 고객 관리 프로그램의 기본 구조를 완성할 차례이다.

public class Customer {
.
.
.
public Customer()
	{
		customerGrade = "SILVER";
		bonusRatio = 0.01;
		initCustomer(); // 객체 멤버 변수 초기화 메서드
	}
	
	public Customer(int customerID, String customerName){
		this.customerID = customerID;
		this.customerName = customerName;
		customerGrade = "SILVER";
		bonusRatio = 0.01;
		initCustomer(); // 객체 멤버 변수 초기화 메서드
	}
    
    private void initCustomer() { //생성자에서만 사용되기 때문에 private 선언, 생성자로 객체 생성시 멤버 변수 초기화
		customerGrade = "SILVER";
		bonusRatio = 0.01;
	}
.
.
.
}
public class VIPCustomer extends Customer{
.
.
.
public int calcPrice(int price){ //오버라이드 재정의된 calcPrice 메서드
		bonusPoint += price * bonusRatio;
		return price - (int)(price * saleRatio);
	}
	
	public int getAgentID(){
		return agentID;
	}
	
	public String showCustomerInfo() { //showCustomerInfo 오버라이드 추가
		return super.showCustomerInfo() + "담당 상담원의 번호는 " + agentID + " 입니다.";
	    }
.
.
.
}
public class CustomerTest
{

	public static void main(String[] args)
	{
		Customer customerLee = new Customer();
		customerLee.setCustomerID(22071901);
		customerLee.setCustomerName("이기찬");
		customerLee.bonusPoint = 1000;
		
		System.out.println(customerLee.showCustomerInfo());
		
		Customer customerKim = new VIPCustomer(22071902, "김미루", 123);
		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.getCustomerName() + " 님이 " + leePrice + "원 지불하셨습니다.");
		
		System.out.println(customerLee.showCustomerInfo());
		
		System.out.println(customerKim.getCustomerName() + " 님이 " + KimPrice + "원 지불하셨습니다.");
		
		System.out.println(customerKim.showCustomerInfo());
	}

추가적으로 Customer 부모 클래스에 생성자 안에 객체를 생성할때 멤버 변수를 초기화하는 메서드 initCustomer를 추가한것과 나머지 메서드 showCustomerInfo를 오버라이드했다. 마지막으로 CustomerTest 클래스에서 customer 클래스형 인스턴스인 customerLee 객체와 VIPCustomer 클래스로 생성되어 Customer 클래스로 상위 클래스 형 변환된 customerKim 객체 둘다 생성해보고 메소드까지 실행하여 테스트 해봤다. 그게 아래 사진처럼 나온다.

일반 고객을 위한 Customer 클래스도 됐고, VIP 고객 맞춤인 VIPCustomer 클래스도 완성했지만 여기서 새로운 고객 등급인 GOLD를 아래 사항과 함께 프로그램에 추가해달라고 클라이언트로부터 연락이 왔다고 가정해보자:

VIP만큼은 아니지만 단골로서 어느정도 물건을 자주 사가시는 분들을 GOLD 등급으로 분류하고 싶습니다. GOLD 등급은 아래 사항을 혜택으로 받습니다:

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

일단 클라이언트 주문사항에 맞춰서 GOLD 등급을 분류하는 goldCustomer 클래스를 아래와 같이 만들었다:

public class GoldCustomer extends Customer
{ 
	double saleRatio;
	
	public GoldCustomer(int customerID, String customerName) {
		super(customerID, customerName);
		customerGrade = "GOLD"; //고객 등급
		bonusRatio = 0.02; //보너스 포인트 2%
		saleRatio = 0.1; //할인율 10%
	}
	
	public int calcPrice(int price) { //calcPrice 오버라이드 재정의
		bonusPoint += price * bonusRatio;
		return price - (int)(price * saleRatio);
	}

}

만약 이 상태에서 고객 5명을 추가해야하는데 일반 고객 SILVER 2명, 골드 등급 GOLD 2명, VIP 고객 1명이라고 해보고 아래 코드문을 입력하고 결과를 보았다:


public class CustomerTest
{

	public static void main(String[] args)
	{
		Customer customerChang = new Customer(072001, "창자룡");
		Customer customerLim = new Customer(072002, "임해룡");
		Customer customerKim = new GoldCustomer(072003, "김상지");
		Customer customerDae = new GoldCustomer(072004, "대명지");
		Customer customerBae = new VIPCustomer(072005, "배청룡", 500);
		
		System.out.println("=====고객 정보 출력=====");
		System.out.println(customerChang.showCustomerInfo());
		System.out.println(customerLim.showCustomerInfo());
		System.out.println(customerKim.showCustomerInfo());
		System.out.println(customerDae.showCustomerInfo());
		System.out.println(customerBae.showCustomerInfo());
		
		System.out.println("=====할인율과 보너스 포인트 계산=====");
		int price = 10000;
		
		int costChang = customerChang.calcPrice(price);
		int costLim = customerLim.calcPrice(price);
		int costKim = customerKim.calcPrice(price);
		int costDae = customerDae.calcPrice(price);
		int costBae = customerBae.calcPrice(price);
		
		System.out.println(customerChang.getCustomerName() + " 님이 " + costChang + "원 지불하셨습니다.");
		System.out.println(customerLim.getCustomerName() + " 님이 " + costLim + "원 지불하셨습니다.");
		System.out.println(customerKim.getCustomerName() + " 님이 " + costKim + "원 지불하셨습니다.");
		System.out.println(customerDae.getCustomerName() + " 님이 " + costDae + "원 지불하셨습니다.");
		System.out.println(customerBae.getCustomerName() + " 님이 " + costBae + "원 지불하셨습니다.");
		
		System.out.println(customerChang.getCustomerName() + " 님의 현재 보너스 포인트는 " + customerChang.bonusPoint + "점입니다.");
		System.out.println(customerLim.getCustomerName() + " 님의 현재 보너스 포인트는 " + customerLim.bonusPoint + "점입니다.");
		System.out.println(customerKim.getCustomerName() + " 님의 현재 보너스 포인트는 " + customerKim.bonusPoint + "점입니다.");
		System.out.println(customerDae.getCustomerName() + " 님의 현재 보너스 포인트는 " + customerDae.bonusPoint + "점입니다.");
		System.out.println(customerBae.getCustomerName() + " 님의 현재 보너스 포인트는 " + customerBae.bonusPoint + "점입니다.");
	}

}

보면 예상대로 SILVER 일반 고객으로 객체를 만들었던 창자룡, 임해룡 고객들은 보너스 포인트 100, 가격 그대로를 받았고 GOLD 골드 고객으로 객체를 만들었던 김상지, 대명지는 보너스 포인트는 200에 가격은 9000원으로 1000원 할인 받았으며 마지막 VIP 고객으로 객체를 만들었던 배청룡은 보너스 포인트 500에 가격도 9000원으로 할인 받았다.

즉, 실제 인스턴스가 어떤 클래스에서 만들어졌냐에 따라 재정의된 메소드를 실행했을때 맞춰서 다른 결과값을 보여줌을 알 수 있다. 이것이 결국 다형성이다.

그럼 결국 잘만 구현하면 하나의 케이스에서 파생된 다양한 하위 케이스들에 대해 코드 중복도 줄이고 활용성도 높이는 이런 상속은 왜 사용해야하며 어떨때 사용해야할까?

만약 우리가 상속을 모른다거나 프로그램에서 아예 기능이 없다고 가정해보자.

그럼 우리가 이 마트 사례에 적용할수 있는, 여태까지 배운 것들 중에서 유일하게 적용할수 있는 솔루션은 조건문, if-else if-else 구문일것이다. 이걸 Customer 클래스에 적용한다고 가정해보면 아래와 같을것이다:

if (customerGrade == "VIP") {
}
else if (customerGrade == "GOLD") {
}
else if (customerGrade == "SILVER") {
}
...
else { }

언뜻 간단해보이지만 VIP, GOLD, SILVER 등급에 각각 들어가는 코드들은 새로 추가나 수정되는 코드들(기능들)을 새로 써야함은 물론 서로 중복되는것이 많기에 중복되는 코드들을 일일이 적어야 한다.

그리고 Customer 클래스에 다 적게 되므로 코드의 길이는 길어짐은 물론 그러면 코드를 보기 힘든것은 당연하며 후에 수정할 사항이 생기면 유지보수 또한 힘들어질게 분명하다.

이처럼 상속은 조건만 맞다면 코드 관리의 편의성, 유지보수, 활용성면에서 아주 훌륭한 자바의 기능이다.

그렇다면 상속을 쓸 수 있는 조건은 어떤걸까?

그건 바로 IS-A 관계 (IS A RELATIONSHIP, INHERITANCE)에서 상속을 쓸수 있다. 예를 들면, '인간은 포유류다.', '인간은 동물이다.'처럼 일반적인 개념과 구체적인 개념간의 관계인 상태에서 상속을 쓸수 있는데, 이는 우리가 여태 배워왔던것처럼 상속이 일반적/포괄적인 개념을 의미하는 상위 클래스를 구체적/좁은 개념을 의미하는 하위 클래스가 '상속 받는'것이 자바의 상속이기 때문이다.

그래서 단순히 코드의 재사용을 위해서, 혹은 한 클래스가 다른 클래스를 소유하는 HAS-A 관계 (HAS A RELATIONSHIP, ASSOCIATION 예를 들면 자동차안에 수많은 기능이 있는데 핸들 조향장치, 가속/브레이크 장치, 라디오, 네비게이션 등 이런 요소들과 자동차는 자동차가 이 요소들로 구체화되는게 아니라 이 요소들을 소유하는 관계로서 묶여 있다.)에서는 상속을 써서는 안된다.

(HAS-A 관계에서는 '소유하는' 클래스에서 '소유 당하는'클래스를 멤버 변수로 사용하면 된다)

그리고 자바에서는 다중 상속을 지원하지 않기에 하위 클래스당 extends는 단 한번만 올수 있다.

3. 다운캐스팅

다운캐스팅(down casting)은 '상속 받은' 하위 클래스의 객체가 상위 클래스형으로 형 변환되고나서 (업 캐스팅, up casting) 보니 사용해야하는 변수나 메서드가 하위 클래스에 존재한 상황에서 해당 객체를 강제로 하위 클래스로 형 변환 하는것을 뜻한다.

하지만 상위 클래스형 객체에 대해 무턱대고 다운 캐스팅을 쓰면 제대로 된 결과값을 얻어낼수 없기에 해당 객체가 처음 생성되었을때 어떤 하위 클래스를 기반으로 생성했는지 알고서 맞는 하위 클래스로 다운 캐스팅을 해줘야 한다. 이를 도와주는것이 자바에서는 instanceof 키워드이다.

아래 코드문을 입력해보고 분석해보자:

class Animal{ //상위 클래스 animal
	public void move() {
    System.out.println("동물이 움직입니다.");
    }
}

class Human extends Animal { //하위 클래스 ① human
	public void move() {
    	System.out.println("사람이 두 발로 걷습니다.");
	}
    
    public void readBook() { 
    	System.out.println("사람이 책을 읽습니다.");
	}
}

class Tiger extends Animal { //하위 클래스 ② Tiger
	public void move() {
    	System.out.println("호랑이가 네 발로 뜁니다.");
	}
    
    public void hunting() {
    	System.out.println("호랑이가 사냥을 합니다.");
    }
}

class Eagle extends Animal { //하위 클래스 ③ Eagle
	public void move() {
    	System.out.println("독수리가 하늘을 납니다.");
    }
    
    public void flying() {
    	System.out.println("독수리가 날개를 쭉 펴고 멀리 날아갑니다.");
    }
}

public class AnimalTest {

	public static void main(String[] args) {
    
		Animal a1 = new Human(); //하위 클래스에서 객체 생성후 상위 클래스로 형 변환
    	Animal a2 = new Tiger();
    	Animal a3 = new Eagle();
    
    	a1.move(); //오버라이드된 메서드 호출
    	a2.move();
    	a3.move();
    
    	System.out.println("=========원래 형으로 다운 캐스팅=========");
    	
        AnimalTest aniT = new AnimalTest();
        aniT.testDownCasting(a1); //다운캐스팅 & 하위 클래스만 가지고 있는 메서드 실행
        aniT.testDownCasting(a2);
        aniT.testDownCasting(a3);
        
    }
    
    public void testDownCasting(Animal ani) {
    
    	if(ani instanceof Human) { //Human 하위 클래스의 경우 실행
        	Human h = (Human)ani;
            h.readBook();
    	}
        else if(ani instanceof Tiger) { //Tiger 하위 클래스의 경우 실행
        	Tiger t = (Tiger)ani;
            t.hunting();
        }
        else if(ani instanceof Eagle) { //Eagle 하위 클래스의 경우 실행
        	Eagle e = (Eagle)ani;
            e.flying();
        }
        else { //하위 클래스들중 어떠한 것에도 속하지 않은 경우 실행
        	System.out.println("지원되지 않는 형입니다.");
        }
	}
}

그럼 위 사진과 같이 결과가 나오는데 처음 Animal 형으로 형 변환후에는 각 하위 클래스에서 따로 제공하는 readBook(), hunting(), flying() 메서드를 사용할수 없기에 사용하려면 각각의 하위클래스별로 다운캐스팅을 해줘야 한다. 그래서 그 실행 내용을 testDownCasting 메서드에 구현하고 더불어 메서드 실행까지 덧붙여 구현했다. 그렇게 모든걸 갖추고 실행해보면 정상적으로 각 객체마다 생성의 근본이었던 하위 클래스별로 다운캐스팅되어 메서드 호출까지 되는것을 알 수 있다.

4. 추상 클래스

우리가 지금까지 보고 입력하고 구현한 클래스는 concrete class, 즉 구체적인 클래스였는데 이거의 반대 클래스는 이제 우리가 배우려고 하는 abstract class, 즉 추상 클래스다.

추상 클래스는 항상 추상 메소드를 1개 이상 보유해야하는데 추상 메소드는 abstract 키워드가 앞에 붙은 메소드를 뜻하며 추상 메소드는 중괄호 {}가 없는, 구현부 implementation이 없는 특징을 가지고 있다. 아래의 모습과 같다:

abstract int calc (int x, int y); //int calc (int x, int y) {}와는 다름, 구현부 { } 가 있기에 일반 메서드임 다만, 코드 구현부가 없을뿐.

즉, 추상 메서드는 단순히 선언만 한 메서드라고 할 수 있다.

일단 아래 예시를 보고 추상 클래스와 추상 메서드를 이용해 코드를 어떻게 짤건지 생각해보자:

자동차 매장에서 고객이 자동차를 고를때 옵션 세트 안에서 자유롭게 고를수 있는 프로그램 개발을 요청받았다. 추가 옵션에는 필수 옵션, 고급 옵션, VIP 옵션 이렇게 나뉘어져 있는데 필수 옵션에는 파워 스티어링, 내비게이션, 블랙박스, 측후방 카메라를 선택할수 있고 고급 옵션에는 필수 옵션 모두 포함에 향상된 오디오 시스템, 크루즈 컨트롤, 자동 주차 기능을 추가적으로 고를수 있는 옵션이며 마지막 VIP 옵션에는 고급 옵션 모두 포함에 향상된 오디오 시스템, 가죽 열선 시트, 고급 휠, 썬루프를 추가할수 있는 옵션이다.

먼저 생각난건 자동차 옵션에 대한 클래스, CarOptions를 만들고 안의 내용은 옵션 내용인데 일단 필수 옵션/고급 옵션/VIP옵션 세 가지로 나눈 다음에 이걸 추상 메소드화시키면 CarOptions가 추상 클래스로 될 것이다. 그리고 옵션을 고른 각각의 고객 클래스를 만들고 고객 클래스에서 CarOptions 추상 클래스를 상속받아 상황에 맞춰서 자유롭게 구현하면 완성이 된다.

만약 고객 A가 필수 옵션 모두 포함에 고급 옵션에서 향상된 오디오 시스템과 자동 주차 기능은 추가하고 크루즈 컨트롤만 추가 안한다고 했을때, 위에서 설명한 코드 구조와 맞추어서 코드를 짜보면 어떻게 나올까? 한번 아래 코드문을 입력해보고 실행해보자:

public abstract class CarOptions //추상 클래스인 CarOptions 클래스
{
	public abstract void mustHaveOptions(); //① 필수 옵션 추상 메서드
	
	public abstract void luxuryOptions(); //② 고급 옵션 추상 메서드
	
	public abstract void VIPOptions(); //③ VIP 옵션 추상 메서드

}

일단 추상 클래스와 메서드로 구현하기로 한 CarOptions와 그 안에 들어가는 필수 옵션/고급 옵션/VIP 옵션은 위와 같다. 만약 abstract 키워드를 메서드명 앞쪽에 넣지 않거나, 클래스명 선언부에 넣지 않거나 하면 아래와 같이 에러와 함께 에러에 마우스 오버하면 abstract 키워드를 넣으라고 알림창이 뜬다.

추상 클래스와 추상 메서드가 준비되었으니 그럼 실제로 이 추상 클래스와 추상 메서드를 구현하기 위한 concrete class, 즉 고객 A가 고른 옵션들에 관한 일반 클래스를 아래와 같이 만들면 된다:

public class Car_CustomerA_Options extends CarOptions //추상 클래스 상속
{

	public static void main(String[] args)
	{
		Car_CustomerA_Options cca = new Car_CustomerA_Options(); //메서드 실행 위해 객체 생성
        cca.mustHaveOptions(); //필수 옵션 메서드 실행
		cca.luxuryOptions(); //고급 옵션 메서드 실행
		cca.VIPOptions(); //VIP 옵션 메서드 실행
	}

	@Override
	public void mustHaveOptions() //오버라이드로 구체적으로 필수 옵션 구현
	{
		System.out.println("파워 스티어링 추가함");
		
		System.out.println("내비게이션 추가함");
		
		System.out.println("블랙박스 추가함");
		
		System.out.println("측후방 카메라 추가함");
	}

	@Override
	public void luxuryOptions() //오버라이드로 구체적으로 고급 옵션 구현
	{
		mustHaveOptions();
		
		System.out.println("필수 옵션 모두 포함");
		
		System.out.println("향상된 오디오 시스템 추가 안함");
		
		System.out.println("크루즈 컨트롤 추가 안함");
		
		System.out.println("자동 주차 기능 추가함");
	}

	@Override
	public void VIPOptions() ////오버라이드로 구체적으로 VIP 옵션 구현
	{
		System.out.println("VIPOptions 선택 안함");
	}

}

그러면 위와 같이 필수 옵션 모두 포함에 크루즈 컨트롤만 뺀 고급옵션이 선택되었음을 알 수 있고, VIPOptions은 아예 선택을 안했기에 안했다고 결과가 나옴을 알 수 있다.

이처럼, 추상 클래스 CarOptions를 상속 받은 고객 A 선택 옵션 클래스 Car_CustomerA_Options는 CarOptions안에서 추상 메서드로 지정된 세 개의 메서드, 즉 mustHaveOptions, luxuryOptions, VIPOptions를 모두 오버라이드해 구현했다.

이렇게 상속 관계에서 추상 클래스 안 추상 메서드를 똑같은 이름으로 모두 구현하지 않으면 아래 사진처럼 모두 구현하라고 내용이 담긴 알림창을 띄워주는 에러가 난다.

그런데, 왜 추상 클래스는 객체 생성을 상속을 통해서 하는 걸까? 그건 아래 사진 알림창처럼 추상 클래스만으로는 객체 생성이 되지 않기 때문이다.

생각해보면 당연한 이야기이다, 만약 추상 클래스에서 객체를 만들었다고 가정했을때 객체에서 추상 메서드를 호출하면 어떤 결과가 나올까? 추상 메서드는 구현부가 없기 때문에 수행할수 있는 내용이 아예 없다. 그렇기에 인스턴스 객체를 생성해도 쓸모도 없고 아예 안된다고 보는게 맞다. (다만 하위 클래스에서 만든 객체를 추상 클래스인 상위 클래스로 형 변환은 가능하다)

추상 클래스는 구체적으로 구현된 하위 클래스를 만들기 위해 '상속을 하기 위한' 클래스라고 보면 된다. 다만 안의 메서드는 두 가지로 나뉘는데 하나는 하위 클래스에 가서도 똑같이 사용될 메서드는 추상 클래스에서 abstract 키워드를 달지 않고 중괄호 body implementation으로 구현하는 경우와 또 다른 하나는 하위 클래스에서 다르게 사용되기 때문에 내용을 바꿔야 하는 메서드는 추상 클래스에서 abstract 키워드를 달고 중괄호 body implementation가 없이 선언되는 경우로 나뉠것이다.

예를 들자면, 앞서 말한 자동차 옵션 상황에 대해서 만약 필수 옵션 세트 안 모든 옵션이 기본으로 제공되는 옵션들이고 그 외 옵션들은 고급 옵션과 VIP 옵션으로 바뀐다고 새롭게 가정하면

필수 옵션 메서드는 추상 클래스 CarOptions를 상속받는 하위 클래스에서도 똑같이 사용할거기에 미리 CarOptions에서 구현하고, 나머지 고급 옵션/VIP 옵션은 상황에 따라서 자유롭게 구현하도록 둘다 메서드는 추상 메서드로 선언하면 되는것이다.

이런 추상 메서드와 추상 클래스를 제대로 활용하여 앞서 설명한 다형성을 살리는 방법은 템플릿 메서드와 함께 사용하는것이다.

템플릿 메서드(template method)란 템플릿의 뜻이 틀, 견본이라는 의미처럼 메서드의 실행 순서 및 시나리오를 설정하는 메서드이다. 이 메서드는 작동 순서가 변경되면 안되기에 변하지 않는다는 의미가 담긴 final 키워드를 같이 사용하며 이 final 키워드를 쓰면 하위클래스에서 메서드를 재정의(Override)할수 없다. (final 키워드는 뒤에서 자세히 설명하겠다.)

그럼, 아래 예시 코드를 입력해보고 실행해보자:

public abstract class Job_Practice
{
	public void beforePrepareWork()
	{
		System.out.println("일어나서 씻고 아침먹고 옷 갈아입어서 출근할 준비를 합니다.");
	}
	
	public void afterWork()
	{
		System.out.println("퇴근해서 씻고 내일 출근을 위해서 일찍 잡니다.");
	}

	public abstract void commuting();
	
	public abstract void working();
	
	final public void living_a_day()
	{
		beforePrepareWork();
		
		commuting();
		
		working();
		
		afterWork();ㅇ
	}
}
public class White_Color extends Job_Practice
{

	public static void main(String[] args)
	{
		Job_Practice wc = new White_Color();
		wc.living_a_day();
	}

	@Override
	public void commuting()
	{
		System.out.println("대중교통 버스 지하철을 이용해 출근합니다.");
	}

	@Override
	public void working()
	{
		System.out.println("사무실에 앉아 열심히 서류 작업을 합니다.");
	}

}
public class Blue_Color extends Job_Practice
{

	public static void main(String[] args)
	{
		Job_Practice bc = new Blue_Color();
		bc.living_a_day();
	}

	@Override
	public void commuting()
	{
		System.out.println("차에 공구함과 재료들을 잔뜩 싣고 차를 운전하며 출근합니다.");
		
	}

	@Override
	public void working()
	{
		System.out.println("현장에서 공구들을 직접 다루면서 열심히 일합니다.");
		
	}

}

White_Color 클래스 실행 결과

Blue_Color 클래스 실행 결과

주목할만한건 Job_Practice 추상 클래스에서 final로 선언한 템플릿 메서드 living_a_day이다. 이 메서드에서는 출근 내용을 출근 준비 - 출근 - 일하기 - 퇴근 순서로 나누어 이것을 beforePrpareWork - commuting - working - afterWork 메서드 순으로 지정해 실행하도록 해놓았다.

여기서 사무직(White_Color)와 현장직(Blue_Color)이냐에 따라 출근 commuting 일하기 working 메소드 내용이 달라지므로 이 둘은 추상 메소드로 선언,

출근 준비 beforePrepareWork와 퇴근 afterWork는 둘 다 똑같으므로 추상 클래스 Job_Practice에서 둘 다 똑같이 구현했다.

마지막으로 사무직(White_Color) 클래스와 현장직(Blue_Color) 클래스는 Job_Practice 추상 클래스를 상속 받고 달라지는 commuting, working 메소드만 재정의 오버라이드해 구현한 뒤

안에서 객체 생성후 상위 클래스 Job_Practice로 형 변환 한 뒤 템플릿 메서드 living_a_day를 호출하면 위처럼 결과가 나온다.

이처럼, 추상 클래스와 추상 메서드는 템플릿 메서드와 같이 활용하여 쓰면서 객체를 만들고 상위 추상 클래스로 형 변환하고 메소드를 호출하면 상위 추상 클래스 자료형만으로도 하위 클래스의 메소드를 호출할 뿐만 아니라 실행 순서까지 로직을 구현할 수 있는 결과를 가져온다. 즉, 하나의 코드형으로 여러 결과를 도출할 수 있을 뿐 아니라 순서까지 구현하는 다형성 + 절차성을 제대로 구현한다고 보면 된다.

+) final 예약어 (키워드)

final은 변수, 메서드, 클래스에 올수 있는데 변수에 오면 상수를 의미하고 메서드에 오면 하위 클래스에서 재정의 할수 없는 메서드를 의미하며 클래스에 오면 상속할 수 없는 클래스를 의미한다.

final로 지정하는 변수는 상수가 되는 경우는 보통 아래와 같다:

final int NUM = 4; //다른 변수와 차이를 주기 위해 final 선언되는 상수의 변수는 모두 영어 대문자로 적어준다

final로 선언된 메서드는 위의 템플릿 메서드처럼 재정의가 안되야 하는 메서드를 써야할때 쓰는 경우이고, final로 선언된 클래스는 상속을 해도 변하지 않아야 하는 내용이 담긴 클래스들, 보통 보안과 관련이 있거나 프로그램의 뿌리가 되는 기반 클래스의 경우 쓴다.

우리가 지금 기본적으로 쓰는 이클립스의 개발 기본 환경 jdk에서 기본 패키지로 제공되는 몇몇 클래스 또한 final로 선언되어있다. (String, Integer 클래스)

5. 인터페이스

인터페이스는 앞서 설명했던 추상 클래스와 유사해보이지만 다르다. 유사한 점은 추상 메서드를 쓴다는 점과 인스턴스 객체를 만들수 없다는 점이지만 다른 점은 abstract 키워드 선언을 따로 하지 않고 메서드는 오로지 public abstract 추상 메서드를 쓸 수 있을뿐이며 인터페이스에 선언되는 변수는 값을 바꿀수 없는 정적 상수(리터럴)로 지정된다는 점이다.(따로 public static final을 써주지 않아도 말이다.)

바로 아래와 같이 말이다:

public interface Interface_ex {
	int A1 = 3;
    double A2 = 2.5;
    
    int sum(n1, n2);
    String welcome(name);
    
    }

(인터페이스를 생성하는 법은 왼쪽 package explorer 에서 해당 패키지 오른쪽 마우스 클릭후 New-file-interface로 생성 가능하다.)

자, 한번 인터페이스에 쓸만한 예시를 들어보자(위의 상속-다형성 A마트의 소스 코드 일부 이용):

A 마트에서 연말에 잠깐 하는, 한시적인 행사를 기획하는데 새로 고객 등록한 사람들에 한하여 11월부터 12월까지 건수 당 구매한 물품들의 총 금액의 15%를 포인트로 적립하고 나중에 쓸 수 있는 10프로 할인권을 고객당 2장씩 지급하며 건수당 구매한 물품들의 총 금액의 수준에 따라 경품을 건수마다 지급하기로 하였다. (구매액 5만원 이상 경품갯수는 10개, 3만원 이상은 30개, 2만원 이상은 100개로 정했다) 경품은 11월에 한번, 12월에 한번 바뀐다. 이를 다루는 관리 프로그램을 구현해보시오.

위 같은 경우에 대해 구현하려면 먼저, 연말에 구입한 총 물품들에 대한 포인트 15%를 계산하고 저장하여 적립하고 할인권을 받는 부분이 따로 필요하겠고 구매한 물품들의 총 금액에 따라 경품을 달리 지급하는 부분 (+달마다 경품이 바뀌는 상황까지)이 또 필요한 상황이다. 하지만, 위에서 보다시피 A마트에 관한 클래스들은 전부 관련이 있는 클래스간 상속 관계로 이어져있다.

이를 유지하면서 또 새로운 내용을 추가하기엔 과정이 번거로워지므로 새롭게 추가되는 내용들을 따로 정리해 기존 클래스와 새로 연결을 해줘야 하는데, 이런 상속 관계를 유지하면서도 새로운 내용을 자유로이 추가할수 있는게 인터페이스의 역할이다.

즉, 인터페이스는 기존 상속관계에 영향을 주지 않는다. 일단 경품 부분을 제외한 위 내용의 대부분을 아래 코드로 구현한 코드문을 한번 보자:

import java.util.ArrayList;

public class Customer_EndYear extends Customer
{
	private static ArrayList<Customer_EndYear> c_List = new ArrayList<Customer_EndYear> (); //고객 정보를 담은 객체들을 리스트화 하는 ArrayList c_List
    
    int count_cey; //할인권 중복을 막기 위해 할인권 발급수를 카운트하는 멤버 변수
	
	public static void main(String[] args)
	{
		Customer_EndYear ce1 = new Customer_EndYear(2001, "이기참");
		Customer_EndYear ce2 = new Customer_EndYear(2002, "진사이");
		
		Customer_EndYear.addCustomer_EndYear(ce1);
		Customer_EndYear.addCustomer_EndYear(ce2);
		
		Customer_EndYear.getDiscountCoupon();
		
		Customer_EndYear.showAllCustomer_EndYear();
	}
	
	public Customer_EndYear() {
		customerGrade = "ENDYEAR";
		bonusRatio = 0.15;
	}
	
	public Customer_EndYear(int customerID, String customerName){
		this.customerID = customerID;
		this.customerName = customerName;
		customerGrade = "ENDYEAR";
		bonusRatio = 0.15;
	}
	
	public static void addCustomer_EndYear(Customer_EndYear ce) {
		c_List.add(ce);
	}
	
	public static void getDiscountCoupon() {
		for (Customer_EndYear cey : c_List) {
			
			if(cey.count_cey == 1) {
				System.out.println("할인권이 이미 발급되었습니다. 기발급된 할인권을 확인해주세요.");
			}
			else {
				System.out.println("10% 할인권 2장이 발급되었습니다.");
				cey.count_cey += 1;
			}
		}
	}
	
	public static void showAllCustomer_EndYear() {
		for (Customer_EndYear ce : c_List) {
			System.out.println(ce);
		}
		System.out.println();
	}
	
	@Override
	public String toString() {
		return "고객 번호: " + customerID + ", 고객 이름: " + customerName;
	}
}

일단 이번 사례에 대한 내용을 클래스 Customer_EndYear 클래스에 구현하되, Customer 클래스를 extends 상속 받고 연말에 새로 가입한 고객들을 따로 객체로 받기 위해 생성자 또한 따로 기술하였다. 그리고 고객들에 대한 정보가 객체로 들어가기에 이 객체들을 한꺼번에 관리하기 위해 나중에 배울, 많이 쓰는 컬렉션 프레임워크들 중 하나인 ArrayList로 Customer_EndYear 클래스 자료형 c_List 선언했다. (ArrayList는 java.util 패키지의 클래스이며 일단 클래스형 이상을 자료형으로 하는 배열이라고 생각하면 된다. 나중에 후술하겠다.)

또한, 이 안에 고객에 대한 객체를 넣는 addCustomer_EndYear 메소드(ArrayList 클래스의 add 메소드 이용)를 구현하고 그렇게 나온 c_List를 향상된 for 반복문으로 돌려서 미리 선언한 int형 count를 기준으로 10% 할인권 2장 발급을 할건지 말건지 결정하는 getDiscountCoupon 메소드도 구현했다. 추가로 toString 메소드를 재정의하고 showAllCustomer_EndYear 메소드를 구현한것 서로 합쳐 고객 아이디와 고객 이름이 서로 나오게끔까지 구현했다.

여기까지의 내용이 지금 위 사례에서 11월 12월 달마다 바뀌는 경품 부분을 뺀 나머지를 구현한 모습이다. 이제 달마다 바뀌는 경품 부분은 인터페이스로 구현할 예정이다. 바로 아래 처럼 말이다:

//11월 경품 부분

public interface Nov_CustomerGift
{
	int Nov_count_p1 = 10; //5만원 이상 10명 한정
	int Nov_count_p2 = 30; //3만원 이상 10명 한정
	int Nov_count_p3 = 100; //2만원 이상 10명 한정
	
	public void Nov_givePresents(int tot_price); //11월 경품 지급 메소드
}
//12월 경품 부분

public interface Dec_CustomerGift
{
	int Dec_count_p1 = 10; //5만원 이상 10명 한정
	int Dec_count_p2 = 30; //3만원 이상 10명 한정
	int Dec_count_p3 = 100; //2만원 이상 10명 한정
	
	public void Dec_givePresents(int tot_price); //12월 경품 지급 메소드
}
//11월 경품 부분 + 12월 경품 부분 + 고객 김종서 추가

import java.util.ArrayList;

public class Customer_EndYear extends Customer implements Nov_CustomerGift, Dec_CustomerGift
{
	private static ArrayList<Customer_EndYear> c_List = new ArrayList<Customer_EndYear> (); //고객 정보를 담은 객체들을 리스트화 하는 ArrayList c_List
	
	int count_cey; //할인권 중복을 막기 위해 할인권 발급수를 카운트하는 멤버 변수
	
	int Customer_EndYear_sum; //건수당 고객 총 구매금액 저장 변수
	
    //11월 경품 지급후 남은 경품개수를 카운트하는 멤버 변수
	static int curNovCount_p1; 
	static int curNovCount_p2;
	static int curNovCount_p3; 
	
    //12월 경품 지급후 남은 경품개수를 카운트하는 멤버 변수
	static int curDecCount_p1; 
	static int curDecCount_p2;
	static int curDecCount_p3;
	
	public static void main(String[] args)
	{
		Customer_EndYear ce1 = new Customer_EndYear(2001, "이기참");
		Customer_EndYear ce2 = new Customer_EndYear(2002, "진사이");
		Customer_EndYear ce3 = new Customer_EndYear(2003, "김종서");
		
		Customer_EndYear.addCustomer_EndYear(ce1);
		Customer_EndYear.addCustomer_EndYear(ce2);
		Customer_EndYear.addCustomer_EndYear(ce3);
		
		Customer_EndYear.getDiscountCoupon();
		
		Customer_EndYear.showAllCustomer_EndYear();
		
		ce1.Nov_givePresents(55000);
		ce2.Nov_givePresents(35000);
		ce3.Nov_givePresents(25000);
		
		System.out.println("현재 5만원 경품의 남은 갯수는 " + curNovCount_p1 + "개 입니다.");
		System.out.println("현재 3만원 경품의 남은 갯수는 " + curNovCount_p2 + "개 입니다.");
		System.out.println("현재 2만원 경품의 남은 갯수는 " + curNovCount_p3 + "개 입니다.");
		
	}
	
	public Customer_EndYear() {
		customerGrade = "ENDYEAR";
		bonusRatio = 0.15;
	}
	
	public Customer_EndYear(int customerID, String customerName){
		this.customerID = customerID;
		this.customerName = customerName;
		customerGrade = "ENDYEAR";
		bonusRatio = 0.15;
	}
	
	public static void addCustomer_EndYear(Customer_EndYear ce) {
		c_List.add(ce);
	}
	
	public static void getDiscountCoupon() {
		for (Customer_EndYear cey : c_List) {
			
			if(cey.count_cey == 1) {
				System.out.println("할인권이 이미 발급되었습니다. 기발급된 할인권을 확인해주세요.");
			}
			else {
				System.out.println("10% 할인권 2장이 발급되었습니다.");
				cey.count_cey += 1;
			}
		}
	}
	
	public static void showAllCustomer_EndYear() {
		for (Customer_EndYear ce : c_List) {
			System.out.println(ce);
		}
		System.out.println();
	}
	
	@Override
	public String toString() {
		return "고객 번호: " + customerID + ", 고객 이름: " + customerName;
	}

	@Override
	public void Nov_givePresents(int tot_price)
	{
		Customer_EndYear_sum += calcPrice(tot_price);
		
		int Nov_tot_sum = Customer_EndYear_sum;
		
		if (Nov_tot_sum >= 50000 && curNovCount_p1 <= 10) {
			System.out.println("11월 5만원 이상 경품이 지급되었습니다.");
			curNovCount_p1 = Nov_count_p1 - 1;
		} else if (Nov_tot_sum >= 30000 && curNovCount_p2 <= 30) {
			System.out.println("11월 3만원 이상 경품이 지급되었습니다.");
			curNovCount_p2 = Nov_count_p2 - 1;
		} else if (Nov_tot_sum >= 20000 && curNovCount_p3 <= 100) {
			System.out.println("11월 2만원 이상 경품이 지급되었습니다.");
			curNovCount_p3 = Nov_count_p3 - 1;
		} else {
			System.out.println("경품을 받으려면 최소 2만원 이상 구매하셔야 합니다. 다음 기회에 구매 부탁드립니다.");
		}
	}

	@Override
	public void Dec_givePresents(int tot_price)
	{
		Customer_EndYear_sum += calcPrice(tot_price);
		
		int Dec_tot_sum = Customer_EndYear_sum;
		
		if (Dec_tot_sum >= 50000 && curDecCount_p1 <= 10) {
			System.out.println("12월 5만원 이상 경품이 지급되었습니다.");
			curDecCount_p1 = Dec_count_p1 - 1;
		} else if (Dec_tot_sum >= 30000 && curDecCount_p2 <= 30) {
			System.out.println("12월 3만원 이상 경품이 지급되었습니다.");
			curDecCount_p2 = Dec_count_p2 - 1;
		} else if (Dec_tot_sum >= 20000 && curDecCount_p3 <= 100) {
			System.out.println("12월 2만원 이상 경품이 지급되었습니다.");
			curDecCount_p3 = Dec_count_p3 - 1;
		} else {
			System.out.println("경품을 받으려면 최소 2만원 이상 구매하셔야 합니다. 다음 기회에 구매 부탁드립니다.");
		}
	}
}

11월 경품 관련 내용은 인터페이스 Nov_CustomerGift에 담고, 12월 경품 관련 내용은 인터페이스 Dec_CustomerGift에 담는데 각각 안에는 경품 지급을 가려내는 메소드 (Nov_givePresents와 Dec_givePresents)를 선언하고, (=> abstract 안써도 추상 메소드로 정의)

또한 정적 상수로서 5만원 이상/3만원 이상/2만원 이상 경품 최대 지급 개수를 담는 int 자료형 Nov_count_p1, Nov_count_p2, Nov_count_p3도 선언하였다. (객체 모두 단 하나의 메모리를 공유하도록 하기 위해서)

그리고 오버라이드 한 givePresents 메소드에서는 구매한 총 물품의 가격을 계산하는, Customer 클래스의 calcPrice메소드를 사용해 나온 값을 최종적으로 Nov/Dec_tot_sum 변수에 담아

if문을 돌려 이 변수가 50000보다, 30000보다, 20000보다 크고 그러면서 금액에 따라 남은 경품 개수들을 표현하는 curDecCount_p1, curDecCount_p2, curDecCount_p3이 최대 갯수들보다 작으면 경품을 지급하고 그게 아니면 경품을 지급하지 않도록 구현했다.

(+ 인터페이스를 implements해 오버라이드한 메소드 혹은 변수는 꼭 인터페이스에서 선언된 접근제어자 이상으로 접근 제어자 선언을 해줘야 한다. (default면 default 혹은 public 둘중 하나로, 이 접근 제어자 부분은 나중에 후술하겠다.)

결국 이런 11월, 12월 인터페이스들은 한시적인 행사이기에 끝나면 더이상 쓰지 않을 내용이기에 클래스 상속, extends로 하지 않고 (extends는 오로지 하나만 되기에 어차피 할수는 없지만 그리고 상속 extends의 경우 부모 클래스의 내용이 바뀌면 자식 클래스의 내용도 바꿔주어야 하는게 일반적이다)

implements를 통한 인터페이스로 잠깐 쓸 내용인 11월, 12월 경품 지급 이벤트를 선언하고 그것을 구체적으로 자유롭게 구현하는 Customer_EndYear 클래스라는 구조를 만들어서 어떤 일시적인 사례에 대해서 이미 부모 클래스로부터 상속을 받았더라도 자식 클래스가 그에 구애받지 않고 인터페이스를 통해 또 추가적인 기능을 구현하는게 가능해진다.

결국, 인터페이스를 쓰는 목적은 상속을 이미 받은 클래스나 혹은 상속으로 인한 추가적인 코드 수정 작업을 피하면서 자유롭게 추가 기능이나 변수들을 넣기 위한것이라고 할수 있겠다.

만약 인터페이스를 상속받은 클래스가 인터페이스에서 선언된 추상 메소드를 전부 구체적으로 구현하지 않았다면, 그 클래스는 추상 클래스로 선언되어야 한다. 또한, 위의 Customer_EndYear는 Customer 클래스를 상속받긴 했지만 인터페이스 Nov_CustomerGift,Dec_CustomerGift를 Implements로 끌어왔기에 extends 상속처럼 상위 인터페이스가 되어 객체 생성시 상위 인터페이스 형으로 묵시적 형 변환이 가능해진다. 바로 아래처럼 말이다:

Nov_CustomerGift nc = new Customer_EndYear();

Dec_CustomerGift nc = new Customer_EndYear();

다만 상속처럼 상위 인터페이스로 묵시형 변환뒤에는 상위 인터페이스가 가지고 있던 정적 상수나 멤버 변수 및 메서드밖에 사용하지 못한다. (하위 클래스에만 있던 메서드는 호출 사용 X)

좀 더 깊게 인터페이스에 대해 생각해보자면, 사실 인터페이스만 봐도 그 인터페이스를 도입 implements한 클래스의 내용을 짐작할수 있다.

예를 들면, 클라이언트 프로그램 A에서 클래스 B를 사용하는데 클래스 B에는 인터페이스 zxcv를 사용한다고 한다. 그렇다면 클래스 B를 굳이 다 보지 않고도 인터페이스 zxcv로 가서 안에 있는 정적 상수나 변수, 메서드만을 보고도 클래스 B안에서 어떤 메서드를 쓰고 그 메서드가 어떤 매개변수를 받고 리턴값은 무엇인지 대략적이나마 짐작할수 있다.

또한, 상황이 바뀌어 인터페이스는 그대로 도입하되 내용이 바뀐 클래스 C를 써야한다고 할때 extends 상속과는 달리 인터페이스를 implements 도입 하는것은 제한없이 가능하므로 클래스 C를 다시 새롭게 만들고 인터페이스 zxcv를 implements 도입해 안의 내용을 구체화 한 뒤 이것을 클라이언트 프로그램 A에서 사용만 하면 된다.

즉, 어떤 인터페이스를 도입한 클래스가 있다고 할때 그 인터페이스는 앞으로 클래스가 사용할 메서드나 정적 상수에 관한 설명이며 만약 상황이 바뀌어 인터페이스는 유지하지만 클래스를 바꿔야 한다고 할때 기존 클래스나 이미 있던 상속 관계와 상관없이 자유롭게 클래스를 또 다시 만들고 그 클래스와 인터페이스를 묶어주고 클래스만 구체화 해주면 새롭거나 추가된 내용을 프로그램에 실행할수 있게 된다.

덧붙여서, 자바 7 버젼까지 인터페이스에서 코드를 구체적으로 구현할수 없었기에 인터페이스를 implements한 여러 클래스가 서로 공통 내용을 담은 메소드를 사용할 경우가 많았었는데

자바 8 버젼부터는 인터페이스에서도 메소드 명 앞에 default 선언을 하고 중괄호{}를 사용해 공통 내용을 구현할수 있게 하는 디폴트 메서드, 메소드 명 앞에 static 선언을 하고 객체 생성 안하고 클래스명.메소드명만으로도 호출할수 있게끔 하는 정적 메서드가 가능해졌다.

또, 자바 9 부터는 private 선언도 가능해져 하위 클래스에서 직접적으로 사용하거나 재정의가 안되게 해야하는, 그리고 개인정보나 보안쪽으로 민감한 내용이거나 기본 공통 내용의 경우 private 선언을 하고 (다만 private과 abstract는 같이 올수 없기에) 코드를 모두 구현하고 인터페이스의 디폴트 메서드나 정적 메서드를 통해 간접적으로 호출하는 형태도 구현이 가능하다.

더 나아가자면 인터페이스의 활용은 무궁무진하다. 첫번째로는 여러 인터페이스들을 한 클래스에서 한꺼번에 그냥 implements 도입하는 경우도 있으며,

두번째는 다중 인터페이스를 implements 도입한 클래스가 있는 상황에서 인터페이스들이 공통적으로 가지고 있는, 앞에서 언급한 디폴트 메서드가 동일한 명에 동일한 매개변수에 동일한 리턴 타입이라고 할때는 하위 클래스에서 Override 재정의를 통해 문제를 해결하는 형태도 있고,

(이 경우 상위 인터페이스로 묵시적 형 변환이 가능하며 그렇게 형 변환해도 중복이 됐던 디폴트 메서드를 호출하면 최종 하위 클래스에서 재정의 했던 메서드 내용이 호출된다. <= 자바의 가상 메서드 원리)

세번째는 상위 인터페이스들을 다중 상속 extends (같은 인터페이스간/클래스간 사이에서 인터페이스/클래스를 상속하는/받는것은 extends이다) 받은 중간 인터페이스를 마지막 하위 클래스에서 implements 도입해 모든 인터페이스들이 언급한 메소드를 모두 모아 한꺼번에 구현하는 형태도 있고,

마지막으로 네번째는 위에서 A마트 연말 이벤트 행사 사례로 제시한것처럼 클래스 상속과 함께 인터페이스 구현도 같이 쓰는 (public class X extends Y implements Z) 형태가 있을수 있다.

이처럼, 인터페이스는 위에서 봤던 다형성 향상에 큰 도움을 주며 이는 상속과 함께 객체 지향적이라는 자바 프로그램 구현에 있어서 아주 중요한 열쇠가 된다. 왜냐하면 하나의 클래스 혹은 하나 아니면 여러개의 인터페이스를 상속 extends 받거나 implements 도입받으므로써 하나의 코드 선언만으로도 마지막 하위 클래스들에서 여러개의 내용을 구현하고 사용할수 있기 때문이다.

일단 가장 꽃이자 나름 중요했던 상속, 인터페이스, 다형성 부분은 이로써 마치고 다음장에선 우리가 많이 썼지만 항상 잘 모르고 지나쳤던 접근 제어자들과 this 예약어 그리고 가장 많이 쓰는 코드 디자인 패턴인 싱글톤 패턴에 대해 보겠다.

0개의 댓글