[Java] 객체지향 프로그래밍 - 상속

상속(Inheritance)의 정의와 장점

상속Inheritance은 기존의 클래스를 '재사용'하여 새로운 클래스를 작성하는 기법이에요. 상속을 통해 클래스를 구현하다 보면 적은 양의 코드로 새로운 클래스를 작성할 수 있고 코드를 공통적으로 관리할 수 있어서 코드 추가나 변경이 편해요. 이런 특징은 코드의 재사용성을 높이고, 중복을 제거해서 프로그래밍 생산성과 유지보수에 크게 기여해요.

사용 방식

public ParentApp{
	void parentMethod() {}
}

public class ChildApp extends ParentApp {
	// 해당 클래스는 상속을 받은 클래스의 메서드도 사용할 수 있어요
    // 그렇기에 ParentApp을 확장한다는 의미로 `extends`라는 키워드를 기입해요
    // 'Child extends Parent', `자식은 부모를 확장한다`
	// 	void parentMethod() {}
    void method() {}
}

여기서 ParentApp 그리고 ChildApp은 상속 관계에 있다고 하고, 상속을 해주는 ParentApp부모 클래스, 상속을 받는 ChildApp자식 클래스라고 해요. 다른 방법으로도 표현하는데 아래와 같아요.

부모 클래스 \to 기반Base, 상위super, 조상
자식 클래스 \to 하위sub, 파생Derived, 자손

상속 관계를 그림으로 표시하면 아래와 같아요.

클래스는 타원으로 표현, 화살표는 상속을 받는 클래스 \to 상속하는 클래스로 표현되요. 이렇게 그림으로 표현한 것을 상속 계층도Inheritance Hierarchy라고 해요.

클래스간의 관계 - has-a, is-a

클래스는 상속을 통해 관계를 맺고 기존 코드를 재사용 가능하게 하죠. 이렇게 extends 키워드로 이뤄진 관계를 is-a라고 해요.

관계를 통한 재사용 외에 다른 방법이 존재해요. 예시는 아래와 같아요.

class Point { int x, y; }
class Circle {
	// 상속이 아닌 인스턴스 변수로서 관계를 맺어요
	Point point;
    int radius;
}

상속이 아닌 변수로서 관계를 맺었어요. 통상적으로 알고 있는 개념을 빗대어 설명드리자면, Point는 x, y라는 개념이에요. 그리고 Circle은 x, y 좌표와 반지름의 길이를 가진 것이지, Circle 자체가 Point인 것이 아닌 Point라는 개념을 가지고 있는 것이죠.

물론 Circle extends Point도 기능상 동일하게 동작해요. 하지만 이는 우리가 이해하기 어려운 구조인데다 Point의 개념이 바뀌면 Circle도 동시에 바뀌게 되죠. 둘은 독립적으로 작동해야 해요. 그렇기에 CirclePoint를 '가지고'있어야 하는 방식으로 구현해야죠.

이처럼 클래스가 한 클래스 내에 인스턴스 변수로서 존재하는 관계를 has-a 관계라고 해요. 결론은 클래스를 가지고 문장을 만들었을 때 '~은~이다'라는 문장이 성립하면 is-a, '~은 ~를 가진다'라는 문장이 성립하면 has-a 관계로 구현하면 되요.

단일 상속(Single Inheritance)

C++같은 다른 객체지향 언어는 여러 부모 클래스로부터 상속받는 것이 가능한 다중 상속Multiple Inheritance를 허용하지만 자바는 단일 상속만 허용해요.

public class ABC extend A, B, C	// 이렇게 안되요! 
{}

다중 상속을 허용하면 복합적인 기능을 다룰 수 있는 클래스를 만들 수 있겠지만 관계가 복잡해져 유지보수가 힘들어지는 단점이 존재해요. 제일 큰 문제가 상속 받는 다수의 클래스가 동일한 메서드를 가지게 되면 '모호성'을 띄게 되어 에러가 발생해요.

class A { void method(){} }
class B { void method(){} }
// 누구 method()를 사용해야 하는 걸까요?
class AB extends A, B {} 

물론 이를 해결하는 방법은 이름 중복을 제거하면 되겠죠. 하지만 이는 정교하게 짜여진 코드에서 이름을 바꾸는 것 자체가 매우 힘든 일이에요. 그래서 자바는 아예 다중 상속을 막아서 보다 신뢰할 수 있는 코드를 작성할 수 있게 만들었어요.

물론 다중 상속과 비슷하게 하는 문법도 존재하긴 합니다. 추후에 다루도록 할게요 ㅎㅎ.

Object 클래스 - 모든 클래스의 조상

Object 클래스는 모든 클래스 상속 계층도의 최상위에 있는 조상 클래스에요. 다른 클래스로부터 상속 받지 않는 모든 클래스는 '자동적으로' Object 클래스로부터 상속받게 해요.

클래스를 선언하게 되면 해당 클래스에서 toString(), getHashCode() 같은 기능들을 사용할 수 있죠. Object 클래스를 상속했기 때문에 이것이 가능한 거에요. 총 11개의 기본 메서드가 제공되요.

class App // extends Object // 자바가 알아서 상속시켜줘요

오버라이딩(Overriding)

오버라이딩Overriding은 부모 클래스로부터 상속받은 메서드 내용을 변경하는 작업을 말해요. 상속받은 메서드를 그대로 사용하기도 하지만, 자식 클래스에 맞게 변경할 때 유용해요.

class Father{
	void introduce() { "나는 아빠다" } 
}

class Son extends Father {
	@Override	// 이렇게 명시해주면 부모 클래스에 
    			// 해당 메서드가 없으면 컴파일 에러 처리해요
                // 이름이 틀려서 상속이 잘못되는 것을 방지해요
    void introduce() { "나는 아들이다" }
}

오버라이딩 조건

오버라이딩은 메서드의 내용만을 새로 작성하는 것이라 메서드의 선언부는 조상의 것과 완전히 일치해야 해요. 성립 조건은 아래와 같아요.

  1. 이름이 같아야 해요
  2. 매개변수가 같아야 해요
  3. 반환 타입이 같아야 해요

그리고 JDK 1.5부터 공변 반환 타입Covariant return type이 추가되어 반환 타입을 자식 클래스의 타입으로 변경하는 것이 가능도록 조건을 완화 했어요.

class Parent {
 //it contain data member and data method
}

class Child extends Parent {
	// 반환형이 부모지만, 자식 클래스도 반환이 가능해요
    public Parent methodName() {
       return new Parent();	// 이걸 쓰거나
       또는
       return Child();		// 이걸 써도 되요
    }
}

해당 조건을 한마디로 요약하면 선언부는 완전히 일치해야한다는 의미에요. 다만 접근 제어자access modifier와 예외exception는 제한된 조건 하에 다른게 적용할 수 있어요.

1. 접근 제어자는 조상 클래스의 메서드보다 좁을 수 없어요
부모에서 정의된 메서드의 제어자가 protected라면 이를 오버라이딩하는 자식은 protectedpublic로만 선언할 수 있어요. private는 안되요.

2. 부모 클래스의 메서드보다 많은 수의 예외는 선언할 수 없어요
코드로 설명하는게 편할 것 같네요.

class Parent{
	void method() throws IOException, SQLException;
}

class Child extend Parent{
	// 정상! 예외 수가 적네요
	void method() throws IOException;
	// 정상! 예외 수가 적네요
	void method() throws IOException, SQLException;
    // 비정상! 예외 수가 많아요
	void method() throws IOException, SQLException, TestException;
}

그런데, 이렇게 단순하게 예외의 개수만 따지는 것이 아니에요. 예를 들면 Exception이라는 예외 클래스는 모든 예외들의 최상위 부모에요. 그래서 위에 부모보다 적거나 같은 수의 예외를 던지더라도 Exception는 모든 예외에 대응하기 때문에 '예외의 개수는 적거나 같아야 한다'는 조건을 만족시키지 못해 컴파일 에러가 발생해요.

class Parent{
	void method() throws IOException, SQLException;
}

class Child extend Parent{
	// 비정상!
	void method() throws Exception;
}

3. 인스턴스 메서드를 클래스 메서드로 또는 반대로 변경할 수 없어요
static void method()였던게 상속했다고 void method()로 바꿀 수 없고 반대도 마찬가지란 뜻이에요.

오버로딩 vs. 오버라이딩

이름 때문에 혼동하기 쉽지만 차이는 명백해요. 정리하자면,

오버로딩Overloading : 기존에 없는 새로운 메서드를 정의(new)
오버라이딩Overriding : 상속받은 메서드의 내용을 변경(change)

super

자식 클래스에서 조상 클래스로부터 상속받은 메서드나 변수를 참조하는데 사용되는 참조 변수에요. 멤버 변수와 지역 변수를 구분할 때 this를 사용하듯이 super를 통해 부모의 변수와 메서드를 사용할 수 있어요.

class A {
	int a = 10;
	void method() { a 출력 }
}

class B extend A {
	int a = 20;
	@Override
	void method() { b 출력 }
    void useMethod() {
    	super.method(); // 10 출력
        this.method(); or method(); // 20 출력
    }
}

super() - 부모 클래스 생성자

this()와 마찬가지로 super() 역시 생성자에요. 차이가 있다면 상속받은 부모의 생성자를 호출하는데 사용되요. this()와 마찬가지로 생성자 로직 내 첫 줄에 선언되어야 해요(이유는 객체지향 프로그래밍 - 생성자를 참고해주세요!).

그리고 모든 자식 클래스 생성자는 자신의 다른 생성자 또는 부모 생성자를 호출해줘야 해요. 부모 클래스 생성자를 선언해서 올바른 초기화를 유도하기 위함이에요. 물론 자바가 이를 강제하는 것도 있고요.

그렇기에 자식 클래스 생성자 호출은 상속 관계를 거슬러 올라 최상위 클래스인 Object 클래스의 생성자를 먼저 수행하면서 타고 내려가 생성자 로직을 수행해요.

class A {
	A(int a) {}
}

class B {
	B(int a) {
    	super(a);	// 없으면 컴파일 에러!
    }
}

좋은 웹페이지 즐겨찾기