본문 바로가기

Java/Chapter 14. 클레스 상속 1 : 상속의 기본

[Java] 14.01 - 상속의 기본 문법 이해

들어가며

 상속의 적절한 활용 방법은 한두
문장으로 가볍게 설명할 수 있는
내용이 아니다.

 그리고 이에 대한 설명을 듣기에
앞서, 상속에 대한 문법적이 이해가
선행되어야 한다.

 

상속에 대한 매우 치명적인 오해

 상속의 이유와 목적을 물어보면
다음과 같이 답을 하는 경우를
매우 흔하게 본다.

"상속은 코드의 재활용을 위한 문법이다."

 그러나 객체지향 기반의 개발 경험이
풍부한 개발자나,

 대학원엣서 컴퓨터공학을 전공한
이들에게 질문을 하면 다음의
내용으로 답을 한다.

"연관된 일련의 클래스들에 대해 공통적인 규약을 정의할 수 있다."

 위의 답변은 매우 모범적인 답변이긴
하지만

 지금 이해할 수 있는 내용은 아니다.

 그러나 이 문장의 이해를 목표로
상속을 공부해야 하며,

 Chapter 16까지 공부하는 과정에서
이해할 수 있을 것으로 기대한다.

 그러나 상속에 들어가기에 앞서
머릿속에서 각인시키고 싶은
내용이 있다.

"상속은 코드의 재활용을 목적으로 사용하는 문법이 아니다."

 만약에 재활용을 목적으로 상속을
사용할 경우

 무의미하게 코드가 복잡해지고,

 기대와 달리 코드를 재활용하지
못하는 상황을 쉽게 경험하게 될
것이다.

 코드의 재활용은 프로그래머라면
누구나 바라는 일이다.

 그리고 이를 목적으로 20년 넘게
학문적으로 연구가 진행 중이다.

 그러나 전자부품이나 기계부품처럼,

 일부를 뜯어서 다른 장치에 사용하는
것은 아직은 요원한 일이며,

 소프트웨어의 특성상 이러한 형태의
재활용이 아닌

 다른 형태의 재활용이 시도되고
연구되고 있다.

 

상속의 가장 기본적인 특성

상속을 단순하게 설명하면,

 기존의 클래스에 메소드와 변수를
추가하여 새로운 클래스를 정의하는
것이 상속이다.

 예를 들어서 다음의 클래스가
정의되어 있다고 가정해보자.

class Man {
    String name;
    
    public void tellYourName() {
        System.out.println("My name is " + name);
    }
}

 이때 위의 클래스를 상속하여
(물려받아서)

 다음과 같이 새로운 클래스를
정의할 수 있다.

 참고로 키워드 extends는 상속을
의미하는 키워드이다.

 즉 extends Man은 Man 클래스를
상속한다는 의미이다.

class BusinessMan extends Man {     // Man을 상속하는 BusinessMan 클래스
    String company;
    String position;

    public void tellYourInfo() {
        System.out.println("My company is " + company);
        System.out.println("My position is " + position);
        tellYourName();     // Man 클래스를 상속했기 때문에 호출 가능
    }
}

 그리고 이렇게 Man 클래스를
상속하는 BusinessMan 클래스의
인스턴스를 생성하면,

 다음 형태의 인스턴스가 생성된다.

BusinessMan man = new BusinessMan();

[그림 14-1]

 BusinessMan 인스턴스에는
Man 클래스의 변수와 메소드가
존재한다.

 이는 BusinessMan 클래스가
Man 클래스를 상속했기 때문이다.

 그래서 BusinessMan 클래스
내에서는 tellYourName 메소드를
호출할 수 있다.


 지금 설명한 내용이 '상속의
기본적인 특성'이다.

 그리고 상속을 하는 클래스와
상속의 대상이 되는 클래스를
가리켜

 각각 다음과 같이 부른다.

  • 상속의 대상이 되는 클래스 - 상위 클래스, 기초 클래스, 부모 클래스
    ex) Man 클래스
  • 상속을 하는 클래스 - 하위 클래스, 유도 클래스, 자식 클래스
    ex) BusinessMan 클래스

 이러한 상속 관계는 UML 기호라는
것으로 다음과 같이 표현을 하는데,

 이 표현은 본서를 포함하여 매우
널리 사용되므로 기억해 두어야 한다.

[그림14-2]

  위 그림에서 중요한 것은

 상위 클래스와 하위 클래스의
위치가 아니라

 화살표의 촉이 향하는 방향이다.

 이 촉은 하위 클래스에서 상위 클래스로
향하게 되어 있다.

 

상속과 생성자

 이번에는 앞서 정의한 클래스에
생성자를 정의하고자 한다.

 먼저 Man 클래스이다.

 기본적으로 인스턴스 변수의 초기화를
위한 생성자를 정의하였다.

class Man {
    String name;
    
    public Man (String name) {
        this.name = name;
    }

    public void tellYourInfo() {
        System.out.println("My name is " + name);
    }
}

 이어서 위의 클래스를 상속하는
BusinessMan 클래스를 소개한다.

 마찬가지로 인스턴스 변수의
초기화를 위한 기본적인 생성자를
정의하였다.

class BusinessMan extends Man {
    String company;
    String position;

    public BusinessMan (String company, String position) {
        this.company = company;
        this.position = position;
    }

    public void tellYourInfo() {
        System.out.println("My company is " + company);
        System.out.println("My position is " + position);
        tellYourName();
    }
}

 상위 클래스인 Man, 그리고 이를
상속하는 하위 클래스인 BusinessMan에도
적절한 생성자가 정의되어 있다.

 그런데 문제가 하나 있다.

 BusinessMan의 인스턴스가 생성되면,
그 안에는 Man 클래스의 상속으로 인해

 다음의 변수도 존재하게 되는데,
이를 초기화하지 않는 문제가 발생한다.

String name;
// 상속으로 인해 BusinessMan 클래스의 멤버가 된 변수

 따라서 다음 예제에서 보이는 바와

 같이 BusinessMan 생성자에서 위의
변수도 초기화를 해줘야 한다.

class Man {
    String name;

    public void tellYourName() {
        System.out.println("My name is " + name);
    }
}

class BusinessMan extends Man {
    String company;
    String position;

    public BusinessMan (String name, String company, String position) {
        // 상위 클래스 Man의 멤버 초기화
        this.name = name;

        // 클래스 BusinessMan의 멤버 초기화
        this.company = company;
        this.position = position;
    }

    public void tellYourInfo() {
        System.out.println("My company is " + company);
        System.out.println("My position is " + position);
        tellYourName();
    }
}

class MyBusinessMan {
    public static void main (String[] arg) {
        BusinessMan man = new BusinessMan("Yoon", "Hybrid ELD", "staff Eng.");
        man.tellYourInfo();
    }
}

/* 실행 결과
My company is Hybrid ELD
My position is staff Eng.
My name is Yoon
*/

 위 예제의 Man 클래스에는 생성자가
정의되어 있지 않다.

 대신에 BusinessMan에서 Man의
멤버까지 초기화를 해주고 있다.

 따라서 코드의 흐름상 문제는 없다.

 그러나 적절한 생성자의 정의 형태는
아니다.

 그렇다면 어떠한 것이 상속 관계에
있는 클래스의 적절한 생성자 정의인가?

 이와 관련하여 다음 예제를 관찰하자.

 그리고 상속 관계에 있는 생성자의
호출 관계를 파악하자.

class SuperCLS {
    public SuperCLS() {     // 생성자
        System.out.println("I'm Super Class");
    }
}

class SubCLS extends SuperCLS {
    public SubCLS() {       // 생성자
        System.out.println("I'm Sub Class");
    }
}

class SuperSubCon {
    public static void main (String[] arg) {
        new SubCLS();
    }
}

/* 실행 결과
I'm Super Class
I'm Sub Class
*/

 위의 실행 결과는 다음 사실을 알려준다.

"하위 클래스의 인스턴스 생성 시 상위 클래스, 하위 클래스의 생성자 모두 호출된다."
"하위 클래스의 인스턴스 생성 시 상위 클래스의 생성자가 먼저 호출된다."

 즉 다음과 같이 하위 클래스의 생성자를
이해하면 된다.

아래 코드에서 SuperCLS()는 SuperCLS의 생성자가 이 순간에 호출됨을 의미한다.
class SubCLS extends SuperCLS {
    public SubCLS() {       // 생성자
        SuperCLS();         // 상위 클래스의 생성자가 이 순간에 호출됨을 의미함.
        System.out.println("I'm Sub Class");
    }
}

 위의 코드에서 보이듯이 하위 클래스의
생성자에서

 상위 클래스의 생성자를 명시적으로
호출하지 않으면,

 인자를 받지 않는 생성자가 자동으로
호출된다.

 그렇다면 상위 클래스의 생성자는
어떻게 명시적으로 호출하는가?

 키워드 super를 사용하면 되는데,
이와 관련하여 다음 예제를 보자.

class SuperCLS {
    public SuperCLS() {
        System.out.println("Con: SuperCLS()");
    }
    public SuperCLS(int i) {
        System.out.println("Con: SuperCLS(int i)");
    }
    public SuperCLS(int i, int j) {
        System.out.println("Con: SuperCLS(int i, int j)");
    }
}

class SubCLS extends SuperCLS {
    public SubCLS() {
        System.out.println("Con: SubCLS()");
    }
    public SubCLS(int i) {
        super(i);
        System.out.println("Con: SubCLS(int i)");
    }
    public SubCLS(int i, int j) {
        super(i, j);
        System.out.println("Con: SubCLS(int i, int j)");
    }
}

class SuperSubCon {
    public static void main (String[] arg) {
        System.out.println("1. ");
        new SubCLS();
        System.out.println();

        System.out.println("2. ");
        new SubCLS(1);
        System.out.println();

        System.out.println("3. ");
        new SubCLS(1, 2);
        System.out.println();
    }
}

/* 실행 결과
1. 
Con: SuperCLS()
Con: SubCLS()

2. 
Con: SuperCLS(int i)
Con: SubCLS(int i)

3. 
Con: SuperCLS(int i, int j)
Con: SubCLS(int i, int j)
*/

 위 예제의 SubCLS 클래스에 정의된
다음 생성자를 보자.

public SubCLS(int i) {
    super(i);	// 상위 클래스의 생성자 호출을 의미함.
    System.out.println("Con: SubCLS(int i)");
}

 이렇듯 생성자 내에서 사용된 키워드
super는 '상위 클래스의 생성자 호출'을
의미한다.

 그리고 다음과 같은 방식으로 호출할
상위 클래스의 생성자를 지정한다.

super(1);       // 1을 인자로 전달받을 수 있는 생성자 호출

super(1, 2);    // 1과 2를 인자로 전달받을 수 있는 생성자 호출

 상위 클래스의 생성자는 하위 클래스
생성자의 몸체 부분에 앞서 실행되어야
한다.

 그래서 super를 이용한 상위 클래스의
생성자 호출문은 생성자의 첫 문장으로
등장해야 한다.

 만약에 다음과 같이 생성자를 정의하면
컴파일 오류가 발생한다.

public SubCLS(int i) {
    System.out.println("Con: SubCLS(int i)");
    super(i);    // 이 위치에 있으면 컴파일 오류 발생
}

 또한 하위 클래스의 생성자에서

 다음과 같이 상위 클래스의 생성자
호출을 생략하면,

public SubCLS() {
    System.out.println("Con: SubCLS()");
}

 다음과 같이 인자를 받지 않는
상위 클래스의 생성자 호출문이
자동으로 삽입된다.

public SubCLS() {
    super();    // 자동으로 삽입이 된 문장
    System.out.println("Con: SubCLS()");
}

 

상속 관계에 있는 두 클래스의 적절한 생성자 정의

 자바는 상속 관계에 있을지라도,

 상위 클래스의 멤버는 상위 클래스의
생성자를 통해서 초기화 하도록 유도하고
있다.

하위 클래스의 인스턴스 생성 과정에서 상위 클래스의 생성자가 호출되는 이유가 여기에 있다.

 그럼 앞서 제시한 예제 MyBusinessMan에서
생성자를 적절히 정의한 다음 결과를 보자.

class Man {
    String name;
    
    public Man(String name) {
        this.name = name;
    }

    public void tellYourName() {
        System.out.println("My name is " + name);
    }
}

class BusinessMan extends Man {     // Man을 상속하는 BusinessMan 클래스
    String company;
    String position;

    public BusinessMan (String name, String company, String position) {
        super(name);    // 상위 클래스의 생성자 호출
        this.company = company;
        this.position = position;
    }

    public void tellYourInfo() {
        System.out.println("My company is " + company);
        System.out.println("My position is " + position);
        tellYourName();     // Man 클래스를 상속했기 때문에 호출 가능
    }
}

class MyBusinessMan {
    public static void main (String[] arg) {
        BusinessMan man = new BusinessMan("Yoon", "Hybrid ELD", "staff Eng.");
        man.tellYourInfo();
    }
}

/* 실행 결과
My company is Hybrid ELD
My position is staff Eng.
My name is Yoon
*/

 위의 예제에서는 상속 관계에 있는
두 클래스의 적절한 생성자 정의 모델을
보이고 있다.

 결론은 간단하다.

 상속 관계에 있을지라도 인스턴스
변수는

 각 클래스의 생성자를 통해서
초기화해야 한다는 것이다.

참고 - 접근 수준 지시자 protected

Chapter 09의 09-2에서 4가지의 '접근 수준 지시자'를 소개하였다. 당시에 상속에 대한 간략한 소개와 더불어 protected 선언에 대한 설명을 진행하였다. 만약 저자의 권유대로 그 부분을 건너뛰었다면 이제 그 부분을 공부할 차례이다.

 

단일 상속만을 지원하는 자바

 자바는 프로그램이 과도하게 복잡해지는
것을 막기 위해 단일 상속만을 지원한다.

 이는 다음과 같이 하나의 클래스가
상속할 수 있는 클래스의 수가

 최대 하나라는 것을 의미한다.

class AAA {....}

class BBB extends AAA {....}

 그러나 다음과 같이,

 상속의 깊이를 더하는 것은 얼마든지
가능하다.

class AAA {....}

class BBB extends AAA {....}

class CCC extends BBB {....}

 둘 이상의 클래스 상속이 가능한,
다중 상속을 지원하는 프로그래밍
언어도 있다.

 그러나 해당 언어로 프로그래밍을
하는 경우에도

 다중 상속을 하는 클래스를 설계하는
일은 극히 드물다.

 대부분의 경우 다중 상속은 득보다
실이 더 많이 때문이다.


참고 및 출처

윤성우의 열혈 Java 프로그래밍
국내도서
저자 : 윤성우
출판 : 오렌지미디어 2017.07.05
상세보기