본문 바로가기
Programming/JAVA

[JAVA] 자바 상속, super(), 메서드 오버라이딩

클래스 포스팅을 할 때 간단하게 java의 상속과 다형성에 대해 언급했다. 간단하게 언급한 정도기 때문에 이 포스팅에서 좀 더 깊이 들여다 본다.

 

 

우리는 굳이 자바의 상속을 왜 사용할까? 

만약 자바의 문법만을 학습한 채로 클래스의 관계에 대한 고민 없이 프로그래밍 하게 된다면 엄청난 중복 코드와 지나치게 엄격한 관계 형성으로 유지보수성이 떨어진 Application을 만들게 된다. 

 

상속은 객체지향 프로그래밍의 (A  P.I.E) 중 I(Inheritance)에 해당하는 중요한 개념이다. 모두가 알고 있듯이 실생활에서 상속은 부모의 재산을 물려받는 것이다. 상속을 받으면 한번에 부모가 이룬 모든 것들을 물려받을 수 있다. 

 

 

JAVA의 상속

상속은 기존 클래스의 재산을 다른 클래스에서 재사용하기 위해 사용하는 것이다. 여기서 재산이란 기존 클래스에 있던 멤버(변수와 메서드)를 이야기한다. 따라서 생성자와 초기화 블록은 상속의 대상이 아니다!

 

부모 클래스, 조상 클래스, 상위 클래스, 슈퍼 클래스
상속을 물려주는 클래스

자식 클래스, 자손 클래스, 하위 클래스, 서브 클래스
상속을 받는 클래스

 

아래 예제를 보자. 

//Person.java
public class Person {
    String name;

    public void eat(){
        System.out.println("밥 먹기");
    }
    public void jump(){
        System.out.println("점프하기");
    }
}


//SpiderMan.java
public class SpiderMan extends Person{
    boolean isSpider;

}

 

Person 클래스는 eat과 jump 메서드를 가지고 있다. 이를 SpiderMan 클래스에서 extends 키워드로 Person을 상속받았다. 이제 Person은 부모 클래스, SpiderMan은 자식 클래스가 됐다. 

public class Main {
    public static void main(String[] args) {
        SpiderMan man = new SpiderMan();
        man.eat();
    }
}

//결과: 밥 먹기

SpiderMan이 Person을 잘 상속 받았는지 Main에서 확인해보자. 

SpiderMan 객체를 새로 생성하고 SpiderMan이 가지고 있지 않은 eat을 출력했더니, 잘 출력 된다. 

이로써 SpiderMan이 Person을 상속받은 것을 알 수 있다. 

 

 

SpiderMan이 접근할수 있는 멤버들

 

 

SpiderMan이 접근할수 있는 멤버들의 목록을 보니, equlas가 존재하는게 보인다. quals()를 ctrl과 함께 커서를 가져가보니 'Object'가 보인다. Object는 자바에서 아주 중요한 클래스다. Object는 모든 클래스의 조상 클래스로 클래스 선언부에 extends가 없는 경우는 무조건 extends Object가 생략된 것. Person도 마찬가지다.

 

Person도 Object를 상속받았으므로 Object의 모든 메서드를 물려 받은 것이다. 그래서 equals()를 쓸 수 있다. 

그런 Person을 SpiderMan이 상속 받았으므로 SpiderMan 또한 Object와 Person을 상속받는 셈이 된 것이다. 

 

 

 

JAVA는 단일 상속 지원

우리는 위 예제에서 SpiderMan이 Person을 상속받는 한가지의 예를 들어봤다. 그렇다면, Person을 IronMan도 상속받는다면 어떻게 될까? 

SpiderMan과 IronMan은 같은 Person을 상속받는다. 둘다 Person과 부모-자식 관계니까 형제 관계라도 되지 않을까?

하지만 자바에는 그런 관계가 존재하지 않는다. 또한 여러 클래스를 상속 받을 수도 없다. 이는 프로그램의 복잡도를 줄이기 위함이다. 

 

 

 

메서드 오버라이딩 

 

맛집을 소개하는 프로그램을 보면 "n대째 이어져 오는 맛집"이라는 키워드가 자주 등장한다. 대대로 비법이 이어져 전수된 요리 비법은 계속해서 상속된다. 하지만 이렇게 전통을 고집하더라도 현대의 입맛 변화, 개개인의 선택과 같은 다양한 이유로 약간은 변형이 될 것이다. 

자바에서도 이런 현상이 있는데, 상속을 통해서 부모의 메서드들을 물려 받았지만, 자식 입장에서 더 많은 것을 처리할 수 있는 기능으로 바꿀 수도 있다. 이처럼 클래스에 정의된 기능을 자식 클래스에 적합하게 수정해서 재정의 하는 것을 메서드 오버라이딩이라고 한다.

 

메서드 오버라이딩의 규칙은 다음과 같다.

1. 메서드 이름은 조상 클래스의 메서드 이름과 같아야 한다.
2. 매개변수의 개수, 타입, 순서는 조상 클래스의 메서드와 같아야 한다.
3. 리턴 타입은 조상 클래스의 메서드와 같아야 한다.
4. 접근 제한자는 조상 클래스의 메서드보다 범위가 같거나 넓어야 한다.
5. 조상 클래스의 메서드보다 더 상위의 예외를 던질 수는 없다. 

 

간단히 말해서 메서드 선언부는 동일하게, 구현부만 다시 작성하라는 것이다. 

위의 예제에서 Person의 메서드 중 jump()가 있던 것을 기억할 것이다. Person은 점프를 두 다리로 뛰겠지만, SpiderMan은 엄청난 높이를 뛸 수 있다. 

 

//Person.class
public class Person {
    String name;

    public void eat(){
        System.out.println("밥 먹기");
    }
    public void jump(){
        System.out.println("1m 점프");
    }
}

//SpiderMan.class
public class SpiderMan extends Person{
    boolean isSpider;
    public void jump(){
        System.out.println("1000m 점프");
    }
}

 

이를 다시 Main에서 확인해본다.

 

public class Main {
    public static void main(String[] args) {
        SpiderMan man = new SpiderMan();
        man.jump();
    }
}

//결과
1000m 점프

 

jump()가 Person의 jump()가 아닌 SpiderMan의 jump()가 호출됐다. 잘 생각해보면, 마치 메서드가 자식 클래스에서 상속되면서 덮어 씌워진 느낌이 든다. over writing, 덮어 씌워졌다고 생각하면 편하다. 

 

 

Super()

 

상속을 주는 대상을 슈퍼 클래스라고 칭한 바 있다. this를 통해서 객체 멤버에 접근할 수 있었듯이 super를 통해서 조상의 멤버에 접근할 수가 있다. 

public class SpiderMan extends Person{
    boolean isSpider;
    
    public void jump(){
        super.jump();
    }
}

 

SpiderMan의 jump() 메서드를 부모 클래스의 jump 메서드를 호출하도록 변경했다. 

 

//Main
public class Main {
    public static void main(String[] args) {
        SpiderMan man = new SpiderMan();
        man.jump();
    }
}

//결과: 점프하기

 

그럼 Main을 실행할 때 SpiderMan객체의 jump를 호출해도 Person의 "점프하기"가 출력된 것을 볼 수 있다.

 

지금까지 super를 이용해 부모 클래스의 메서드를 호출한 것을 알아봤다. 

만약 부모 클래스와 자식 클래스에 동일한 변수가 존재한다면? 같은 생성자를 호출하게 될텐데, 이 부분을 super를 사용해 간단하게 부모 생성자를 표현할 수 있다.

 

SpiderMan은 name과 age를 갖는다. Person에는 name만 존재하는데, 어차피 같은 name이니 초기화의 과정은 똑같다.

//Person.class
public class Person {
    String name;

    public Person(String name){
        this.name = name;
    }


    public void eat(){
        System.out.println("밥 먹기");
    }
    public void jump(){
        System.out.println("점프하기");
    }
}


//SpiderMan.class

public class SpiderMan extends Person{
    String name;
    int age;
    public SpiderMan(String name, int age){
        super(name);
        this.age = age;
    }
    public void jump(){
        super.jump();
    }
}

 

두 클래스 모두 name을 멤버 변수로 가지고 있는데, SpiderMan에서는 this.name = name과 같은 문장을 쓰지 않고 super(name)으로 부모의 생성자를 호출하고 있다. 지금이야 변수가 2개뿐이지만, 만약 멤버 변수가 많아진다면 일일히 this.~을 작성해 초기화 하는 것이 비효율적일 수도 있다.