본문 바로가기
개념서/Java

[Java] 상속

by 사서T 2022. 6. 10.

상속이란?

   기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것이다. 이때 기존의 클래스를 '부모 클래스', 새로운 클래스를 '자식 클래스'라고 한다. 자식 클래스는 부모 클래스의 멤버를 물려받고, 새로운 내용을 추가하거나 기존 내용을 필요에 따라 수정할 수 있다. 즉, 공통된 코드는 부모 클래스에 존재하며 자식 클래스는 자신만의 고유한 코드를 가진다.

   이처럼 상속을 통해서 클래스를 작성하면 코드의 관리, 추가 및 변경이 용이하다. 이런 특징은 코드의 재사용성을 높이고 중복을 제거하여 프로그램의 생산성을 높이고 효율적인 유지보수를 가능케 한다.

 

<상속의 구현>

class Parent {
    //...
}

//Child 클래스는 Parent 클래스를 상속
class Child extends Parent {
    //...
}

 

<상속 관계의 클래스들의 다른 명칭>

  • 조상 클래스 - 부모 클래스, 상위 클래스, 기반 클래스
  • 자손 클래스 - 자식 클래스, 하위 클래스, 파생된 클래스

   상속은 상위 클래스의 특징은 그대로 물려받으며 자신만의 고유한 특징도 포함하는 형태이다. 아래 분류도를 이용해 상속을 좀 더 쉽게 이해해보자.

 

 

   분류도에 나타나있는 요소들은 모두 클래스이고 자신의 상위에 있는 클래스를 상속받는다고 가정하자(ex 포유류 클래스는 정온동물 클래스를 상속받고 있다.). 포유류, 조류 클래스는 정온동물 클래스를 상속받고 있기 때문에 정온동물 클래스의 특징인 '체온이 일정하다'라는 특징을 가지고 있다. 하지만 포유류 클래스는 '새끼를 낳는다'는 고유한 특징을, 조류 클래스는 '알을 낳는다'는 고유한 특징을 가지고 있다.

   예시를 코드로 표현하면 다음과 같다.

 

class 정온동물 {
    public void method() {
        System.out.println("체온이 일정합니다.");
    }
}

class 포유류 extends 정온동물 {
    public void method2() {
        System.out.println("새끼를 낳다.");
    }
}

class 조류 extends 정온동물 {
    public void method2() {
        System.out.println("알을 낳다.");
    }
}

포유류 포유류 = new 포유류();
조류 조류 = new 조류();

포유류.method(); //출력 : 체온이 일정합니다.
포유류.method2(); //출력 : 새끼를 낳다.

조류.method(); //출력 : 체온이 일정합니다.
조류.method2(); //출력 : 알을 낳다.

 

   위의 코드에서 알 수 있듯이 포유류와 조류 클래스는 정온동물 클래스를 상속받기 때문에 정온동물 클래스의 특징인 method()에 접근이 가능하다. 뿐만아니라 자신만의 고유한 특징인 method2()를 가지기도 한다.

   만약 정온동물 클래스에 특징이 추가되면 어떻게 될까? 포유류와 조류 클래스 문제없이 추가된 특징에 접근이 가능할 것이다. 조류 클래스에 새로운 특징이 추가되면 어떻게 될까? 조류 클래스에 추가된 특징은 조류 클래스만의 고유한 특징이므로 조류 클래스에서만 접근이 가능하며 조류 클래스를 상속받지 않는 한 접근이 불가능하다. 이처럼 상위 클래스의 변경은 자동적으로 하위 클래스에 영향을 주지만, 하위 클래스의 변경은 상위 클래스에 영향을 주지 않는다.

   간단한 예시로 정온동물 클래스에 age라는 멤버를 추가해보자.

 

class 정온동물 {
    int age = 10;

    public void method() {
        System.out.println("체온이 일정합니다.");
    }
}

class 포유류 extends 정온동물 {
    public void method2() {
        System.out.println("새끼를 낳다.");
    }
}

class 조류 extends 정온동물 {
    public void method2() {
        System.out.println("알을 낳다.");
    }
}

포유류 포유류 = new 포유류();
조류 조류 = new 조류();

포유류.age; //10

조류.age; //10

 

   정온동물 클래스에 추가한 age 멤버에 대해 포유류, 조류 클래스 모두 문제없이 접근 가능하다. 물론 포유류와 조류 클래스 각각에서 선언할 수도 있지만 이런 경우 정온동물 클래스에서 선언하고 관리하는 것이 더 효율적이란 것을 알 수 있다. 이처럼 상위 클래스에는 하위 클래스들에 대한 공통적인 멤버를 다루고 하위 클래스는 자신만의 고유한 멤버를 다루는 것이 이후 코드 관리에 있어서도 유용하다.

 

   하위 클래스는 상위 클래스의 모든 멤버를 상속받기 때문에 상위 클래스와 같거나 보다 많은 멤버를 갖게 된다. 따라서 상속을 거듭할수록 멤버의 개수 또한 증가하게 된다. 그래서 표현은 상속이지만 확장의 개념에 더 가깝고 extends 키워드가 사용되는 이유이다. 앞으로 상속은 확장이라고 이해하고 학습을 진행하자.

 

<알아두기>

  • 생성자와 초기화 블럭은 상속되지 않으며 멤버만 상속된다.
  • 하위 클래스의 멤버 개수는 조상 클래스보다 항상 겉거나 많다.
  • 접근 제어자가 private 또는 default인 멤버들은 상속되지만 하위 클래스에서 접근이 제한된다.
  • 하위 클래스의 인스턴스를 생성하면 상위 클래스의 멤버와 하위 클래스의 멤버가 합쳐진 하나의 인스턴스로 생성된다.
  • 다중상속의 경우 상속받은 멤버간의 이름이 같은 경우 구분할 수 없다는 단점이 있기 때문에 자바에선 단일 상속만 허용하였다.

오버라이딩(Overriding)

   오버라이딩은 상위 클래스로부터 상속받은 메서드의 내용을 변경하는 것을 의미하며 상속받은 메서드를 하위 클래스가 자신에 맞게 변경할 때 사용한다. 책에서 소개한 예제를 통해 간단히 알아보자.

 

class Point {
    int x;
    int y;
    
    String getLocation() {
        return "x : " + x + ", y : " + y;
    }
}

class Point3D extends Point {
    int z;
    
    String getLocation() {
        return "x : " + x + ", y : " + y + ", z : " + z;
    }
}

 

   기존 2차원인 Point 클래스를 3차원으로 확장하며 z좌표가 추가되었다. 출력결과도 x, y 좌표만 반환하던 getLocation() 메서드가 z 좌표가 추가된 형태로 반환된다. getLocation() 메서드의 '좌표를 String 형태로 반환한다는 기능'은 같지만 반환되는 정보가 달라진 것이다.

   그렇다면 오버라이딩은 언제 사용할 수 있을까? 오버라이딩의 조건은 다음과 같다.

 

  • 상위 클래스의 메서드와 하위 클래스의 메서드의 선언부(이름, 매개변수, 반환타입)가 같아야 한다.
  • 접근 제어자를 조상 클래스의 메서드보다 좁은 범위로 변경할 수 없다.
  • 예외는 조상 클래스의 메서드보다 많이 선언할 수 없다.
  • 인스턴스 메서드를 static 메서드로 또는 그 반대로 변경할 수 없다.

 

※ static 멤버들의 경우 각 클래스에 종속적이기 때문에 상위 클래스의 static 메서드를 하위 클래스에서 정의하는 경우 오버라이딩이 아닌 별개의 static 메서드를 정의하는 것이다.


Super

   super는 하위 클래스에서 상위 클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조 변수이다. this를 사용해서 구분해도 되지만 상속받은 멤버와의 구분에선 super를 사용하여 명시하는 것이 좋다.

   간단한 예제를 통해 super의 쓰임을 확인해보자.

 

class Parent {
    int age = 30;
}

class Child extends Parent {
    int age = 10;
    
    void method() {
        System.out.println(this.age);
        System.out.println(super.age);
    }
}

Child child = new Child();
child.method();

//this.age : 10
//super.age : 30

 

   이전에 구현했던 Point, Point3D 예제 코드에 super를 이용해보자.

 

class Point {
    int x;
    int y;
    
    String getLocation() {
        return "x : " + x + ", y : " + y;
    }
}

class Point3D extends Point {
    int z;
    
    String getLocation() {
        return super.getLocation() + ", z : " + z;
    }
}

 

   기본 부분을 super.getLocation()으로 바꾸어 getLocation()을 오버라이딩 했음을  명시적으로 확인할 수 있고, Point의 getLocation() 메서드의 내용이 변경되어도 Point3D의 getLocation()에 자동적으로 반영된다.

 

※ 모든 인스턴스 메서드에는 자신이 속한 인스턴스의 주소가 지역변수로 저장되는데, 이것이 참조 변수인 this와 super이다.

super.super 형태의 접근은 불가능하다.

 

   이번엔 상위 클래스 생성자인 super()에 대해 알아보자. super()는 생성자로 상위 클래스의 생성자를 호출하는데 사용된다.

   하위 클래스의 인스턴스를 생성하면, 하위의 멤버와 상위의 멤버가 모두 합쳐진 하나의 인스턴스가 생성된다. 이 때 상위 클래스 멤버의 초기화 작업이 수행되어야 하기 때문에 하위 클래스의 생성자에서 상위 클래스의 생성자가 호출되어야 한다. 하위 클래스의 멤버가 상위 클래스의 멤버를 사용할 수 있기 때문에 하위 클래스 생성자의 첫 줄에서 상위 클래스의 생성자를 호출해야 한다. 간단하게 정리하면 다음과 같다.

 

  • Object 클래스를 제외한 모든 클래스의 생성자 첫 줄에 생성자, this() 또는 super()를 호출해야 한다. 그렇지 않으면 컴파일러가 자동적으로 'super();' 를 생성자의 첫 줄에 삽입한다.

 

   책의 예제를 통해 좀 더 알아보자.

 

class Point {
    int x, y;
    
    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    //...
}

class Point3D extends Point {
    int z;
    
    Point3D(int x, int y, int z) {
        //컴파일 시에 해당 위치에 super();가 삽입됨
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

 

   위의 경우 컴파일 시 super();가 자동적으로 추가되지만 Point 클래스에선 생성자 Point()가 존재하지 않기 때문에 에러가 발생한다. 따라서 Point 클래스에 생성자 Point()를 추가하거나 Point3D 생성자 내에서 'super(x, y);'의 형태로 호출해야 한다. 이처럼 상위 클래스의 멤버 변수는 상위 클래스의 생성자에 의해 초기화되도록 해야 한다.


클래스간의 포함 관계

   상속 이외에도 클래스를 재사용하는 또 다른 방법은 클래스 간에 '포함'관계를 맺어주는 것이다. 포함관계란 한 클래스의 멤버 변수로 다른 클래스 타입의 참조 변수를 선언하는 것이다. 책에서 소개한 간단한 예제를 통해 살펴보자.

 

class Point {
    int x;
    int y;
}

class Circle {
    Point p = new Point();
    int r;
}

 

   이처럼 단위별로 여러 개의 클래스를 작성한 다음, 단위 클래스들을 이용해 새로운 클래스를 간결하고 손쉽게 작성할 수 있다. 이는 코드에 대한 이해가 쉽고 관리에도 용이하다는 장점이 있다.

 

   지금까지 상속과 포함관계에 대해 알아봤다. 그렇다면 클래스를 작성할 때 두 방법 중 어떤 것을 적용해야 할까? 책에서 말하는 요점은 '클래스 간의 관계를 생각해보는 것'이다. 먼저 Circle과 Point의 관계를 보자. Circle은 Point인가? 아니다. Circle은 단순히 Point를 하나의 구성요소로써 포함하고 있는 형태이다. 따라서 Circle은 Point와 포함관계에 있는 것이다 옳다. 그렇다면 포유류는 정온동물인가? 그렇다. 포유류는 정온동물이며 척추동물이기도 한다. 따라서 포유류 클래스는 정온동물 클래스를 상속받는 형태가 옳다. 상속관계와 포함관계 판단 지침을 정리하면 다음과 같다.

 

  • 상속관계 : '~은 ~이다.(is - a)'
  • 포함관계 : '~은 ~을 가지고 있다.(has - a)'

Object 클래스

   Object 클래스는 모든 클래스 최상위에 있는 상위 클래스이다. 다른 클래스를 상속받지 않는 모든 클래스는 자동적으로 Object 클래스를 상속받게 된다. 즉, 컴파일 시에 실제로는 다음과 같이 추가된다.

 

//before
class TestClass {
    //...
}

//after
class TestClass extends Object {
    //...
}

 

   이처럼 모든 클래스의 최상위에는 Object 클래스가 위치하기 때문에 자바의 모든 클래스들은 Object 클래스의 멤버들을 사용할 수 있다. 이것이 toString()이나 equals(Object o)와 같은 메서드를 따로 정의하지 않고도 사용할 수 있는 이유이다.


Q&A

Q. 상속이란?

A. 기존 클래스를 이용하여 새로운 클래스를 작성 및 확장하는 것이다. 코드의 분리로 코드의 관리 및 추가가 용이해지며 재사용성, 중복제거, 효율적인 유지보수가 가능해진다는 장점이 있다.

 

Q. 자바에서 다중상속은 가능한가?

A. 자바는 단일상속만 가능하다. 다중상속에 의해 발생하는 상속된 멤버 변수간의 구분이 어렵다는 단점을 배제하고, 클래스 간의 명확한 관계를 우선시하였기 때문이다.

 

Q. 클래스 간의 포함관계란?

A. 한 클래스가 다른 클래스를 자신의 구성요소 즉, 참조변수로써 가지고 있는 형태이다.

 

Q. 오버라이딩이란?

A. 상위 클래스로부터 상속받은 메서드의 내용을 변경하는 것이다.

 

Q. super란?

A. 하위 클래스에서 상위 클래스로부터 상속받은 멤버를 참조하는데 사용하는 참조 변수이다.

 

Q. 구체화란?

A. 상속을 통해 클래스를 구현, 확장하는 작업이다.


참고자료

  • Java의 정석
  • 스프링 입문을 위한 자바 객체 지향의 원리와 이해

'개념서 > Java' 카테고리의 다른 글

[Java] 제어자  (0) 2022.06.13
[Java] package와 import  (0) 2022.06.10
[Java] 객체지향 프로그래밍 I  (0) 2022.06.06
[Java] 조건문, 반복문, 배열  (0) 2022.05.28
[Java] 연산자  (0) 2022.05.27

댓글