본 포스트는 카카오 테크 캠퍼스 1기에서 제공하는 패스트캠퍼스 강의에서 배운 내용을 정리하였습니다.

카카오 테크 캠퍼스

상속


설명

  • 상속은 하나의 클래스가 다른 클래스의 속성과 동작을 가져와 그대로 사용할 수 있도록 하는 기능이다.

  • 새로운 클래스를 정의 할 때 이미 구현된 클래스를 상속(inheritance) 받아서 속성이나 기능을 확장하여 클래스를 구현함

  • 이미 구현된 클래스보다 더 구체적인 기능을 가진 클래스를 구현해야 할때 기존 클래스를 상속함

  • 상속하는 클래스 : 상위 클래스, parent class, base class, super class

  • 상속받는 클래스 : 하위 클래스, child class, derived class, subclass

  • 상위 클래스는 하위 클래스 보다 더 일반적인 개념과 기능을 가짐

  • 하위 클래스는 상위 클래스 보다 더 구체적인 개념과 기능을 가짐

  • 하위 클래스가 상위 클래스의 속성과 기능을 확장 (extends)한다는 의미

문법

class B extends A{
} //클래스 B가 클래스 A를 상속한다는 의미.
  • extends 키워드 뒤에는 단 하나의 클래스만 올 수 있음

  • 자바는 단일 상속(single inheritance)만을 지원함

예시

예시는 구체적인 시나리오가 있으면 더욱 좋다. 다음과 같은 예시를 들겠다.

멤버십 시나리오

회사에서 고객 정보를 활용한 맞춤 서비스를 하기 위해 일반고객(Customer)과 이보다 충성도가 높은 우수고객(VIPCustomer)에 따른 서비스를 제공하고자 한다.

1. 물품을 구매 할때 적용되는 할인율과 적립되는 보너스 포인트의 비율이 다름

2. 여러 멤버십에 대한 각각 다양한 서비스를 제공할 수 있음

클래스 상속을 활용하여 구현해보기

  1. 일반 고객(Customer) 클래스 구현
  • 고객의 속성 : 고객 아이디, 고객 이름, 고객 등급, 보너스 포인트, 보너스 포인트 적립비율

  • 일반 고객의 경우 물품 구매시 1%의 보너스 포인트 적립

Customer.java

package ch01;

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 += price * bonusRatio;
		return price;
	}
	
	public String showCustomerInfo() {
		return customerName + "님의 등급은 " + customerGrade + 
				"이며, 보너스 포인트는" + bonusPoint + "입니다";
		
	}
}
  1. 우수 고객(VIPCustomer) 구현

    매출에 더 많은 기여를 하는 단골 고객
    제품을 살때 10%를 할인해 줌
    보너스 포인트는 제품 가격의 5%를 적립해 줌
    담당 전문 상담원이 배정됨

  • Customer 클래스에 추가해서 구현하는 것은 좋지 않음

  • VIPCustomer 클래스를 따로 구현

  • 이미 Customer에 구현된 내용이 중복되므로 Customer를 확장하여 구현함(상속)

😏ㅋㅋ바본가 그냥 if절 추가하셈😏

마자마자. 내말이 그말임 ㅎ. 그냥 VIP 고객만 추가된건데, 굳이 전용 클래스 까지 만들어야하는 이유가 있음? 코드 낭비, 시간 낭비인듯..

=> 그래, 위에 시나리오가 더이상 확장될 여지 없이 고정됐고, 추가적인 요구사항이 절대 없다면 당연히 그게 훨씬 간단하고 가시적이지.

=> 하지만, 대부분의 프로젝트는 그런식으로 흘러가지 않는다. 초기 계획과 완전히 다르게 진행되는 프젝도 있고, 중간중간 요구사항, 수정사항이 끝없이 들어온다.

=> 당장 고객등급만 봐도, VIP 고객만 있어서 그렇지, 나중에 Gold 고객 Silver, Bronze 고객같이 등급을 세분화 해달라고 요구사항이 추가 될수 있잖아?

=> 당장 가격계산 메서드만 봐도

public int calcPrice(int price) {
	if (customerGrade=="Silver") {
    	bonusPoint += price * bonusRatio;
		return price;
    }
    else if (customerGrade=="Gold") {
    		//blablabla
    }
	else if (customerGrade=="Vip") {
    		//blablabla
    }
      ...
    /// 고객 등급이 추가될때마다 모든 메서드마다 등급분류하는 if절 추가해줘야함 ;;
		
	}

위처럼 고객 등급이 추가될떄마다 메서드에 if절 추가할껀가? 등급별 다르게 기능해야될 메서드가 저거 말고도 무수히 많을 텐데?

=> 아 맞네, 추후 확장성, 유연성을 위해 상속을 하는 거구나

=> 그래서 나는 만약 Gold등급을 추가해야한다면, GoldCustomer클래스를 정의할 것이다.

그리고 VIP, Gold 고객 클래스는 기본 고객 클래스와 IS-A관계이기 때문에 상속을 사용하기 더할나위없이 좋은 케이스이다.
(IS-A 관계 뒤에 설명)

이제 이어서,

우수 고객 클래스를 구현하면

VIPCustomer.java

public class VIPCustomer extends Customer{

	private int agentID;
	double salesRatio;
	
	public VIPCustomer() {
		customerGrade = "VIP";    //오류 발생
		bonusRatio = 0.05;
		salesRatio = 0.1;
	}
	
	public int getAgentID() {
		return agentID;
	}
}

다음과 같다. 여기서 오류 발생이라고 한 부분을 보면,

  • 상위 클래스에 선언된 private 멤버 변수는 하위 클래스에서 접근 할 수 없다 => customerGrade 변수사용시 오류 발생

  • 외부 클래스는 접근 할 수 없지만, 하위 클래스는 접근 할 수 있도록 protected 접근 제어자를 사용

  • 그리고 외부에서 접근을 위한 getter/setter 선언

Customer.java

	protected int customerID;
	protected String customerName;
	protected String customerGrade; 
    //이제 VIPCustomer에서 오류안뜸

	//getter, setter 구현
	...
	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;
	}

Customer와 VIPCustomer 테스트하기

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());
	}
}

출력결과

상속에서 클래스 생성 과정

하위 클래스가 생성 되는 과정

  • 하위 클래스를 생성하면 상위 클래스가 먼저 생성 됨

  • new VIPCustomer()를 호출하면 Customer()가 먼저 호출 됨

  • 클래스가 상속 받은 경우 하위 클래스의 생성자에서는 반드시 상위 클래스의 생성자를 호출 함

Customer 생성자

public Customer() {
		customerGrade = "SILVER";
		bonusRatio = 0.01;
		
		System.out.println("Customer() 생성자 호출");
}

VIPCustomer 생성자

public VIPCustomer() {
		customerGrade = "VIP";
		bonusRatio = 0.05;
		salesRatio = 0.1;
		
		System.out.println("VIPCustomer() 생성자 호출");
}

위 경우에서 VIPCustomer 인스턴시 생성시 출력은

"Customer() 생성자 호출"
"VIPCustomer() 생성자 호출"

이렇게 출력될 것이다.

super 키워드

  • 하위 클래스에서 가지는 상위 클래스에 대한 참조 값

  • super()는 상위 클래스의 기본 생성자를 호출

  • 하위 클래스에서 명시적으로 상위 클래스의 생성자를 호출하지 않으면 컴파일러가 자동으로 super()를 호출

    => 자식생성자 호출 시 반드시 부모생성자를 호출해야되기 때문

  • 이때 반드시 상위 클래스의 기본[디폴트] 생성자가 존재 해야 함

    => 상위 클래스의 기본 생성자가 없는데 super() 호출하면 오류가 생김

  • 상위 클래스의 기본 생성자가 없는 경우 ( 다른 생성자가 있는 경우 ) 하위 클래스의 생성자에서는 super를 이용하여
    명시적으로 상위 클래스의 생성자를 호출 함

ex)
Customer.java

// 디폴트 생성자 없애고 매개 변수가 있는 생성자 추가
public Customer(int customerID, String customerName) {
		this.customerID = customerID;
		this.customerName = customerName;
		
		customerGrade = "SILVER";
		bonusRatio = 0.01;
		System.out.println("Customer(int, String) 생성자 호출");
}

VIPCustomer.java

// super를 이용하여 상위 클래스의 생성자 명시적으로 호출
public VIPCustomer(int customerID, String customerName) {		
		super(customerID, customerName);// 이렇게 해줘야함
		// super()를 호출할 경우 오류가남
        // => Customer 클래스가 기본생성자가 없기때문에
        // => 자바에서 클래스는 생성자를 따로 정의할경우, 
        // 기본 생성자는 자동으로 생성되지 않음
		customerGrade = "VIP";
		bonusRatio = 0.05;
		salesRatio = 0.1;
		
		System.out.println("VIPCustomer(int, String) 생성자 호출");
}

super는 생성된 상위 클래스 인스턴스의 참조 값을 가지므로 super를 이용하여 상위 클래스의 메서드나 멤버 변수에 접근할 수 있음

ex)

super.bounusRatio  //변수
super.getCustomerGrade()  //메서드

위에 방식대로 사용가능

상속에서 인스턴스 메모리의 상태

  • 항상 상위 클래스의 인스턴스가 먼저 생성되고, 하위 클래스의 인스턴스가 생성 됨

형 변환(업캐스팅)

  • 상위 클래스로 변수를 선언하고 하위 클래스의 생성자로 인스턴스를 생성

ex)

Customer customerLee = new VIPCustomer();

상위 클래스 타입의 변수에 하위 클래스 변수가 대입;

VIPCustomer vCustomer = new VIPCustomer();
//자식클래스로 변수 선언

int addCustomer(Customer customer){
}
//부모 클래스를 매개변수로 받는 함수 선언

addCustomer(vCustomer);
//매개변수로 자식 클래스 자료형 넣어도 오류안남
//=> 묵시적 형 변환 일어났기 떄문. 

하위 클래스는 상위 클래스의 타입을 내포하고 있으므로 상위 클래스로의 묵시적 형 변환이 가능함

상속 관계에서 모든 하위 클래스는 상위 클래스로 형 변환(업캐스팅)이 됨
( 그 역은 성립하지 않음) => 그 역은 다운캐스팅이라함.

쉽게 말해 원래 대입연산자가 오른쪽을 왼쪽에 대입, 즉 rv를 lv에 넣는 과정이라생각하면 쉽다.

포유류=사람 (사람은 포유류이다)
탈것=비행기(비행기는 탈것이다)

근데 반대는 이상하잖아 => 그게 다운캐스팅 명시적형변환 해줘야함

형 변환과 메모리 접근

  • Customer vc = new VIPCustomer(); 에서 vc가 가리키는 것은?

  • VIPCustomer() 생성자에 의해 VIPCustomer 클래스의 모든 멤버 변수에 대한 메모리는 생성되었지만,

  • 변수의 타입이 Customer 이므로 실제 접근 가능한 변수나 메서드는 Customer의 변수와 메서드임

  • (주의! VIPCustomer 클래스의 멤버들이 생성이 안된게 아님!
    메모리에 로드 돼있음! 접근을 못할 뿐...)

  • vc 변수의 타입은 Customer지만 인스턴스의 타입은 VIPCustomer 임

  • 자바에서는 항상 인스턴스의 메서드가 호출 됨 (가상메서드의 원리)

  • 자바의 모든 인스턴스 메서드는 가상 메서드(virtual method) 임

가상메서드

메서드는 어떻게 호출되고 실행 되는가?

  • 메서드(함수)의 이름은 주소값을 나타냄

  • 메서드는 명령어의 set 이고 프로그램이 로드되면 메서드 영역(코드 영역)에 명령어 set이 위치

  • 해당 메서드가 호출 되면 명령어 set 이 있는 주소를 찾아 명령어가 실행됨

  • 이때 메서드에서 사용하는 변수들은 스택 메모리에 위치 하게됨

  • 따라서 다른 인스턴스라도 같은 메서드의 코드는 같으므로 같은 메서드가 호출됨

  • 인스턴스가 생성되면 변수는 힙 메모리에 따로 생성되지만, 메서드 명령어 set은 처음 한번만 로드 됨

쉽게 말해서 code영역에서 우리가 짠 메서드가

⇒ 기계어(명령어집합(인스트럭션 셋))로 코드 영역에 저장

⇒그리고 함수 호출시 해당 명령어 셋이있는 주소로가서 cpu가 읽는다

⇒ 그 메서드에서도 지역변수가 쓰인다? 그럼 stack영역에 메모리 점유했다가 메서드 블록이 끝나면 소멸되겠지;; 앞에서 배웠잖아!

그리고 당연히, 인스턴스마다 메서드를 호출하면 그 메서드가 복제되서 주소가 생기는게 아니라,

그냥 A라는 메서드를 호출하면 그 A에 담겨있는 로직이 이 위치해있는 주소로 가는거임.

배열의 이름이 주소인것과 마찬가지로, 함수 명도 그 함수의 로직이 담겨있는 주소라고 생각하면된다.

ex)

public class TestMethod {

	int num;
	
	void aaa() {
		System.out.println("aaa() 호출");
	}
	
	public static void main(String[] args) {
		
		TestMethod a1 = new TestMethod();
		a1.aaa();
		
		TestMethod a2 = new TestMethod();
		a2.aaa();
	}

}

위 코드에서 a1, a2 는 참조 '지역변수' 로서 stack영역에 할당,
a1,a2 가 각각 가리키는 멤버변수 num은 각각 heap영역에 할당,
인스턴스 메서드인 aaa는 인스턴스 a1,a2가 생성될떄마다 추가되는게아니고, 메서드영역에 한번만 할당됨.

가상 메서드의 원리

  • 클래스마다 가상 메서드 테이블이 있다.

  • 가상 메서드 테이블(vitual method table)에서 해당 메서드에 대한 address를 가지고 있음

  • 재정의된 경우는 재정의 된 메서드의 주소를 가리킴

그럼 이제설명이 가능하다

부모클래스 변수명=new 자식클래스(); 했을떄,

변수 타입은 부모클래스가 맞지만, 인스턴스 타입은 자식클래스이다.

따라서 자바는 인스턴스 타입의 가상메서드 테이블을 따르므로, 호출한 함수가 자식클래스에서 오버라이딩한 함수라면 재정의된 함수가 실행된다!

클래스 메서드는 가상 메서드?

클래스 메서드(Static method)는 가상 메서드가 아니다!

클래스 메서드는 객체의 인스턴스와는 무관하게 클래스의 이름을 통해 호출되며, 정적 바인딩(Static binding)으로 호출 대상이 결정된다.

따라서, 클래스 메서드는 가상 메서드가 아니며, 호출 대상 객체의 실제 타입에 따라 실행되는 메서드가 아닙니다.

다운 캐스팅과 instanceof

다운 캐스팅(downcasting)

  • 업캐스팅된 클래스를 다시 원래의 타입으로 형 변환

  • 하위 클래스로의 형 변환은 명시적으로 해야 함

  • Customer vc = new VIPCustomer(); //묵시적

  • VIPCustomer vCustomer = (VIPCustomer)vc; //명시적

instanceof를 이용하여 인스턴스의 형 체크

  • 원래 인스턴스의 형이 맞는지 여부를 체크하는 키워드 맞으면 true 아니면 false를 반환 함

상속은 언제 사용할까

IS-A 관계 (is a relationship : inheritance)

  • 일반적인(general) 개념과 구체적인(specific) 개념과의 관계

  • 상위 클래스 : 하위 클래스보다 일반적인 개념 ( 예: Employee )

  • 하위 클래스 : 상위 클래스보다 구체적인 개념들이 더해짐 ( 예: Engineer, Manager...)

  • 상속은 클래스간의 결합도가 높은 설계

  • 상위 클래스의 수정이 많은 하위 클래스에 영향을 미칠 수 있음

  • 계층구조가 복잡하거나 hierarchy가 높으면 좋지 않음

ex) Customer 라는 일반적인 개념
VIPCustomer, GoldCustomer , SilverCustomer 라는 구체적인 개념

HAS-A 관계 (composition)

  • 클래스가 다른 클래스를 포함하는 관계 ( 변수로 선언 )

  • 코드 재사용의 가장 일반적인 방법

  • 상속하지 않음
    => 인터페이스를 이용해서 HAS-A 관계 표현

ex)

  • Student가 Subject를 포함함

  • Library를 구현할 때 ArrayList 생성하여 사용

    => 우리가 클래스안에 ArrayList를 사용한다고 해서 ArrayList를 상속하지는 않음

  • 보통 HAS-A 관계는 인터페이스로 구현함
    (velog에 인터페이스 정리한 곳 참고)

결론

IS-A 관계면 상속을 사용하고, HAS-A 관계이면 인터페이스로 구현 권장

다형성


다형성(polymorphism) 이란?

  • 하나의 코드가 여러 자료형으로 구현되어 실행되는 것

  • 같은 코드에서 여러 다른 실행 결과가 나옴

  • 정보은닉, 상속과 더불어 객체지향 프로그래밍의 가장 큰 특징 중 하나임

  • 다형성을 잘 활용하면 유연하고 확장성있고, 유지보수가 편리한 프로그램을 만들수 있음

다형성의 예

class Animal{
	
	public void move() {
		System.out.println("동물이 움직입니다.");
	}
	
	public void eating() {
		
	}
}

class Human extends Animal{
	public void move() {
		System.out.println("사람이 두발로 걷습니다.");
	}
	
	public void readBooks() {
		System.out.println("사람이 책을 읽습니다.");
	}
}

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


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



public class AnimalTest {

	public static void main(String[] args) {
		Animal hAnimal = new Human();
		Animal tAnimal = new Tiger();
		Animal eAnimal = new Eagle();
		
		ArrayList<Animal> animalList = new ArrayList<Animal>();
		animalList.add(hAnimal);
		animalList.add(tAnimal);
		animalList.add(eAnimal);
		
		for(Animal animal : animalList) {
			animal.move();
		}
	}	
	
}

출력은 다음과 같다.

사람이 두발로 걷습니다.
호랑이가 네 발로 뜁니다.
독수리가 하늘을 날아갑니다.

Animal 자리에 어떤 동물 타입이 들어가냐에 따른 다른 출력이 나온다.
=> 다형성 보여줌

profile
새로운 여정은 언제나 두렵고 동시에 흥미로 가득 차 있다.

0개의 댓글