[Live Study] #6 상속

ohzzi·2021년 1월 21일
0

Live Study

목록 보기
6/13

목표

자바의 상속에 대해 학습하세요.

학습할 것 (필수)

  • 자바 상속의 특징
  • super 키워드
  • 메소드 오버라이딩
  • 다이나믹 메소드 디스패치 (Dynamic Method Dispatch)
  • 추상 클래스
  • final 키워드
  • Object 클래스

상속

상속(Inheritance)란 자식이 부모로부터 무언가를 물려받는 행위를 말한다. 객체 지향 프로그래밍에서도 비슷하게 부모 클래스가 자식 클래스에게 기능을 물려줄 수 있고, 이는 객체 지향 프로그래밍의 3요소 중 하나다.

과거에 유행했던 스타크래프트를 예시로 들어보도록 하겠다. 모든 유닛들은 각각의 값은 다 다르지만 공통적인 속성들을 가진다. 또한 이동, 공격 등의 공통적인 행동도 가진다. 때문에 이들 위에 Unit이라는 상위 클래스를 구현하고, 이를 상속해서 재사용할 수 있다.

자바 상속의 특징

자바의 상속은 extends 키워드에 의해서 이루어진다.

"Unit.java"
public class Unit {
    int hitPoint;
    int speed;
    int range;
    int damage;
}

"Marine.java"
public class Marine extends Unit {
    public void steamPack() {
        this.speed += 10;
    }
}

이렇게 Marine 클래스가 Unit 클래스를 extends 키워드로 상속받게 되면, Unit 클래스에 있는 속성들과 메소드들을 이어받게 된다. 즉, Marine 클래스는 Unit 클래스의 멤버 변수들과 메소드들을 가져다 쓸 수 있다. 그러나 Marine 클래스에 정의되어 있는 steamPack 이라는 메소드를 Unit 클래스가 사용하는 것은 불가능하다.

자바는 다중 상속을 할 수 없다. 이게 무슨 의미냐 하면, 한 클래스는 여러개의 부모 클래스를 가질 수 없는다는 의미다. (한 부모 클래스가 여러 자식 클래스를 갖는 것은 상관 없다.) Marine, Medic 두 클래스가 모두 Unit 클래스를 상속받는 것은 가능하지만, 어떤 새로운 클래스 A가 Marine, Medic 두 클래스로부터 상속받는 것은 불가능하다.


(죽음의 다이아몬드)

이렇게 다중으로 상속 받는 것을 죽음의 다이아몬드라고 하는데, 한 구문에 대해 여러 의미로 해석될 수 있어서 문제가 된다. 때문에 자바는 아예 다중 상속 자체를 막아버려 이를 방지하고 있다.

자바의 상속의 또다른 특징은 상속의 상속의 상속의 ... 상속이 가능하다는 것이다. 다시 말하자면, 자식 클래스가 다른 클래스의 부모 클래스가 될 수 있다는 것이다.

다시 스타크래프트를 예로 들어보면, 스타크래프트에는 세 개의 종족이 있고, 각 종족별로 다른 특징들을 가지고 있다. 때문에 Unit 클래스 하위에 각 종족별 클래스를 구분하고 이를 상속할 수 있다.

"Unit.java"
public class Unit {
    int hitPoint;
    int speed;
    int range;
    int damage;
}

"TerranUnit.java"
public class TerranUnit extends Unit {
    boolean isHealable; // 테란 유닛 중 메딕이 치료 가능한지 여부
}

"Marine.java"
public class Marine extends Unit {
    ...
}

"ProtossUnit.java"
public class ProtossUnit extends Unit {
    int shields; // 실드는 프로토스만의 특징이므로 protoss 클래스에만 작성
}

"Dragoon.java"
public class Dragoon extends ProtossUnit {
    ...
}

super 키워드

상속받은 자식 클래스는 super 키워드를 통해 부모 클래스에 접근할 수 있다. 인스턴스 변수의 이름과 지역 변수의 이름이 같을 경우 인스턴스 변수 앞에 this 키워드를 사용하여 구분할 수 있듯이, 부모 클래스의 멤버와 자식 클래스의 멤버 이름이 같을 경우 super 키워드를 사용하여 구별할 수 있다.

일반적으로 super 키워드가 많이 쓰이는 곳은 바로 생성자다. 모든 객체는 생성 시 반드시 생성자를 호출해야 하는데, 자식 클래스의 인스턴스를 생성할 때 부모 클래스의 멤버를 사용할 수도 있기 때문에 먼저 부모 클래스의 생성자를 호출하여 초기화 해 주어야 한다.

부모 클래스의 생성자는 자식 클래스의 생성자의 첫 줄에서 호출된다. 만약 부모 클래스의 생성자가 자식 클래스의 생성자에서 명시적으로 호출되지 않으면 컴파일러는 자식 클래스의 생성자에 자동으로 부모 클래스의 생성자 super() 메소드를 추가한다. 이 때, 부모 클래스에 no args constructor (매개변수를 받지 않는 생성자)가 없다면 오류가 발생한다.

만약 부모 클래스의 생성자 중 매개변수를 받는 생성자를 호출하고 싶다면 자식 클래스의 생성자에 super(매개변수...)를 명시적으로 작성해 주면 된다.

"Unit.java"
public class Unit {
    int hitPoint;
    int speed;
    int range;
    int damage;
    
    public Unit(int hitPoint, int speed, int range, int damage) {
        this.hitPoint = hitPoint;
        this.speed = speed;
        this.range = range;
        this.damage = damage;
    }
}

"ProtossUnit.java"
public class ProtossUnit extends Unit {
    int shields;
    
    public ProtossUnit(int hitpoint, int speed, int range, int damage, int shields) {
        super(hitpoint, speed, range, damage);
        this.shields = shields
    }
}

"Dragoon.java"
public class Dragoon extends ProtossUnit {
    public Dragoon(int hitpoint, int speed, int range, int damage, int shields) {
        super(hitpoint, speed, range, damage, shields);
    }
}

올바르게 설계된 예시는 아니지만, 위와 같이 작성을 하면 super 키워드를 이용해서 인스턴스를 생성할 수 있다.

메소드 오버라이딩

앞서 자식 클래스가 상속을 받으면 부모 클래스의 메소드를 사용할 수 있다고 했다. 하지만 부모 클래스의 메소드의 동작이 자식 클래스에 딱 맞게 동작한다고는 보장할 수 없다. 이런 경우에, 자식 클래스에서 부모 클래스의 메소드의 동작을 재정의 할 수 있는데, 이를 메소드 오버라이딩(Method Overriding)이라고 한다.

메소드 오버라이딩을 하면 자식 객체에서 메소드를 호출했을 때 부모의 메소드가 아닌 오버라이딩을 한 메소드가 호출된다. 단, 선언을 다시 하는 것이 아닌 동작만 수정하는 것이므로 메소드의 선언부는 같아야 한다. 메소드 오버라이딩 된 메소드의 위에는 @Override 어노테이션을 붙여서 표시하는데, 생략이 가능하지만 메소드 오버라이딩이 되었는지에 대한 체크를 정확히 하기 위해서는 생략하지 않는 것이 더 좋다.

"People.java"
public class Person {
    public void tell() {
        System.out.println("사람입니다.");
    }
}

"Student.java"
public class Student extends Person {
    @Override
    public void tell() {
        System.out.println("학생입니다.");
    }
}

"Teacher.java"
public class Teacher extends Person {
    @Override
    public void tell() {
        System.out.println("교사입니다.");
    }
}

"Main.java"
public class Main {
    public static void main(String[] args) {
        Person student = new Student(); // 부모 타입으로 생성할 수 있다.
        Person teacher = new Teacher();
        student.tell(); // 학생입니다. 출력
        teacher.tell(); // 교사입니다. 출력
    }
}

오버라이딩을 할 때는 메소드의 선언부가 같아야 한다고 했는데, 이는 단순히 매개변수가 같아야 한다는 것 뿐 아니라 접근 제어자도 같아야 하고 (예를 들어 부모 클래스에서 public인 메소드를 자식 클래스에서 private로 오버라이딩 할 수 없다.) 새로운 예외도 던질 수 없다. (부모 클래스에서 IOException을 던지는데 오버라이딩한 메소드에서 ArithmeticException을 던질 수 없다.)

메소드 오버라이딩을 한 상태에서 오버라이딩을 하기 이전의 부모 메소드를 호출하고 싶다면 super.메소드이름 을 이용해 부모 메소드를 호출할 수 있다.

다이나믹 메소드 디스패치

메소드 디스패치(Method Dispatch)는 어떤 메소드를 호출할지를 결정하여 실행시키는 것을 말한다. 메소드를 오버로딩 하게 되면 이름은 같은 메소드라도 매개변수에 따라 메소드가 구분되기 때문에 컴파일 타임에 어떤 메소드를 호출해야 할 지 알 수 있다. 이를 스태틱 디스패치(Static Dispatch)라고 한다.

이에 반대되는 다이나믹 메소드 디스패치(Dynamic Method Dispatch)는 컴파일 타임에 어떤 메소드를 호출해야 할 지 모르는 경우, 런타임에 어떤 메소드를 호출할지 결정되는 것을 의미한다.

(사실 몇가지 예제를 봐도 이 부분은 아직 잘 이해가 가지 않아서, 나중에 날 잡고 공부해 봐야 겠다는 생각이 든다.)

추상 클래스

클래스 중에 abstract 키워드가 붙여진 클래스를 볼 수 있다. 이를 추상 클래스(Abstract Class)라고 한다. 추상 클래스는 하나 이상의 추상 메소드를 포함하며 인스턴스화 할 수 없다는 점을 제외한 부분들은 일반 클래스와 다르지 않다.

추상 메소드는 추상 클래스와 마찬가지로 abstract 키워드가 붙은 메소든데, 미완성 메소드를 의미한다. abstract가 붙은 메소드는 인터페이스의 그것과 마찬가지로 선언부만 있고 구현부는 없다.

추상 클래스는 반드시 추상 메소드만 가지지는 않는다. 인터페이스와는 다르게, 추상 클래스는 구현까지 되어 있는 일반 메소드와 멤버 변수를 가질 수 있다. 다음 예시를 보자.

"AsianCountry.java"
public abstract class AsianCountry {
    private String continent = "Asia";
    
    public String getContinent() {
        return continent;
    }
    
    public abstract String getCapital();
    // 참고: 추상 메소드는 private 접근 제어자 사용 불가능
}

"Korea.java"
public class Korea extends AsianCountry {
    @Override
    public String getCapital() {
        return "Seoul";
    }
}

"Japan.java"
public class Japan extends AsianCountry {
    @Override
    public String getCapital() {
        return "Tokyo";
    }
}

AsianCountry 클래스는 그 나라의 수도를 반환하는 getCapital 메소드가 어떤 값을 반환할 지 정해지지 않았다. 따라서 getCapital을 추상 메소드로 만들고 AsianCountry를 추상 클래스로 선언했다.

반면 모든 AsianCountry들은 소속 대륙이 아시아임은 분명하기 때문에, continent 멤버 변수를 이미 가지고 있고, 해당 continent를 반환하는 getContinent 메소드는 구현까지 완료되어 있는 일반 메소드로 들어있다.

따라서 AsianCountry를 상속받는 Korea, Japan 클래스는 추상 메소드인 getCapital 메소드만 오버라이딩해서 사용하면 된다. 만약 해당 추상 메소드를 오버라이딩 하지 않을 경우, 자식 클래스도 마찬가지로 추상 클래스로 선언되어야 한다.

추상 클래스를 사용하므로써 상속관계에서 공통된 필드와 메소드를 통일시키면서 각각의 자식 클래스 별로 따로 구현해야 할 메소드만 따로 구현할 수 있다. 또한 이미 공통된 필드와 실체 메소드가 구현되어 있기 때문에 클래스 구현에 걸리는 시간을 단축할 수 있다. 또한 자식 클래스들을 구현할 때 반드시 추상 메소드들을 오버라이딩 해서 구현해야 하기 때문에 클래스들의 규격을 정의할 수 있다는 장점도 있다.

final 키워드

일반적으로 final 키워드는 상수 변수를 정의할 때 사용된다. 그래서 흔히 상수를 만드는 키워드 라고만 생각할 수 있는데, final의 용례가 꼭 그렇게 국한되지는 않는다. 다만 final이 붙으면 "최종적" 이라는 뜻에 맞게 어딘가 수정 불가능한 부분이 생기게 된다.

우선 가장 일반적인 사용으로 프리미티브 타입 변수 앞에 final 키워드를 붙이면 한번 초기화 하면 값을 수정할 수 없는 상수 변수가 된다. 자바스크립트의 const를 생각하면 이해가 쉬울 것 같다. 때문에 자바에서 상수를 정의할 때는 보통 final 키워드를 붙여서 사용한다.

객체 변수를 final로 선언하면 프리미티브 타입 변수와 비슷하게 다른 참조 값을 지정할 수 없는 변수가 된다. 즉, final로 선언한 객체 변수는 같은 이름의 다른 객체를 생성할 수 없다.

class Student {
    ...
}

public class Main {
    public static void main(String[] args) {
        final Student student = new Student();
        // student = new Student(); 불가능
    }
}

만약 final 키워드가 클래스 선언부에 붙게 되면, 해당 클래스는 상속이 불가능한 클래스가 된다. 예를 들어 String 클래스는 final로 선언되어 있다. 단, setter를 통해 멤버 변수를 변경할 수는 있다. (이 경우에도 final이 붙은 멤버 변수는 변경이 불가능하다.)

만약 클래스 안의 메소드에 final 키워드가 붙으면 상속은 가능하지만 상속받은 클래스가 해당 메소드를 재정의 할 수 없다.

메소드 자체가 아니라 메소드의 인자에 final이 붙으면, 해당 메소드 내에서 final이 붙은 인자의 값을 변경할 수 없게 된다.

Object 클래스

Object 클래스는 자바의 모든 클래스의 조상격이라고 볼 수 있다. Object 클래스는 모든 클래스가 가지고 있어야 할 기능들을 가지고 있으며, 자바의 모든 클래스는 사실 Object 클래스를 상속받고 있다.

Object 클래스는 필드를 가지지 않으며, 11개의 메소드만을 가지고 있다.

오라클 공식 문서의 Object 클래스의 메소드 설명이다. 모든 클래스들은 자동으로 Object 클래스를 상속받기 때문에, 해당 메소드들을 사용할 수 있다.

참고 자료
https://limkydev.tistory.com/188
https://wikidocs.net/219
https://opentutorials.org/course/1223/6241

profile
배울 것이 많은 초보 개발자 입니다!

0개의 댓글