본문 바로가기

Java/Java Language

[Java] 상속 (Inheritance)

클래스간의 상속 관계

상속의 집합 관계와 타입 선언

클래스는 멤버 (멤버 변수, 메서드)의 집합이다. 자식 클래스는 부모 클래스의 모든 멤버를 상속(Inheritance) 받는다. 그리므로 부모 클래스는 자식 클래스의 집합 관계로 표현될 수 있다. 

 

위의 집합 관계가 형성되기 때문에, 자식 클래스(Child)는 부모 클래스(Parent) 타입으로 선언할 수 있다. 정확히는 자식 클래스는 부모 클래스에게 모든 멤버를 상속 받았기 때문에, 자식 클래스가 부모 클래스를 포함한 더 큰 범위의 타입이다. 그리하여 아래의 코드 중 Parent parent = new Child(1, 2, 3);에서 자식 클래스(Child)의 인스턴스를 생성하는데, 부모 타입(Parent)으로 선언이 가능하다. 이는 인스턴스의 실제 Child 클래스 타입 이지만, 인스턴스 참조 변수 parent는 Parent 타입으로 선언되어 부모 클래스의 멤버에만 접근할 수 있다.

public class Parent {  
    int x;  
    public int y;  
    private int z;  

    Parent(int x, int y, int z) {  
        this.x = x;  
        this.y = y;  
        this.z = z;  
    }

    void a() {  
        System.out.println("Call Parent Method a");  
    }  

    private void b() {  
        System.out.println("Call Parent Method b");  
    }  

    public void c() {  
        System.out.println("Call Parent Method c");  
    }  

    public static void d() {  
        System.out.println("Static method in Parent c");  
    }  
}

class Child extends Parent {  
    private String str;

    Child(int x, int y, int z) {  
        super(x, y, z);  
    }

    public void e() {  
        System.out.println("Parent fields " + x + " " + y);
        System.out.println("Call Child Method e");   
    }  
}

public class Main {
    public static void main(String[] args) {
        Parent parent = new Child(1, 2, 3); // 
        Parent.d(); // Parent 클래스의 static method 호출  
        parent.c(); // Parent 클래스의 method 호출  
        parent.e(); // Child 클래스의 method 참조 불가  

        Child child = new Child(1, 2, 3);  
        Child.d(); // Parent 클래스의 static method 호출  
        child.c(); // Parent 클래스의 method 호출  
        child.e(); // Child 클래스의 method 호출
    }
}

 

상속과 접근 범위

자식 클래스는 부모 클래스의 멤버를 추가하여, 확장하는 개념이므로 extends 와 일맥 상통한다. 그러나 초기화 설정을 위한 생성자와 초기화 블럭은 멤버가 아니기 때문에,생성자(Constructor)와 초기화 블럭(Initialize Block)은 상속되지 않는다. 그리하여 위의 코드에서처럼 super 키워드를 사용하여 부모 클래스의 생성자 호출이 필요하다.

 

그리고 접근 제어자(Access Modifier)가 private이나 default이면 상속은 받지만, 하위 클래스에서 부모 클래스로의 접근이 제한될 수 있다. private이면 같은 패키지 내에서도 접근이 불가하고, default이면 같은 패키지 내에서만 접근이 가능하다. 위의 코드의 Parent 클래스의 인스턴스 변수 z와 메서드 b()는 Child 클래스에서 접근이 불가하다. 그리고 Child 클래스가 Parent 클래스와 다른 package에 있을 경우, 인스턴스 변수 x와 메서드 a()에도 접근이 불가하다.

 

상속받은 클래스의 인스턴스

클래스의 인스턴스는 상위 클래스의 멤버와 해당 클래스의 멤버가 포함된 하나의 인스턴스로 생성된다. 그리하여 클래스의 인스턴스 생성를 생성할때, 상위 클래스의 인스턴스를 생성하지 않고 상위 클래스의 멤버들을 사용할 수 있다. 예시로, 위의 코드에서 Child 클래스가 Parent 클래스와 다른 package에 있을 경우, child 인스턴스 참조 변수가 가리키는 인스턴스의 멤버 집합은 다음과 같다. 

 

상속의 장점

클래스 Child1과 Child2가 Parent 클래스를 상속받으면, 자식 클래스는 부모 클래스에 의존성이 생겨 다음의 상속계층도를 나타낸다. 

 

위의 두 자식 클래스(Child1, Child2)에 공통적인 멤버의 추가가 필요할 경우에, 부모 클래스에 추가하여 상속받는 것이 코드 중복과 오류 발생을 최소화한다. 

 

코드 중복이 증가할수록 유지보수가 어려워지고 일관성이 깨질수 있게된다. 이로 인해 프로그램이 어떨 때는 잘 동작하지만, 어떨 때는 오작동이 일어날 수 있다. 코드 수정 이후, 중복된 코드 중에서 올바르게 변경되지 않은 곳이 있을 확률이 아주 높다.

 

이렇게 상위 클래스만 변경해도 모든 하위 클래스 상속받은 멤버도 관리되므로, 상속이란 개념은 프로그램의 유지보수 향상에 대해 중요한 역할을 한다.


클래스간의 포함 관계

다음 Point와 Circle 클래스가 있다. Circle 클래스는 원점의 x, y 좌표를 갖고 Point 클래스는 x, y 좌료를 갖는다. 그러므로 Circle 클래스는 Point 클래스의 멤버를 가지고 있으므로, Point 클래스를 포함시킬 수 있다.

class Point {
    int x; // x 좌표
    int y; // y 좌표
}

class Circle {
    int x; // 원점의 x 좌표
    int y; // 원점의 y 좌표
    int r; // 반지름
}

 

클래스를 포함시키는 것은, 다른 클래스의 인스턴스를 클래스 내의 멤버 변수로 포함시키는 것을 의미한다. 아래의 Circle 클래스는 Point 객체를 멤버 변수로 포함하고 있다. 두 클래스는 객체 지향 프로그래밍에서 상속 이외에도 클래스를 재사용하는 포함(composition) 관계를 나타낸다.

class Circle {
    Point centerPoint = new Point(); // 원점
    int r; // 반지름
}

클래스간의 관계 설정

클래스를 작성하는데 상속관계와 포함관계를 맺어 줄 것인지 결정하는데 헷갈릴 수가 있다. 아래의 예시에서 Circle 클래스는 Point 클래스를 상속 받아도 되고, 포함해도 된다.

class Point {  
    int x; // x 좌표  
    int y; // y 좌표  

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

class Circle {
    int x; // 원점의 x 좌표
    int y; // 원점의 y 좌표
    int r; // 반지름

    Circle(int x, int y, int z) {  
        this.x = x;  
        this.y = y;  
        this.z = z;
    } 
}

class Shape {  
    String color;  
    
    Shape(String color) {
        this.color = color;
    }

    // 도형의 면적을 계산하는 메서드  
    double area() {  
        return 0;  
    }  
}

 

프로그램이 커지면서 여러 클래스들이 관계 설정을 하게되기 때문에, 명확한 관계 설정을 해주어야 보다 단순하게 클래스를 설계할 수 있다.

아래처럼 클래스간의 is a와 has a 문장을 사용하여 관계를 명확히 파악 할 수 있다.

 

상속 관계(Inheritance): 원(Circle)은 점(Point) 이다. - Circle is a Point

포함 관계(Composition): 원(Circle)은 점(Point)을 가지고 있다. - Circle has a Point

 

원(Circle)과 점(Point)의 관계에서는 포함 관계가 옳은 관계이다.

class Circle {  
    Point centerPoint; // 원점  
    int r; // 반지름  

    Circle() {  
        this(new Point(0, 0), 0);  
    }  

    Circle(Point centerPoint, int r) {  
        this.centerPoint = centerPoint;  
        this.r = r;  
    }  
}

 

상속 관계(Inheritance): 원(Circle)은 도형(Shape) 이다. - Circle is a Shape

포함 관계(Composition): 원(Circle)은 도형(Shape)을 가지고 있다. - Circle is a Shape

 

원(Circle)과 도형(Shape)의 관계에서는 상속 관계가 옳은 관계이다.

class Circle extends Shape {
    Point centerPoint; // 원점
    int r; // 반지름

    Circle() {
        this(new Point(0, 0), 0, "black");
    }

    Circle(Point centerPoint, int r, String color) {
        super(color);
        this.centerPoint = centerPoint;
        this.r = r;
    }

    @Override
    double area() {
        return Math.PI * r * r;
    }
}

단일 상속 (Single Inheritance)

다중 상속은 한 클래스가 여러 부모 클래스를 상속받을 수 있다. C++은 다중 상속을 직접 지원하는 대표적인 언어이다. Java는 클래스에 대한 단일 상속만을 지원한다. 허나, 인터페이스를 통해 다중 상속과 유사한 기능을 제공한다.

 

클래스의 다중 상속을 허용하면 여러 클래스로부터 상속받을 수가 있기 때문에, 복합적인 기능을 가진 클래스를 쉽게 작성이 가능하다. 그러나 서로 다른 클래스로부터 상속받은 멤버간의 이름이 동일하면 구별할 수가 없다. 이럴 경우, 상위 계층의 멤버명을 변경해야한다.

 

이처럼 다중 상속의 문제점과 더불어 몇 가지 중요한 문제점이 발생한다. 이러한 문제점을 통해 자바에서 다중 상속을 왜 지원하지 않는지 이해할 수 있다.

 

  1. 단일 책임 원칙 (Single Responsibility Principle, SRP) 
    단일 책임 원칙은 하나의 클래스는 하나의 책임만 가져야 한다는 원칙이다. 다중 상속을 사용하면 클래스가 여러 부모 클래스로부터 다양한 기능을 상속받아, 클래스의 책임이 모호해지고 단일 책임 원칙을 위반할 가능성이 높아진다.

  2. 상속의 복잡성 
    다중 상속은 클래스 간의 관계를 복잡하게 만들어 이해하기 어려워진다. 다이아몬드 문제(Diamond Problem)가 대표적인 예이다. 다이아몬드 문제는 두 부모 클래스가 같은 조상 클래스를 상속받는 경우, 자식 클래스는 그 조상 클래스를 두 번 상속받는 셈이 되는 것이다. 이는 어느 부모 클래스의 멤버를 상속받아야 하는지 모호해지는 문제가 발생한다.

  3. 유지보수의 어려움
    다중 상속을 사용하면 클래스 간의 관계가 복잡해져 코드의 유지보수가 어려워진다. 클래스가 여러 부모 클래스를 상속받으면, 부모 클래스의 변경이 자식 클래스에 예기치 않은 영향을 미칠 수 있어, 변경 사항을 추적하고 테스트하는 데 어려움을 겪을 수 있다.

 


챰고 자료

  • 자바의 정석 (남궁성 지음)

'Java > Java Language' 카테고리의 다른 글

[Java] Package와 Import  (0) 2024.06.19
[Java] 오버라이딩 (Overriding)  (0) 2024.06.18
[Java] 변수의 초기화  (0) 2024.06.13
[Java] 생성자 (Constructor)  (0) 2024.06.13
[Java] 오버로딩 (Overloading)  (0) 2024.06.13