본문 바로가기

Study/오프젝트

10) 상속과 코드 재사용

이번장에서는 상속에 대해서 배우고 중복코드의 치명적인 단점을 학습하는 장이다. 책의 예시로는 전화통화 요금을 예로 들어서 설명한다. 나는 택시로 예를 들어서 따라 학습할 예정이다.

 

우선 택시의 클래스를 작성했다. 탑승시간과 하차시간을 저장하는 Ride 클래스를 정의했다.

public class Ride {
    private final LocalDateTime boardingTime;
    private final LocalDateTime gettingOffTime;

    public Ride(LocalDateTime boardingTime, LocalDateTime gettingOffTime) {
        this.boardingTime = boardingTime;
        this.gettingOffTime = gettingOffTime;
    }

    public Duration getTime() {
        return Duration.between(boardingTime, gettingOffTime);
    }
}

 

그런 다음 택시의 요금을 계산할 객체 Taxi를 구현할 차례다. 간편하게 설정을 하기 위해서 기본요금을 제외하고 초당 요금을 설정하도록 하겠다.

public class Taxi {
    private final int amount;
    private final Duration seconds;
    private final List<Ride> rides = new ArrayList<>();

    public Taxi(int amount, Duration seconds) {
        this.amount = amount;
        this.seconds = seconds;
    }

    public void ride(Ride ride) {
        rides.add(ride);
    }

    public int calculateFee() {
        int fee = 0;

        for (Ride r : rides) {
            fee += ((r.getTime().getSeconds() / seconds.getSeconds()) * amount);
        }

        return fee;
    }
}

 

택시의 할증요금을 계산하는 SurchargeTaxi 클래스를 만들어 보자. 물론 간단하게 만들 예정이다. 만약 할증시간이면 할증요금으로 계산한다.

public class SurchargeTaxi {
    private static final int SURCHARGE_TIME = 22;

    private final int nightlyAmount;
    private final int regularAmount;
    private final Duration seconds;
    private List<Ride> rides = new ArrayList<>();

    public SurchargeTaxi(int nightlyAmount, int regularAmount, Duration seconds) {
        this.nightlyAmount = nightlyAmount;
        this.regularAmount = regularAmount;
        this.seconds = seconds;
    }

    public int calculateFee() {
        int fee = 0;

        for (Ride r : rides) {
            if (r.getBoardingTime().getHour() >= SURCHARGE_TIME) {
                fee += ((r.getTime().getSeconds() / seconds.getSeconds()) * nightlyAmount);
            } else {
                fee += ((r.getTime().getSeconds() / seconds.getSeconds()) * regularAmount);
            }
        }

        return fee;
    }
}

 

여기서 확인을 해보면 기존 요금을 계산하는 것과 할증요금을 계산하는 것과 중복 코드를 발견할 수 있다. 만약 여기서 추가적인 요구사항을 추가하게 되면 어떻게 될까? 그럼 두 코드 모드를 수정해야 한다. 이게 바로 중복코드가 가지는 단점이다. 중복 코드를 제거하는 방법으로 클래스를 하나로 합치는 방법이 있다. 기본요금과 할증요금을 구분하는 타입 코드를 추가하고 타입 코드의  값에 따라 로직을 분리시키면 된다. 하지만 타입 코드를 사용하는 클래스는 낮은 응집도높은 결합도를 가지는 문제를 가진다.

 

이를 해결할수 있는 좋은 방법이 있다. 바로 상속을 이용하는 방법이다. 상속을 이용해서 기존 Taxi의 코드를 대부분 재사용하여 구현을 할 수 있다. 하지만 상속은 결합도를 높인다. 그리고 상속이 초래하는 부모 클래스와 자식 클래스 사이의 강한 결합이 코드를 수정하기  어렵게 만든다.

public class SurchargeTaxi extends Taxi {
    private static final int SURCHARGE_TIME = 22;

    private final int nightlyAmount;

    public SurchargeTaxi(int regularAmount, Duration seconds, int nightlyAmount) {
        super(regularAmount, seconds);
        this.nightlyAmount = nightlyAmount;
    }

    @Override
    public int calculateFee() {
        int fee = super.calculateFee();

        int nightlyFee = 0;
        for (Ride r : getRides()) {
            if (r.getBoardingTime().getHour() >= SURCHARGE_TIME) {
                nightlyFee += ((r.getTime().getSeconds() / super.getSeconds().getSeconds()) * (nightlyAmount - super.getAmount()));
            }
        }
        
        return fee + nightlyFee;
    }
}

 

여기서 만약 추가 요구사항이 생겼을 경우를 떠올려보자. 추가 요구사항을 구현하기 위해 Taxi를 수정할 때 SurchargeTaxi에도 추가 수정을 해야 한다. 코드 중복을 제거하기 위해 상속을 사용했지만 추가 기능 구현을 하기 위해 새로운 중복 코드를 만들어야 한다는 문제가 있다. 이처럼 상속 관계로 연결된 자식 클래스가 부모 클래스의 변경에 취약해지는 현상을 가리켜 취약한 기반 클래스 문제라고 부른다.

 

여기서 취약한 기반 클래스 문제를 어느 위험을 완하시키는 것이 가능하다. 정답은 역시 추상화다. 부모 클래스와 자식 클래스 모두 추상화에 의존하도록 수정하면 된다. 여기서 코드 중복을 제거하기 위한 두 가지 원칙을 소개한다.

  • 두 메서드가 유사하게 보인다면 차이점을 메서드로 추출해라.
  • 부모 클래스의 코드를 하위로 내리지 말고 자식 클래스의 코드를 상위로 올려라.

우선 차이를 메서드로 추출하라를 따라보겠다. 지금 살펴보면 요금을 계산하는 메서드가 조금 다르다. 그래서 두 클래스의 메서드에서 다른 부분을 별도의 메서드로 추출해 보자.

public class Taxi {
    private final int amount;
    private final Duration seconds;
    private final List<Ride> rides = new ArrayList<>();

    public Taxi(int amount, Duration seconds) {
        this.amount = amount;
        this.seconds = seconds;
    }

    public int calculateFee() {
        int fee = 0;

        for (Ride r : rides) {
            fee += calculateRideFee(r);
        }

        return fee;
    }

    private long calculateRideFee(Ride r) {
        return (r.getTime().getSeconds() / seconds.getSeconds()) * amount;
    }
}
public class SurchargeTaxi {
    private static final int SURCHARGE_TIME = 22;

    private final int nightlyAmount;
    private final int regularAmount;
    private final Duration seconds;
    private List<Ride> rides = new ArrayList<>();

    public SurchargeTaxi(int nightlyAmount, int regularAmount, Duration seconds) {
        this.nightlyAmount = nightlyAmount;
        this.regularAmount = regularAmount;
        this.seconds = seconds;
    }

    public int calculateFee() {
        int fee = 0;

        for (Ride r : rides) {
            fee += calculateRideFee(r);
        }

        return fee;
    }

    private long calculateRideFee(Ride r) {
        if (r.getBoardingTime().getHour() >= SURCHARGE_TIME) {
            return (r.getTime().getSeconds() / seconds.getSeconds()) * nightlyAmount;
        } else {
            return (r.getTime().getSeconds() / seconds.getSeconds()) * regularAmount;
        }
    }
}

 

이렇게 차이를 메서드로 추출을 했다. 그다음 단계인 중복 코드를 부모 클래스로 올려보겠다. 여기서 의도를 나타내기 위해서 클래스명도 수정해 주겠다. 기존에 Taxi를 BaseAmount를 하고 할증요금을 SurchargeAmount라고 수정하고 부모 클래스를 Taxi로 구현을 하겠다. 우선 추상클래스를 만들어 준다. 자식 클래스에서 오버라이딩할 수 있도록 protected로 선언해 준다.

public abstract class Taxi {
    private List<Ride> rides = new ArrayList<>();

    public int calculateFee() {
        int fee = 0;

        for (Ride r : rides) {
            fee += calculateRideFee(r);
        }

        return fee;
    }

    abstract protected int calculateRideFee(Ride ride);
}
public class SurchargeAmount extends Taxi {
    private static final int SURCHARGE_TIME = 22;

    private final int nightlyAmount;
    private final int regularAmount;
    private final Duration seconds;

    public SurchargeAmount(int nightlyAmount, int regularAmount, Duration seconds) {
        this.nightlyAmount = nightlyAmount;
        this.regularAmount = regularAmount;
        this.seconds = seconds;
    }
    
    @Override
    protected long calculateRideFee(Ride ride) {
        if (ride.getBoardingTime().getHour() >= SURCHARGE_TIME) {
            return (ride.getTime().getSeconds() / seconds.getSeconds()) * nightlyAmount;
        }
        return (ride.getTime().getSeconds() / seconds.getSeconds()) * regularAmount;
    }
}

 

1단계인 차이를 메서드로 추출하고 둘 사이의 공통점을 부모 클래스로 옮김으로써 실제 코드를 기반으로 상송  계층을 구성할 수 있었다. 만약 추가 요구 사항이 생기더라도 쉽게 구현을 할 수 있게 되었다. 상속이 코드 재사용이라는 측면에서 좋은 도구이지만 잘못 사용할 경우 돌아오는 피해가 크다. 상속의 오용과 남용은 애플리케이션을 이해하고 확장하기 어렵게 만들기 때문에 꼭 필요한 경우에만 상속을 사용하자.

'Study > 오프젝트' 카테고리의 다른 글

12) 다형성  (1) 2024.01.25
11) 합성과 유연한 설계  (0) 2024.01.17
09) 유연한 설계  (1) 2024.01.06
08) 의존성 관리하기  (0) 2024.01.02
07) 객체 분해  (0) 2023.12.27