관리 메뉴

나만을 위한 블로그

[JAVA] 상속이란? 오버라이딩이란? 본문

JAVA

[JAVA] 상속이란? 오버라이딩이란?

참깨빵위에참깨빵 2020. 12. 19. 23:46
728x90
반응형

이전 포스팅에서 클래스에 관한 포스팅을 썼기 때문에, 이번 포스팅에선 상속에 대해서 기록하려 한다.

 

먼저 상속의 사전적 정의부터 확인해보자.

 

상속 : 뒤를 이음, 일정 친족관계가 있는 사람 사이에서 한 사람이 사망한 후에 다른 사람에게 재산에 관한 권리, 의무 일체를 이어주거나, 다른 사람이 사망한 사람으로부터 그 권리와 의무 일체를 이어받는 일

 

뒤를 잇는다는 뜻, 뭔가를 물려받는다는 뉘앙스의 뜻이 있다.

흔히 드라마를 보면 돈이 많은 어떤 기업의 할아버지가 자기 자식/손자에게 재산을 물려준다는 식으로 말하는 걸 볼 수 있다. 이 때 할아버지가 자식/손자에게 재산을 물려준다는 것 자체를 '상속해준다' 라고 말한다.

 

그럼 자바에서 쓰는 상속도 이 상속의 뜻과 크게 다르지는 않을 것이다.

영문/한글 위키백과에선 상속을 아래와 같이 말하고 있다.

 

객체 지향 프로그래밍에서 상속은 유사한 구현을 유지하면서 다른 객체(프로토타입 기반 상속) 또는 클래스(클래스 기반 상속)를 기반으로 객체 또는 클래스를 기반으로 하는 매커니즘이다. 또한 슈퍼 클래스 또는 기본 클래스와 같은 기존 클래스에서 새 클래스(하위 클래스)를 파생한 다음, 클래스 계층 구조로 형성하는 것으로 정의된다.
대부분의 클래스 기반 객체 지향 언어에서 상속을 통해 생성된 객체인 "자식 객체"는 생성자, 소멸자, 오버로드된 연산자 및 동반자 함수를 제외하고 "부모 객체"의 모든 속성 및 동작을 획득한다.
상속을 통해 프로그래머는 기존 클래스에 빌드된 클래스를 생성하고, 동일한 동작을 유지하면서(인터페이스 구현) 새로운 구현을 지정하고, 코드를 재사용하고, 공용 클래스 및 인터페이스를 통해 원본 소프트웨어를 독립적으로 확장할 수 있다. 상속된 클래스를 상위 클래스 또는 슈퍼 클래스의 하위 클래스라고 한다.
상속이란 용어는 클래스 기반 프로그래밍과 프로토타입 기반 프로그래밍 모두에 느슨하게 사용되지만, 좁은 사용에서는 클래스 기반 프로그래밍(한 클래스가 다른 클래스에서 상속됨)을 위해 보존되며, 프로토타입 기반 프로그래밍의 해당 기술은 대신 위임(한 객체를 다른 클래스로 위임함)이라고 불린다.
객체 지향 프로그래밍(OOP)에서 상속은 객체들 간의 관계를 구축하는 방법이다. 클래스가 객체로 정의되는 고전 상속에서, 클래스는 기반 클래스, 수퍼클래스, 또는 부모 클래스 등의 기존의 클래스로부터 속성과 동작을 상속받을 수 있다. 그 결과로 생기는 클래스를 파생 클래스, 서브클래스, 또는 자식 클래스라고 한다. 상속을 통한 클래스들의 관계는 계층을 형성한다. 프로토타입 기반 프로그래밍에서는, 객체가 클래스를 따로 정의할 필요 없이 다른 객체로부터 직접 정의될 수 있다. 이러한 특징을 차등 상속이라고 부른다.

 

A클래스와 B클래스가 있고, B클래스가 A클래스를 상속한다고 치면 A클래스는 부모(상위) 클래스가 된다. 그리고 이 A클래스를 상속받는 B클래스는 자식(하위) 클래스가 되는 것이다.

이 때 B클래스는 A클래스의 모든 변수/메서드를 갖고 있어서, B클래스는 A클래스의 변수와 메서드를 사용할 수 있다.

여기서 왜 상속이라는 단어를 갖다 썼는지 알 수 있다. B클래스는 A클래스의 모든 변수와 메서드를 물려받아서 사용할 수 있기 때문에 상속이라는 단어로 두 클래스의 관계를 표현한 것이다.

 

코드로 확인하기 전에 먼저 부모/자식 클래스의 관계를 다이어그램으로 만들어보자.

사용한 프로그램은 starUML이라는 것으로 클래스 다이어그램을 설계할 수 있는 무료 프로그램이다. 아래는 설치 프로그램을 받을 수 있는 공식 홈페이지 링크다. 맥과 리눅스에서도 쓸 수 있다고 하며, 설치 프로그램 용량은 146MB로 가벼운 편이다. 다크 테마도 지원한다.

staruml.io/

 

StarUML

UML 2 Compatible with UML 2.x standard metamodel and diagrams: Class, Object, Use Case, Component, Deployment, Composite Structure, Sequence, Communication, Statechart, Activity, Timing, Interaction Overflow, Information Flow and Profile Diagram. SysML Sup

staruml.io

 

간단한 사용법은 아래와 같다. 먼저 설치하고 실행하면 아래와 같은 화면이 나온다.

 

 

왼쪽 밑에 보면 Class라는 버튼이 있는 게 보인다. 이걸 클릭한 후 하얀 바탕의 아무데나 드래그해서 적당한 크기로 네모 박스를 만들면 클래스 다이어그램이 하나 만들어진다.

 

 

클래스 이름을 정해준다. 그리고 여기서 사용할 것은 빨간 박스 안의 단 2가지다.

왼쪽 비스듬한 직사각형 모양은 클래스 안에 변수들을 만들어주는 버튼이고, 오른쪽 톱니바퀴 모양 버튼은 클래스 안에 메서드들을 만들어준다.

만약 변수 생성 버튼을 누르면 오른쪽의 박스들이 사라지는데, 이 경우 다이어그램을 우클릭한 다음 Add > Operation을 누르면 메서드를 만들 수 있다.

 

 

이렇게 변수와 메서드를 하나씩 만들어주면 아래와 같은 그림이 완성된다.

 

 

참고로 변수든 메서드든 작성한 다음 Ctrl + Enter를 누르면 바로 밑에 새로 만들 수 있는 칸이 만들어진다. 일일이 우클릭해서 만들 필요는 없다. 메서드는 이름만 작성한 후 Ctrl + Enter를 누르면 뒤에 자동으로 ()를 만들어준다.

이제 이 클래스를 부모 클래스로 만들어보자. 게임의 캐릭터를 모티브로 부모 클래스를 만들어봤다.

아래의 클래스 다이어그램은 이해를 돕기 위한 하나의 예시고 정답은 아니니 주의.

 

 

이제 막 게임의 캐릭터를 만들었단 설정으로 Beginner라는 클래스를 만들고, 그 안에 체력/마나/아이디를 변수로 만들었다. 메서드는 이동, 공격, 사망, 대화 시도의 4가지 메서드를 만들었다.

이 변수들과 메서드는 Beginner 클래스만의 고유한 것이므로, 이 클래스를 상속한 자식 클래스는 저것을 다시 정의해줄 필요는 없다.

이제 자식 클래스를 만들자. 대충 전사와 궁수 클래스를 만든다고 치고 아래와 같이 만들었다.

 

 

이제 전사와 궁수 클래스는 Beginner 클래스를 상속받았다고 표시하려고 한다.

warrior, archer 클래스가 Beginner 클래스를 상속받았다고 표시하려면 warrior, archer 클래스에서 Beginner 클래스 쪽으로 화살표를 그어주면 된다. 아래와 같이 화살표를 만들어주면 두 클래스가 Beginner 클래스를 상속받았다는 것이 된다.

 

 

이제 이 다이어그램을 토대로 클래스들을 만들어보자. 먼저 Beginner 클래스다.

 

public class Beginner
{
    public int mp = 10;
    public int hp = 10;
    public String id = "초보자입니다";

    public Beginner()
    {
    }

    public Beginner(int mp, int hp, String id)
    {
        this.mp = mp;
        this.hp = hp;
        this.id = id;
    }

    public void move()
    {
        System.out.println("캐릭터가 이동합니다");
    }

    public void attack()
    {
        System.out.println("캐릭터가 무기를 휘둘러 공격합니다");
    }

    public void die()
    {
        System.out.println("캐릭터가 사망하였습니다");
    }

    public void talk()
    {
        System.out.println("캐릭터가 대화를 시도합니다");
    }

}

나중에 메인에서 확인하기 위해 hp, mp는 10으로 초기화했다. id도 "초보자입니다"라는 문자열로 초기화했다.

부모 클래스가 완성됐으니 이제 자식 클래스들을 만들어보자.

코딩 시 자식 클래스가 부모 클래스를 상속받았다는 걸 나타내려면 extends 키워드를 통해 나타낸다.

 

public class Warrior extends Beginner
{
    public int str;
    public String 마법이름;
    public String 마법이름2;
    public boolean 중갑착용여부;
    public boolean 대검착용여부;

    public Warrior()
    {
    }

    public Warrior(int str, String 마법이름, String 마법이름2, boolean 중갑착용여부, boolean 대검착용여부)
    {
        this.str = str;
        this.마법이름 = 마법이름;
        this.마법이름2 = 마법이름2;
        this.중갑착용여부 = 중갑착용여부;
        this.대검착용여부 = 대검착용여부;
    }

    public Warrior(int mp, int hp, String id, int str, String 마법이름, String 마법이름2, boolean 중갑착용여부, boolean 대검착용여부)
    {
        super(mp, hp, id);
        this.str = str;
        this.마법이름 = 마법이름;
        this.마법이름2 = 마법이름2;
        this.중갑착용여부 = 중갑착용여부;
        this.대검착용여부 = 대검착용여부;
    }

    public void 중갑방어구장착(String 방어구이름)
    {
        System.out.println("전사가 " + 방어구이름 + "을 착용했어요");
    }

    public void 대검장착(String 무기이름)
    {
        System.out.println("전사가 " + 무기이름 + "을 착용했어요");
    }

    public void skill(String 마법이름)
    {
        System.out.println("전사가 " + 마법이름 + "을 사용했어요");
    }

    public void skill2(String 마법이름2)
    {
        System.out.println("전사가 " + 마법이름2 + "을 사용했어요");
    }

}
public class Archer extends Beginner
{
    public int dex;
    public String 마법이름;
    public String 마법이름2;
    public boolean 경갑착용여부;
    public boolean 활착용여부;

    public Archer()
    {
    }

    public Archer(int dex, String 마법이름, String 마법이름2, boolean 경갑착용여부, boolean 활착용여부)
    {
        this.dex = dex;
        this.마법이름 = 마법이름;
        this.마법이름2 = 마법이름2;
        this.경갑착용여부 = 경갑착용여부;
        this.활착용여부 = 활착용여부;
    }

    public Archer(int mp, int hp, String id, int dex, String 마법이름, String 마법이름2, boolean 경갑착용여부, boolean 활착용여부)
    {
        super(mp, hp, id);
        this.dex = dex;
        this.마법이름 = 마법이름;
        this.마법이름2 = 마법이름2;
        this.경갑착용여부 = 경갑착용여부;
        this.활착용여부 = 활착용여부;
    }

    public void 경갑방어구장착(String 방어구이름)
    {
        System.out.println("궁수가 " + 방어구이름 + "을 착용했어요");
    }

    public void 활장착(String 활이름)
    {
        System.out.println("궁수가 " + 활이름 + "을 장착했어요");
    }

    public void skill(String 마법이름)
    {
        System.out.println("궁수가 " + 마법이름 + "을 사용했어요");
    }

    public void skill2(String 마법이름2)
    {
        System.out.println("궁수가 " + 마법이름2 + "을 사용했어요");
    }

}

 

이제 Main 클래스를 만들고 각 클래스의 객체를 만들어보자.

 

public class Main
{
    public static void main(String[] args)
    {
        Beginner beginner = new Beginner();
        Beginner 초보자 = new Beginner(30, 30, "초보자");

        Warrior warrior = new Warrior();
        Warrior 전사 = new Warrior(100, "마법이름", "마법이름2", true, true);
        Warrior 전사2 = new Warrior(50, 50, "전사", 100, "마법이름", "마법이름2", true, true);

        Archer archer = new Archer();
        Archer 궁수 = new Archer(100, "마법이름", "마법이름2", true, true);
        Archer 궁수2 = new Archer(50, 50, "궁수", 100, "마법이름", "마법이름2", true, true);
    }
}

 

메인 메서드의 처음 3줄을 보면 각 클래스의 객체를 만든 걸 볼 수 있다.

그리고 각 자식 클래스 별로 정의한 생성자를 통해서도 객체를 만들었다.

이제 아이디 값을 테스트해보자. '전사' 객체는 Beginner를 상속받아서 Beginner의 id 값이 나올 것이고, '전사2' 객체는 Warrior 클래스의 생성자를 통해 id를 따로 만들었기 때문에 "전사"가 출력될 것이다. hp도 같이 출력해보자.

 

public class Main
{
    public static void main(String[] args)
    {
        Beginner beginner = new Beginner();
        Beginner 초보자 = new Beginner(30, 30, "초보자");

        Warrior warrior = new Warrior();
        Warrior 전사 = new Warrior(100, "마법이름", "마법이름2", true, true);
        Warrior 전사2 = new Warrior(50, 50, "전사", 100, "마법이름", "마법이름2", true, true);

        Archer archer = new Archer();
        Archer 궁수 = new Archer(100, "마법이름", "마법이름2", true, true);
        Archer 궁수2 = new Archer(50, 50, "궁수", 100, "마법이름", "마법이름2", true, true);

        System.out.println("Beginner를 상속받은 전사의 id = " + 전사.id);
        System.out.println("Warrior 클래스의 생성자로 정의된 전사의 id = " + 전사2.id);

        System.out.println("Beginner를 상속받은 전사의 체력 = " + 전사.hp);
        System.out.println("Warrior 클래스의 생성자로 정의된 전사의 체력 = " + 전사2.hp);
    }
}

위와 같은 실행결과를 얻었으니, 게임을 만든다면 클래스와 생성자를 어떻게 사용해야 할지 생각해볼 수 있다.

이제 자식 클래스의 객체인 warrir, archer를 통해 부모 클래스의 메서드들을 사용해보자.

 

public class Main
{
    public static void main(String[] args)
    {
        Warrior warrior = new Warrior();
        Archer archer = new Archer();

        warrior.move();
        archer.move();
    }
}

 

그런데 warrior와 archer가 이동하는데 캐릭터가 이동한다고 나오는 것도 조금 이상하다.

warrior인 경우 전사가 이동한다 식으로 출력되야 하는데, 이런 경우는 어떻게 해야 할까?

 

부모 클래스로부터 상속받은 메서드의 내용을 바꿔주면 된다. 이것을 오버라이딩(Overriding)이라고 한다.

부모 클래스한테 상속받은 메서드를 그대로 쓸 수도 있지만 자식 클래스(warrior, archer)에 맞게 변경해야 하는 경우에 메서드의 내용을 바꿔주는 걸 오버라이딩(재정의)한다고 말한다.

Warrior 클래스에 move() 하나를 추가해주자.

 

public class Warrior extends Beginner
{
    public int str;
    public String 마법이름;
    public String 마법이름2;
    public boolean 중갑착용여부;
    public boolean 대검착용여부;

    public Warrior()
    {
    }

    public Warrior(int str, String 마법이름, String 마법이름2, boolean 중갑착용여부, boolean 대검착용여부)
    {
        this.str = str;
        this.마법이름 = 마법이름;
        this.마법이름2 = 마법이름2;
        this.중갑착용여부 = 중갑착용여부;
        this.대검착용여부 = 대검착용여부;
    }

    public Warrior(int mp, int hp, String id, int str, String 마법이름, String 마법이름2, boolean 중갑착용여부, boolean 대검착용여부)
    {
        super(mp, hp, id);
        this.str = str;
        this.마법이름 = 마법이름;
        this.마법이름2 = 마법이름2;
        this.중갑착용여부 = 중갑착용여부;
        this.대검착용여부 = 대검착용여부;
    }

    public void 중갑방어구장착(String 방어구이름)
    {
        System.out.println("전사가 " + 방어구이름 + "을 착용했어요");
    }

    public void 대검장착(String 무기이름)
    {
        System.out.println("전사가 " + 무기이름 + "을 착용했어요");
    }

    public void skill(String 마법이름)
    {
        System.out.println("전사가 " + 마법이름 + "을 사용했어요");
    }

    public void skill2(String 마법이름2)
    {
        System.out.println("전사가 " + 마법이름2 + "을 사용했어요");
    }

    @Override
    public void move()
    {
        System.out.println("전사가 이동합니다");
    }

}

 

이제 메인에서 사용해보자.

 

public class Main
{
    public static void main(String[] args)
    {
        Warrior warrior = new Warrior();
        Archer archer = new Archer();

        warrior.move();
        archer.move();
    }
}

 

move()를 재정의하지 않은 Archer 객체는 '캐릭터가 이동합니다'라는 문자열을 출력하고, move()를 재정의한 Warrior 객체는 '전사가 이동합니다' 라고 바뀐 문자열을 출력하는 걸 볼 수 있다.

이 오버라이딩은 모든 경우에만 쓸 수 있는 건 아니다. 자식 클래스에서 부모 클래스의 메서드를 오버라이딩하는 경우, 자식 클래스의 메서드는 부모 클래스의 메서드와

 

  • 이름이 같아야 한다
  • 인자(매개변수)가 같아야 한다
  • 리턴값의 타입이 같아야 한다
  • 조상 클래스의 메서드보다 좁은 범위의 접근 제어자로 바꿀 수 없다

접근 제어자가 private인 경우는 오버라이딩 자체가 불가능하다. 왜냐면 private이 그 클래스에서만 사용한다는 뜻의 접근 제어자기 때문이다.

참고로 인텔리제이를 쓸 경우 오버라이딩할 메서드를 호출하는 단축키는 Alt + Insert다. 이 때 작은 창이 나오는데 밑으로 내려서 Override를 찾으면 된다.

 

그럼 이 상속의 장점은 무엇일까? 정리하면 아래와 같다.

  • 적은 양의 코드로 새 클래스를 만들 수 있다
  • 부모 클래스를 수정하면 자식 클래스도 같이 바뀌니 유지보수가 쉽다

상속을 구현할 때는 여러모로 고민할 것이 많다. 한번에 이해할 수 있는 개념은 아니라 진득하게 시간을 들여 이해할 필요성이 있다.

다른 포스팅에 좋은 예제들이 있으니 그것들을 참고하며 익히자.

반응형
Comments