[OOP] 상속(Inheritance)이란 ?

이석환·2024년 2월 14일
0

개념 정리

목록 보기
4/6
post-thumbnail

상속(Inheritance)

상속이란 무엇일까 ?
일반적으로 드라마에서 자주 보는 단어다.
예를 들어, 가난한 집안의 소년이 알고 보니 회장님의 핏줄이며 상속 문제로 다투는 드라마일 것이다.

객체 지향에서의 상속도 동일한 뜻을 내포한다.

객체 지향 프로그래밍에서 상속이란 객체가 다른 객체를 상속받아 상속받은 객체의 요소를 사용하는 것을 의미한다.

즉, 부모 클래스의 메소드 혹은 필드를 자식 클래스에게 물려주는 것이다.

  • 객체를 상속받은 객체는 자식, 상속된 객체를 부모라고 칭한다.

상속의 장점 중에 하나는 자식 클래스에서 부모 클래스에 없는 기능을 추가하거나 수정할 수 있다는 것이다. (필요시에 확장이 가능)
바로 예제를 보면서 알아보자.

장점

코드 재사용

다음과 같은 두 개의 class가 있다고 가정하자.
철수와 영희는 가족이다.
이 가족에는 특별한 규칙이 있는데, 아침과 저녁을 같이 먹는 것이다.
철수는 직장인이고, 영희는 학생이다.

public class 첫째 {
    private final String name;

    public 첫째(String name) {
        this.name = name;
    }

    public void eatBreakfastWithFamily() {
        System.out.println(name + ": 가족과 함께 아침을 먹습니다.");
    }

    public void eatDinnerWithFamily() {
        System.out.println(name + ": 가족과 함께 저녁을 먹습니다.");
    }

    public void goWork() {
        System.out.println(name + ": 회사로 갔습니다.");
    }
}

public class 둘째 {
    private final String name;

    public 둘째(String name) {
        this.name = name;
    }

    public void eatBreakfastWithFamily() {
        System.out.println(name + ": 가족과 함께 아침을 먹습니다.");
    }
    public void eatDinnerWithFamily() {
        System.out.println(name + ": 가족과 함께 저녁을 먹습니다.");
    }

    public void goSchool() {
        System.out.println(name + ": 학교로 갔습니다.");
    }
}


위와 같이 첫째와 둘째의 기능을 정의했다.
다음은 프로그래머 관점에서 작성한 코드를 보자.

public class Main {

    public static void main(String[] args) {
        final 첫째 firstBorn = new 첫째("철수");
        final 둘째 secondBorn = new 둘째("영희");

        firstBorn.eatBreakfastWithFamily();
        secondBorn.eatBreakfastWithFamily();

        System.out.println();

        firstBorn.goWork();
        secondBorn.goSchool();

        System.out.println();

        firstBorn.eatDinnerWithFamily();
        secondBorn.eatDinnerWithFamily();


    }
}

다음과 같은 결과가 나온다.

코드를 잘 살펴보자.
정확히 철수의 goWork와 영희의 goSchool을 제외하고 중복이 발생한다.

중복되는 기능을 상위 클래스로 정의해서 상속하는 방법을 생각해 보자.

public class HwanFamily {
    protected final String name;
    
    public HwanFamily(String name) {
        this.name = name;
    }
    
    public void eatBreakfastWithFamily() {
        System.out.println(name + ": 가족과 함께 아침을 먹습니다.");
    }
    
    public void eatDinnerWithFamily() {
        System.out.println(name + ": 가족과 함께 저녁을 먹습니다.");
    }
}
public class 첫째 extends HwanFamily{
    public 첫째(String name) {
        super(name);
    }

    public void goWork() {
        System.out.println(name + ": 회사로 갔습니다.");
    }
}

public class 둘째 extends HwanFamily {
    public 둘째(String name) {
        super(name);
    }

    public void goSchool() {
        System.out.println(name + ": 학교로 갔습니다.");
    }
}

Main 메서드를 실행하면 똑같은 결과가 나오는 것을 알 수 있다.
하지만, 중복된 코드는 엄청나게 감소됐다.
아침과 저녁을 먹는 기능을 재사용하고, name 필드도 상위 클래스의 변수를 재사용하는 것으로 하위 클래스의 기능에만 집중할 수 있게 되었다.

여기서 기능을 확장하고 싶다면 다음과 같이 하면 된다.

public class 첫째 extends HwanFamily{
    public 첫째(String name) {
        super(name);
    }

    @Override
    public void eatDinnerWithFamily() {
        System.out.println(name + ": 가족과 함께 저녁을 먹지만, 가끔 야근이 있는 날은 함께 하지 못합니다.");
    }

    public void goWork() {
        System.out.println(name + ": 회사로 갔습니다.");
    }
}

직장인인 철수는 가끔 저녁을 먹을 수 없게 되었다.
또한, HwanFamily에서 다른 객체와 공통적으로 하는 기능이 생긴다면, 새로운 상위 클래스를 만들 수 있다.

다형성

필자의 벨로그에는 아직 등장하지 않았지만, 상속은 다형성을 지원할 수 있게 한다.
객체간의 관계가 생기기 때문이다.

public class Main {

    public static void main(String[] args) {
        final 첫째 firstBorn = new 첫째("철수");
        final 둘째 secondBorn = new 둘째("영희");

        final List<HwanFamily> hwanFamily = List.of(firstBorn, secondBorn);

        for (HwanFamily familyMember : hwanFamily) {
            familyMember.eatBreakfastWithFamily();
            familyMember.eatDinnerWithFamily();
            System.out.println();
        }
    }
}

상속을 통해 다형성을 구현하는 경우, 반복문을 통해 공통된 메소드를 한 번만 작성하면 된다.

단점

강한 결합

상속의 가장 큰 문제점은 하위 클래스가 상위 클래스에 강하게 결합한다는 것이다.

public class 수상기관 {
	public String givePrize() {
    	[...]
    }
}

상을 주는 기관들의 부모클래스가 존재한다.
지금까지는 그냥 상장만을 주는 것으로 각 수상 기관마다 상장의 멘트만 달라지는 것으로 통일하고 있었는데, 갑자기 상장을 만드는 것 대신에 상금을 주는 것으로 바꼈다.
즉, String -> Integer로 바뀌게 되었다 !

하위 클래스에서 해당 메소드를 사용하고 있다면, 모조리 Return Type을 고쳐야 한다.

캡슐화 깨짐

장점에서 보였던 기능의 확장을 사용하는 경우, 오히려 캡슐화가 깨진다 !
상위 클래스의 메소드를 오버라이딩하는 것이 캡슐화를 깬다는 것이다.
위의 코드를 다시 가져와 보자.

public class 첫째 extends HwanFamily{
    [...]

    @Override
    public void eatDinnerWithFamily() {
        System.out.println(name + ": 가족과 함께 저녁을 먹지만, 가끔 야근이 있는 날은 함께 하지 못합니다.");
    }

    [...]
}

HwanFamilyeatDinnerWithFamily 메소드를 오버라이딩 하여 기능을 확장하였는데, 이러한 경우에 상위 클래스의 구현이 하위 클래스에 노출되었기 때문에 캡슐화가 깨졌다고 볼 수 있다.

상속은 언제 사용할까 ?

  • 클래스 상속은 기능 재사용에 있어서 굉장히 좋은 방법이다.
  • 문제점이 많기 때문에 클래스 상속보다는 조합(Composition)을 사용하라.
  • 클래스 상속보다는 추상 클래스를 사용하되, 해당 상위 클래스가 미래에도 변하지 않음이 확실하다면 사용해라.

References
https://steady-coding.tistory.com/451

profile
반갑습니다.

0개의 댓글