Chapter 07 - 상속

김태원·2023년 1월 12일
0

상속 개념

상속(Inheritance)은 부모가 자식에게 물려주는 행위를 말한다. 객체 지향 프로그램에서도 부모 클래스의 필드와 메소드를 자식 클래스에게 물려줄 수 있다.


상속은 이미 잘 개발된 클래스를 재사용해서 새로운 클래스를 만들기 때문에 중복되는 코드를 줄여 개발 시간을 단축시킨다.

예를 들어 다음 그림처럼 자식 클래스(B)에서 field1과 method1을 부모 클래스(A)에서 상속받고 field2와 method2만 추가 작성하는 것이 보다 효율적이다.

상속의 또 다른 이점은 클래스의 수정을 최소화할 수 있다는 것이다.
부모 클래스를 수정하면 모든 자식 클래스에 수정 효과를 가져온다.
예를 들어 B, C가 A를 상속할 경우 A의 필드와 메소드를 수정하면 B, C를 수정하지 않아도 수정된 A의 필드와 메소드를 이용할 수 있다.

클래스 상속

프로그램에서는 자식이 부모를 선택한다. 자식 클래스를 선언할 때 어떤 부모로부터 상속받을 것인지를 결정한다.

부모 클래스를 다음과 같이 extends 뒤에 기술한다

public class 자식클래스 extends 부모클래스 {
}

다른 언어(e.g. Python)와는 달리 자바는 다중 상속을 허용하지 않는다.
즉, 여러 개의 부모 클래스를 상속할 수 없다. 따라서 extends 뒤에는 단 하나의 부모 클래스만이 와야 한다.

public class 자식클래스 extends 부모클래스1, 부모클래스2(X) {
}

부모 생성자 호출

현실에서 부모 없이는 자식이 생성될 수 없듯이 자바에서도 자식 객체를 생성하려면 부모 객체가 먼저 생선되어야 한다.

이것을 메모리로 표현하면 다음과 같다.

모든 객체는 생성자를 호출해야먄 생성된다. 부모 객체도 예외는 아니다.
그렇다면 부모 객체의 생성자는 어디서 호출된 것일까? 이것에 대한 비밀은 자식 생성자에 숨어 있다.
부모 생성자는 자식 생성자의 맨 첫 줄에 숨겨져 있는 super()에 의해 호출된다.

// 자식 생성자 선언
public 자식 클래스(...) {
    super();
    ...
}

super()는 컴파일 과정에서 자동 추가되는데, 이것은 부모의 기본 생성자를 호출한다. 만약 부모 클래스에 기본 생성자가 없다면 자식 생성자 선언에서 컴파일 에러가 발생한다.
부모 클래스에 기본 생성자가 없고 매개변수를 갖는 생성자만 있다면 개발자는 다음과 같이 super(매개값, ...) 코드를 직접 넣어야 한다. 이 코드는 매개값의 타입과 개수가 일치하는 부모 생성자를 호출한다.

// 자식 생성자 선언
public 자식 클래스(...) {
    super(매개값, ...);
    ...
}

메소드 재정의

부모 클래스의 모든 메소드가 자식 클래스에 맞게 설계되어 있다면 가장 이상적인 상속이지만, 어떤 메소드는 자식 클래스가 사용하기에 적합하지 않을 수 있다. 이러한 메소드는 자식 클래스에서 재정의해서 사용해야 하는데 이것을 메소드 오버라이딩(Overriding)이라고 한다.

메소드 오버라이딩

메소드가 오버라이딩되었다면 해당 부모 메소드는 숨겨지고, 자식 메소드가 우선적으로 사용된다.

메소드를 오버라이딩할 때는 다음과 같은 규칙에 주의해서 작성해야 한다.

  • 부모 메소드의 선언부(리턴 타입, 메소드 이름, 매개변수)와 동일해야 한다.
  • 접근 제한을 더 강하게 오버라이딩할 수 없다.(e.g. public -> private으로 변경 불가)
  • 새로운 예외를 throws할 수 없다.

자바는 개발자의 실수를 줄여주기 위해 정확히 오버라이딩이 되었는지 체크해주는 @Override 어노테이션(Annotation)을 제공한다.
@Override를 붙이면 컴파일 단계에서 정확히 오버라이딩이 되었는지 체크하고, 문제가 있다면 컴파일 에러를 출력한다.

이것이 자바다 교재가 개정되기 전에는 Chapter 06 - 클래스15절에서 어노테이션에 대해 설명하는데 개정판에서는 Chpater 12 - java.base 모듈의 12절에서 설명한다.

Chpater 12까지는 아직 많이 남았으니 어노테이션(Annotation)에 대해 간단하게만 짚고 넘어가자.

어노테이션(Annotation)

자바 어노테이션(Java Annotation)은 자바 소스 코드에 추가하여 사용할 수 있는 메타데이터의 일종이다.
@기호를 앞에 붙여서 사용하며 JDK 1.5 버전 이상에서 사용 가능하다.
자바 어노테이션은 클래스 파일에 임베디드되어 컴파일러에 의해 생성된 후 자바 가상머신에 포함되어 작동한다.

JDK 1.5 이전 버전에서는 다음과 같이 파일을 관리했다.

하지만 개발자들이 자바 코드는 변경했지만 설정 파일을 변경하지 않는 실수가 자주 발생했고, 설정과 코드가 분리되어 있어 개발의 편의성이 떨어진다는 문제가 있었다.

그래서 채택하게 된 방식이 아래 그림과 같이 하나의 파일에서 코드와 설정을 함께 관리할 수 있는 어노테이션이다.

부모 메소드 호출

메소드를 재정의하면, 부모 메소드는 숨겨지고 자식 메소드만 사용되기 때문에 비록 부모 메소드의 일부만 변경된다 하더라도 중복된 내용을 자식 메소드도 가지고 있어야 한다.

예를 들어 부모 메소드가 100줄의 코드를 가지고 있을 경우, 자식 메소드에서 1줄만 추가하고 싶더라도 100줄의 코드를 자식 메소드에서 다시 작성해야 한다.

이러한 문제는 자식 메소드와 부모 메소드의 공동 작업 처리 기업을 이용하면 쉽게 해결된다.

다음과 같이 super 키워드와 도트 연산자를 사용하면 숨겨진 부모 메소드를 호출할 수 있다.

final 클래스와 final 메소드

필드 선언 시에 final을 붙이면 초기값 설정 후 값을 변경할 수 없다. 그렇다면 클래스와 메소드에 final을 붙이면 어떻게 될까?

final 클래스

클래스를 선언할 때 final 키워드를 class 앞에 붙이면 최종적인 클래스이므로 더 이상 상속할 수 없는 클래스가 된다.
즉, final 클래스는 부모 클래스가 될 수 없어 자식 클래스를 만들 수 없다.

final 메소드

메소드를 선언할 때 final 키워드를 붙이면 이 메소드는 최종적인 메소드이므로 오버라이딩할 수 없다.
즉, 부모 클래스를 상속해서 자식 클래스를 선언할 때, 부모 클래스에 선언된 final 메소드는 자식 클래스에서 재정의할 수 없다.

protected 접근 제한자

protected는 상속과 관련이 있고, public과 deafult의 중간쯤에 해당하는 접근 제한을 한다.

아래 그림을 보자.
default는 접근 제한자가 아니라 접근 제한자가 붙지 않은 상태를 말한다.

접근 제한자제한 대상제한 범위
protected필드, 생성자, 메소드같은 패키지이거나, 자식 객체만 사용 가능

protected는 같은 패키지에서는 default처럼 접근이 가능하나, 다른 패키지에서는 자식 클래스만 접근을 허용한다.
protected는 필드와 생성자 그리고 메소드 선언에 사용될 수 있다.

타입 변환

타입 변환이란 타입을 다른 타입으로 변환하는 것을 말한다. 기본 타입과 마찬가지로 클래스도 타입 변환이 있는데, 클래스의 타입 변환은 상속 관계에 있는 클래스 사이에서 발생한다.

자동 타입 변환

자동 타입 변환(Promotion)은 자동적으로 탕비 변환이 일어나는 것을 말한다.
다음과 같은 조건에서 일어난다.

자식은 부모의 특징과 기능을 상속받기 때문에 부모와 동일하게 취급될 수 있다.
예를 들어 고양이가 동물의 특징과 기능을 상속받았다면 고양이는 동물이다가 성립한다.

그래서 Cat 객체를 생성하고 이것을 Animal 변수에 대입하면 자동 타입 변환이 일어난다.

Cat cat = new Cat();
Animal animal = cat;

// 이렇게도 가능하다
Animal animal = new Cat();

그림으로 보면 다음과 같다.

따라서 두 참조 변수의 == 연산 결과는 true이다.

cat == animal // true

바로 위의 부모가 아니더라도 상속 계층에서 상위 타입이라면 자동 타입 변환이 일어날 수 있다.
다음 그림을 보면서 이해해보자.

부모 타입으로 자동 타입 변환된 이후에는 부모 클래스에 선언된 필드와 메소드만 접근이 가능하다.
비록 변수는 자식 객체를 참조하지만 변수로 접근 가능한 멤버는 부모 클래스 멤버로 한정된다.

그러나 자식 클래스에서 오버라이딩된 메소드가 있다면 해당 메소드가 호출된다.
이것은 다형성(Polymorphism)과 관련 있기 때문에 잘 알아두어야 한다.

강제 타입 변환

자식 타입은 부모 타입으로 자동 변환되지만, 반대로 부모 타입은 자식 타입으로 자동 변환되지 않는다.
대신 다음과 같이 캐스팅 연산자로 강제 타입 변환(Casting)을 할 수 있다.

그렇다고 해서 부모 타입 객체를 무조건 강제 변환할 수 있는 것은 아니다.
자식 객체가 부모 타입으로 변환된 후 다시 자식 타입으로 변환할 때 사용할 수 있다.

Parent parent = new Child(); // 자동 타입 변환
Child child = (Child) parent; // 강제 타입 변환

자식 객체가 부모 타입으로 자동 변환하면 부모 타입에 선언된 필드와 메소드만 사용 가능하다고 배웠다. 따라서 만약 자식 타입에 선언된 필드와 메소드를 꼭 사용해야 한다면 자식 타입으로 강제 타입 변환이 필요하다.

다형성

다형성이란 사용 방법은 동일하지만 실행 결과가 다르게 나오는 성질을 말한다.


객체의 사용 방법이 동일하다는 것은 동일한 메소드를 가지고 있다는 뜻이다. 한국 타이어와 금호 타이어는 모두 타이어를 상속하고 있으므로 두 타이어는 부모의 메소드를 동일하게 가지고 있다고 할 수 있다.
하지만 만약 두 타이어가 부모의 메소드를 오버라이딩하고 있다면 어떨까?
오버라이딩된 내용은 두 타이어가 다르기 때문에 실행 결과가 다르게 나온다.
이것이 바로 다형성이다.

다형성을 구현하기 위해서는 자동 타입 변환과 메소드 재정의가 필요하다. 위 그림에서 한국 타이어와 금호 타이어는 타어 타입(부모)으로 자동 타입 변환이 될 수 있고, 타이어의 메소드를 재정의하고 있다.

객체 타입 확인

매개변수의 다형성에서 실제로 어떤 객체가 매개값으로 제공되었는지 확인하는 방법이 있다.

꼭 매개변수가 아니더라도 변수가 참조하는 객체의 타입을 확인하려고 할 때, instanceof 연산자를 사용할 수 있다.

boolean result = 객체 instanceof 타입;

예시를 보자

public void method(Parent parent) {
    if (parent instanceof Child) {
        Child child = (Child) parent;
    }
}

Java 12부터는 instanceof 연산의 결과가 true일 경우, 우측 타입 변수를 사용할 수 있기 때문에 강제 타입 변환이 필요 없다.

public void method(Parent parent) {
    if (parent instanceof Child child) {
        // child 변수 사용 가능
    }
}

추상 클래스

사전적 의므로 추상(Abstract)은 실체 간에 공통되는 특성을 추출한 것을 말한다.

추상 클래스란?

객체를 생성할 수 있는 클래스를 실체 클래스라고 한다면, 이 클래스들의 공통적인 필드나 메소드를 추출해서 선언한 클래스를 추상 클래스라고 한다.
추상 클래스는 실체 클래스의 부모 역할을 한다. 따라서 실체 클래스는 추상 클래스를 상속해서 공통적인 필드나 메소드를 물려받을 수 있다.

예시를 보자

추상 클래스는 실체 클래스의 공통되는 필드와 메소드를 추출해서 만들었기 때문에 new 연산자를 사용해서 객체를 직접 생성할 수 없다.

Animal animal = new Animal(); // X

추상 클래스는 새로운 실체 클래스를 만들기 위한 부모 클래스로만 사용된다.
즉, 추상 클래스는 extends 뒤에만 올 수 있다.

class Fish extends Animal {
}

추상 클래스 선언

클래스 선언에 abstract 키워드를 붙이면 추상 클래스 선언이 된다.

public abstract class 클래스명 {
    // 필드
    // 생성자
    // 메소드
}

추상 메소드와 재정의

자식 클래스들이 가지고 있는 공통 메소드를 뽑아내어 추상 클래스로 작성할 때, 메소드 선언부만 동일하고 실행 내용은 자식 클래스마다 달라야 하는 경우가 많다.

이런 경우를 위해서 추상 클래스는 다음과 같은 추상 메소드를 선언할 수 있다.

abstract 리턴타입 메소드명(매개변수, ...);

일반 메소드와의 차이점은 abstract 키워드가 붙고, 메소드 실행 내용인 중괄호가 없다는 점이다.

추상 메소드는 자식 클래스에서 반드시 재정의(오버라이딩)해서 실행 내용을 채워야 한다.

봉인된 클래스

기본적으로 final 클래스를 제외한 모든 클래스는 부모 클래스가 될 수 있다. 그러나 Java 15부터는 무분별한 자식 클래스 생성을 방지하기 위해 봉인된(sealed) 클래스가 도입되었다.

다음과 같이 Person의 자식 클래스는 Employee와 Manager만 가능하고, 그 이외는 자식 클래스가 될 수 없도록 Person을 봉인된 클래스로 선언할 수 있다.

public sealed class Person permits Employee, Manager {...}

sealed 키워드를 사용하면 permits 키워드 뒤에 상속 가능한 자식 클래스를 지정해야 한다.
봉인된 Person 클래스를 상속하는 Employee와 Manager는 final 또는 non-sealed 키워드로 다음과 같이 선언하거나, sealed 키워드를 사용해서 또 다른 봉인 클래스로 선언해야 한다.

public final class Employee extends Person {...}
public non-sealed class Manager extends Person {...}

final은 더 이상 상속할 수 없다는 뜻이고, non-sealed는 봉인을 해제한다는 뜻이다.

profile
개발이 재밌어서 하는 Junior Backend Developer

0개의 댓글