새로운 내용을 공부할 때
새로운 내용의 공부를 시작할 때 용어의 정의를 이해하지 못하거나 정확하게 알지 못한다면 그 용어가 포함된 문장을 이해하지 못합니다.
작은 단어 하나가 내용을 이해하지 못하게 하기 때문에 용어를 정확하게 이해하는 것이 중요합니다.

4 분 소요

오늘은 상속에 대해서 알아보려고 합니다.

자바를 처음 배울 때 객체 지향 프로그래밍에서 상속은 기존 클래스의 속성과 기능을 그대로 물려받기 때문에 코드의 재사용할 수 있다는 강력한 장점을 가지고 있다고 배웠습니다.

하지만 실무에서는 상속을 사용한 코드를 보거나 사용한 기억이 없습니다.

더 나아가 왜 상속을 사용하지 않고, 인터페이스 구현이나 구성(Composition)을 선호하는지 알아보려고 합니다.

상속을 사용하지 않는 이유를 설명하기 전에 상속에 대해 설명해보겠습니다.

상속이란Permalink

상속에 대해 간단하게 설명하면 객체 지향 프로그래밍의 핵심 요소 중 하나로, 기존 클래스의 필드와 메서드를 상속한 클래스에서 재사용하게 해줍니다.

상속은 extends 키워드를 사용하여 대상 하나만 선택할 수 있습니다.

public abstract class Car {
    private String model; // 이름
    private int size; // 크기

    public Car(String model, int size) {
        this.model = model;
        this.size = size;
    }
    
    public String getModel() {
        return model;
    }
    
    public void showInfo() {
        // 구현 내용
    }
    // getter & setter 
    // methods ...
}

public class Truck extends Car {
    
    // 자식 클래스에 추가한 필드
    private int modelYear;
    
    public Truck(String model) {
        super(model, 1); // 소형차는 size를 1로 설정
    }
    
    @Override
    public String getModel() {
        return super.getModel() + " " + this.modelYear;
    }
}

단, 모든 필드나 메서드를 자식 클래스에서 사용하지 못합니다.

패키지가 다른 경우

  • private
  • package-private (default)

패키지가 같은 경우

  • private 위 접근 제어자가 붙어있는 메서드나 필드는 접근할 수 없으며 부모 클래스에서 제공하는 메서드를 통해 접근할 수 있습니다.
  • 부모의 생성자를 호출할 때는 super()
  • 부모의 메서드를 호출할 때는 super.xxxx

상속과 결합도Permalink

자식 클래스는 부모 클래스의 속성메서드를 그대로 사용할 수 있다는 장점이 있습니다.

부모 클래스 Car에 요청사항으로 추가 필드인 Door이 추가되었습니다.

public abstract class Car {
    private String model; 
    private int size; 
    private Door[] doors; // 추가 코드

    public Car(String model, int size, Door[] doors) {
        this.model = model;
        this.size = size;
        this.doors = doors;
    }
}

public class Truck extends Car {
    // 예외발생
    public Truck(String model) {
        super(model, 1); // 소형차는 size를 1로 설정
    }
}

부모 클래스를 수정하고 프로그램을 실행하려면 오류가 발생합니다.

image-20240622190049102

자식 클래스에서 사용하는 부모 클래스의 생성자가 없기 때문에 컴파일 에러가 발생했습니다.

부모 클래스의 변경이 자식 클래스에 영향을 주게 되는 경우처럼 클래스 간 변경 시 영향을 미칠 경우 결합도가 올라갔다고 합니다.

상속으로 코드 재사용성은 높아지지만, 수정과 확장에는 어려움이 있습니다. 이는 대규모 시스템에서 어떤 문제로 나타날지 모릅니다.

또한 캡슐화와 정보 은닉의 문제가 있습니다.

상속을 통해 자식 클래스는 부모 클래스의 구현 세부 사항에 접근할 수 있게 되며, 이는 캡슐화와 정보 은닉을 저해할 수 있습니다.

부모 클래스의 메서드를 올바르게 재정의하거나 사용하려면 자식 클래스가 부모 클래스의 내부 구조를 알아야 하기 때문에 한번 만들어진 내부 구조를 변경하기 어렵게 만듭니다.

추상화의 문제가 있습니다. 상속은 일반적으로 is-a 관계를 표현합니다. 실제로 부모 클래스와 자식 클래스 간의 관계가 실제로 has-a 또는 uses-a 관계인 경우에도 상속을 사용하게 될 수 있습니다. 이러한 잘못된 추상화로 코드의 유연성을 떨어뜨립니다.

유지보수와 확장성의 문제가 발생합니다. 부모 클래스가 복잡할수록 자식 클래스의 유지보수는 더 어려워집니다.

부모 클래스의 변경이 자식 클래스에 영향을 미치기 때문에 더 많은 테스트와 검증이 필요하게 됩니다.

정리하면, 부모 클래스의 코드 변경이 자식 클래스에게 전파되기 때문에 부모 클래스의 결함까지 전파될 수 있는 문제가 있습니다.

상태를 공유한다는 의미Permalink

// 비즈니스 로직
UpdateMemberDto memberDto = ...// 회원 정보를 수정하려는 Dto

// 회원가입 경로 체크
if (memberDto.checkJoinRoot()) {
    updateMemberFee(memberDto);
}

// 이벤트 대상 여부 체크
if (memberDto.isEventTarget()) {
    updateMemberEventFlag(memberDto);
}
//... 400줄

객체가 수정 가능한 가변 객체라고 할 때 위 코드를 확장하거나 수정한다면 메소드를 하나씩 들어가서 확인해야 합니다.

캡슐화도 깨지게 되고, 정보 은닉도 사라집니다. 그래서 객체 상태를 변경할 수 없도록 하거나 변경을 최소화하는 불변객체를 사용합니다.

불변 객체를 사용하면 코드의 안정성도 높아지며, hashCode를 사용하는 자료구조에서도 문제가 발생하지 않습니다.

하지만 부모 클래스를 상속한 자식 클래스는 불변으로 만들 수 있지만, 불안전하며 부모 클래스에 영향을 받기 때문에 불변 객체로 만들기 어렵고 유지하기도 어렵습니다.

그래서 다형성을 효과적으로 사용하기 위해 인터페이스와 추상 클래스가 있지만 추상 클래스는 상속으로 상태를 공유하기 때문에 불변 객체를 만들기 어렵고, 불변 객체로 얻을 수 있는 장점을 잃게 됩니다.

하지만 추상 메서드의 상속이 무조건 나쁜 건 아닙니다.

is-a 구조로 만들어진 클래스라면 추상 메서드를 통해 코드 재사용성도 높이고 유지보수하기 쉬워질 수 있습니다.

템플릿 메서드 패턴Permalink

// 상위 클래스: 카드 결제
abstract class CardPayment {

    // 템플릿 메서드
    public void pay() {
        startPayment();
        self();
        endPayment();
    }

    private void startPayment() {
        System.out.println("결제 시작 준비");
    }

    protected abstract void self();

    private void endPayment() {
        System.out.println("결제 종료 로직");
    }
}

// 하위 클래스 1: 페이코 결제
class PaycoPayment extends CardPayment {

    @Override
    protected void self() {
        System.out.println("페이코 관련 결제가 진행됩니다.");
    }
}

// 하위 클래스 2: 네이버 페이 결제
class NaverPayPayment extends CardPayment {

    @Override
    protected void self() {
        System.out.println("네이버 관련 결제가 진행됩니다.");
    }
}

상위 클래스의 템플릿 메서드를 작성하고 하위 클래스에서는 추상 메서드만 구현하면 됩니다.

일관된 알고리즘 구조를 유지할 수 있으며 기존 구조를 변경하지 않아도 새로운 결제 방식을 추가할 수 있는 장점이 있습니다.

템플릿 메서드 패턴의 장점Permalink

  • 코드 재사용성 증가: 공통 로직을 상위 클래스에서 정의하고, 하위 클래스에서는 변하는 부분만 구현하여 코드 중복을 줄이고 재사용성을 높입니다.
  • 유지보수성 향상: 공통된 알고리즘 구조는 상위 클래스에 집중시키고, 변경 가능성이 있는 세부 사항은 하위 클래스에서 처리함으로써 유지보수를 용이하게 합니다.
  • 확장성 개선: 새로운 하위 클래스를 추가하여 알고리즘의 일부를 변경할 수 있으므로 확장성이 좋아집니다. 기본 구조를 변경하지 않고도 새로운 기능을 쉽게 추가할 수 있습니다.
  • 일관된 알고리즘 구조: 템플릿 메서드는 전체 알고리즘의 뼈대를 제공하여 하위 클래스들이 일관된 방식으로 특정 작업을 수행하도록 보장합니다.

정리Permalink

상속은 객체 지향 프로그래밍에서 중요한 개념으로, 코드 재사용성을 높이는 강력한 도구입니다. 그러나 상속을 남용하면 결합도가 높아지고, 캡슐화와 정보 은닉이 깨지며, 유지보수와 확장성이 어려워질 수 있습니다. 이러한 이유로 실무에서는 상속보다 인터페이스 구현이나 구성(Composition)을 선호하는 경향이 있습니다.

대안Permalink

  • 인터페이스 구현: 객체의 행위를 정의하고 다양한 클래스에서 구현하여 유연성을 높입니다.
  • 구성(Composition): 객체를 조합하여 기능을 확장하는 방법으로, 상속보다 낮은 결합도를 유지할 수 있습니다.

결론적으로, 상속은 적절한 상황에서 강력한 도구가 될 수 있지만, 그 사용에 주의가 필요합니다. 상속을 사용할 때는 결합도를 낮추고, 캡슐화와 정보 은닉을 유지하며, 유지보수와 확장성을 고려한 설계가 중요합니다.

태그: ,

카테고리:

업데이트:

댓글남기기