IT 서적/Object

상속과 중복 코드 : 오브젝트 Chapter 010

GustavEiffels 2025. 5. 17. 13:32
반응형
Garbage Collection Guide

상속과 중복 코드 : 오브젝트 Chapter 010

---


중복 코드

중복 코드는 말 그대로 중복되는 코드를 의미하며 여러가지 이유로 좋지 못하다.
특히 코드 변경에서 좋지 않는데, 만약 중복된 코드들 중 일부분이 변경된다면

  1. 어떤 코드가 중복인지 찾아야하고,
  2. 변경 대상 모두 일관되게 변경해야한다. ( 많은 시간이걸림 )

간단하게 말했지만, 중복된 코드를 알아보는 것 자체가 많은 수고를 들인다.
이런 이유 때문에 코드안에 중복이 존재해서는 안된다DRY 원칙을 준수 해야한다.

중복 코드를 수정하기

효과적으로 중복된 코드를 수정하려면 어떻게 해야할까?

public class Phone{
  public Phone(Money amount, Duration seconds, double taxRate){
   ...
  }
}

public class NightlyDiscountPhone {
	public NightlyDiscountPhone( Money nightlyAmount, Money regularAmount ... ){
		...
	}
}

1. 비슷한 두개의 클래스를 하나의 클래스로 합치기

public class Phone{
  public Phone(Money amount, Duration seconds, double taxRate){
   ...
  }
  	public Phone( Money nightlyAmount, Money regularAmount ... ){
		...
	}
	
	enum PhoneType { REGULAR, NIGHTLY }
}

이러한 비슷한 구조를 가진 객체가 존재할 경우
필드를 하나로 합치고 Enum ( 타입 코드 ) 를 활용하여 구분하여 사용하면
중복을 피할 수 있다.


2. 상속을 사용

public class NightlyDiscountPhone extend Phone {

} 

가장 객체 지향 스러운 방법이다.

하지만 비지니스 로직에 따라 다시 사용하는 것이 생각보다 어렵고 예외 상황이 너무 많다.
최종적으로 객체지향 스러운 상속은 부모 자식 클래스 간의 결함도를 높여 코드 수정에 어려움을 만든다.

여기서 상속 시 주의해야할 점으로 자식 클래스 메서드에서 부모 클래스의 메서드를 호출한다면
결합도가 엄청 높아지기 때문에 super 호출을 제거하는 방향으로 로직을 생각해야한다.

상속의 잘못된 예시


Stack 과 Vector

Vector 는 임의의 위치에서 요소를 추출하고 삽입할 수 있는 LIST 의 구현체이며, Stack 은 이 Vector 를
상속 받아서 구현되어 있다. ( Stack 은 Vector 를 상속 받는다 )
예시에서 stack의 메세지인 push 를 이용하여 1 ~ 3 까지 숫자를 입력하고 4를 입력한다.
그리고 stack 의 멘 마지막에 들어간 요소를 추출하는 pop 에서 4가 아닌 3이 나왔다.

stack.push("1");
stack.push("2");
stack.push("3");
stack.add("4");

stack.pop(); // 3을 출력
  • Vector 는 임의의 위치에서 앞 뒤로 요소를 삽입할 수 있는 구현체
  • Stack 은 이런 Vector 를 상속 받는다.
  • push 대신 Vector 의 add 를 사용하여 올바른 값을 추출하지 못했다.

코드 재사용 목적으로 상속을 쓰려면 불필요한 Operation 을 지우는게 좋다.


부모 클래스와 자식 클래스 동시 수정

public class Song{
 private String singer;
 private String title;
}
----------

public class Playlist{
 private List tracks = new ArrayList<>();

 public List getTrack(){
  return tracks
 }
}
 
-----------
public class PersonalPlaylist extends Playlist{
 public void remove(Song song){
  getTracks().remove(song);
 }
}

PlayList 를 상속 받는 PersonalPlaylist 에서 삭제하는 기능을 구현하기 위해
PlayList 의 getTrack 메서드를 사용하였다.

요구사항이 변경되어 PlayList 에서 가수별 노래의 제목도 함께 관리한다고 하여
다음과 같은 메서드가 PlayList ( 부모 ) 에서 수정이되었다.

public class Playlist{
 private List tracks = new ArrayList<>();
 private Map singers = new HashMap<>();

 public List getTrack(){
  return tracks
 }
 
 public void append(Song song){
  tracks.add(song);
  singers.put(song.getSinger(), song.getTitle());
 }
 
 public Map getSingers(){
   return singers; 
 }
}

이때

public class PersonalPlaylist extends Playlist{
 public void remove(Song song){
  getTracks().remove(song);
 }
}

이 method 가 정상적으로 작동하려면, getSingers()를 호출하여 지우려는
song 의 가수 이름 역시제거해야한다.


✅ 코드 중복을 제거하기 위해 상속을 사용하면 생기는 문제점

  1. 부모 클래스의 메서드를 호출하여 사용할 경우 예외 발생
  2. 부모의 operation 을 함께 혼용하여 사용하여 예외 발생
  3. 불필요한 상속이나 오버라이딩이 없이 부모에서 요구사항이 변경되는 경우

    자식 클래스에서도 변경해야하는 일이 발생한다.

이러한 문제점은 부모와 자식간의 결합도가 높아져서 발생하는 문제점이다.

해결 방법은 추상화?

이러한 상속의 문제점을 해결하기 위한 방벙으로
부모 뿐만아니라 자식 클래스 역시 추상화에 의존하도록 하는 방법이다.
여기에는 두가지 원칙이 있는데

  1. 메서드가 유사하게 보이면 차이점을 메서드로 추출한다.
  2. 자식 클래스 코드를 상위에 올리는 방식으로 하는 것이 재사용 성이 좋다.

여기에 대해서 자세히 알아보자


차이로 메서드 추출하기

Before

Phone...

    public Money calculateFee() {
        Money result = Money.ZERO;

        for(Call call : calls) {
            result = result.plus(amount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
        }

        return result.plus(result.times(taxRate));
    }
    

NightlyDiscountPhone...

    public Money calculateFee() {
        Money result = Money.ZERO;

        for(Call call : calls) {
            if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
                result = result.plus(nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
            } else {
                result = result.plus(regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
            }
        }

        return result;
    }

After

Phone...
    public Money calculateFee() {
        Money result = Money.ZERO;

        for(Call call : calls) {
            result = result.plus(calculateCallFee(call));
        }

        return result.plus(result.times(taxRate));
    }
    
    private Money calculateCallFee(Call call){
	     return amount.times(call.getDuration().getSeconds()/seconds.getSeconds());
    }
    
    

NightlyDiscountPhone...

    public Money calculateFee() {
        Money result = Money.ZERO;

        for(Call call : calls) {
            result = result.plus(calculateCallFee(call));
        }

        return result.plus(result.times(taxRate));
    }

    private Money calculateCallFee(Call call){
                if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
                return nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
            } else {
								return regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
            }
    }

이렇게 추출함으로 써 두 클래스의 calculateFee 메서드는 완전 일치하게 만들어서 추출이 가능해진다.


중복 코드 부모로 올리기

모든 클래스들이 추상화에 의존하도록 만드느 것이 중요하기 때문에
새로운 추상클래스를 만들고 거기에 추출한 메서드를 추가한다.


public abstract class AbstractPhone {
    private List calls = new ArrayList<>();

    public Money calculateFee() {
        Money result = Money.ZERO;

        for(Call call : calls) {
            result = result.plus(calculateCallFee(call));
        }

        return result;
    }
		// 추상메서드로 선언
    abstract protected Money calculateCallFee(Call call);
}
public class NightlyDiscountPhone extends AbstractPhone {
    private static final int LATE_NIGHT_HOUR = 22;

    private Money nightlyAmount;
    private Money regularAmount;
    private Duration seconds;

    public NightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds) {
        this.nightlyAmount = nightlyAmount;
        this.regularAmount = regularAmount;
        this.seconds = seconds;
    }

    @Override
    protected Money calculateCallFee(Call call) {
        if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
            return nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
        } else {
            return regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
        }
    }
}

public class Phone extends AbstractPhone {
    private Money amount;
    private Duration seconds;

    public Phone(Money amount, Duration seconds) {
        this.amount = amount;
        this.seconds = seconds;
    }

    @Override
    protected Money calculateCallFee(Call call) {
        return amount.times(call.getDuration().getSeconds() / seconds.getSeconds());
    }
}

읽고 나서

중복 코드를 위해서 상속을 사용할 때 문제점과 이런 문제점을 해결하기 위해
추상화를 사용하는 방법을 알아보았다!

반응형