Java Refactoring -12, 유사한 기능의 인터페이스 다중 상속 구조 개선

레거시 코드를 클린 코드로 누구나 쉽게, 리팩토링

위 책을 보면서 정리한 글입니다.

유사한 기능의 인터페이스 다중 상속 구조 개선

다중 상속

  • 비슷한 기능을 가진 클래스에 대해 보통 추상 클래스나 인터페이스 상속을 통해 규약을 만든다.
  • 인터페이스 상속은 유연성과 확장성이 높은 구현체를 만들 수 있어 꾸준히 사용되고 있지만, 추상 클래스 상속은 결합도와 가독성이 떨어지는 문제 때문에 잘 사용되지 않는다.
    • 인터페이스 상속을 사용하는 구현체 중 'SubType(추상화된 객체의 특성을 받아 일반화시키는 상속 개념)' 없이 사용되는 경우
    • 특히, 여러 개의 구현 클래스가 메서드명을 통일하기 위해 인터페이스 상속을 사용하는 것이 댸표적인 예
    • 이는 '의존 관계 역전의 원칙(DIP)을 위배하는 것으로, 유연성이 떨어지고 중복 코드가 발생'
    • 예)
      -'클래스 A'는 ArrayList로 생성된 '인스턴스 B'와 LinkedList로 생성된 '인스턴스 C'를 지니고 있다.
      -'클래스 A'의 '메서드 B'는 '인스턴스 B'에서 관리하는 자료를 하나씩 처리하는 기능을 구현.
      -'클래스 A'의 '메서드 C'는 '인스턴스 C'에서 관리하는 자료를 하나씩 처리하는 기능을 구현.
      -이를 개선하기 위해 '메서드 Z'를 만들고, List Interface를 통해 값을 하나씩 처리하는 기능을 만든다.
      -ArrayList와 LinkedList를 묶어 List로 instance를 만들고, 이를 메서드 Z에 위임하여 '메서드 B'와 '메서드 C'의 중복 코드를 제거

  • 이 밖에도 단순한 규약으로만 만들어진 인터페이스는 전혀 의도하지 않은 클래스에 상속되어 사용되기도 하고, 인터페이스를 사용하지 않고 구현체에 직접 메서드를 구현하는 등의 부작용을 낳기도 한다.
  • 이러한 부작용이 생기는 가장 큰 이유는 상속받은 구현체를 사용하는 객체가 인터페이스가 아닌, 구현체 인스턴스를 사용하기 때문.

  • 결국 규약으로 만들어진 인터페이스는 처음 의도와는 다르게 가독성을 떨어뜨리는 주범이 되고, 시간이 흐를수록 애물단지가 된다.

개선방향

규약으로 만들어진 다중 상속이 구조가 아닌, 인터페이스가 인터페이스를 상속하도록 구조 변경

규약으로 만들어진 다중 상속 때문에 '구상 클래스(Concrete Class)'를 사용하는 클래스는 어쩔 수 없이 중복 코드를 만들어야 했다.

  • 이를 개선하기 위해 규약으로 만들어진 '시관 관련 인터페이스를 할인 상품 인터페이스를 상속하도록 변경'
  • 인터페이스를 다중 상속하는 구상 클래스는 변경한 시간 인터페이스만 상속하도록 수정.
  • 구상 클래스를 사용하는 ㅋ르래스는 시간 인터페이스를 사용하여 중복 코드들을 제거.

질문답

인터페이스를 다중 상속하는 것이 나쁜 방법일까

  • 다중 상속이 나쁘 다는 것은 아니다. 다만 상속 관계, 인터페이스, 클래스의 추상화가 명확한 기준으로 구분되어 있다면 상속처럼 좋은 도구도 없다.
    • 대부분의 현업에서는 구현된 다중 상속은 여러 가지 문제를 내포하는 경우가 있다.
    • 다중 상속이 하나의 책임에 대한 기능 확장의 개념으로 사용되지 않는다면 여러 가지 책임의 집합 클래스로 전락하기 마련.
    • 이는 클래스간 결합도를 높이는 꼴이 되고, 가독성을 나쁘게 만들어 결국 생산성 저하로 이어지게 된다.
    • C++의 다이아 몬드 문제, 자바의 interface는 구현체가 없어 이렇게 될 확률은 낮지만 문제가 생긴다면 상속받은 클래스의 오버라이드 메서드가 어느 인터페이스의 기능을 담당하는지 알 수 없게 된다.
    • 여러 구문이 자연스럽게 구현된다면 해당 인터페이스는 결국 하나의 타입으로 재구성해야 한다.

레거시 코드

  • 인터페이스의 다중 상속을 단순히 규약만을 위한 용도로 사용되었다는 것이 매우 큰 장애 요소.
  • 명확하게 인터페이스의 추상화가 되지 않은 상황에서 다중 상속의 구조는 위험 요소를 가진 구조로 전락하고, SubType이 배제된 인터페이스의 개념은 중복 코드를 발생시킨다.

DiscountGoods 인터페이스

  • 할인 상품에 대한 인터페이스로, 할인 상품을 나타내는 구현체 내에 구현할 다음 메서드를 가지고 있다.
    • 할인 상품의 ID를 등록하는 메서드 : setGoodsID()
    • 할인 제품들의 정보를 반환하는 메서드 : getDiscountGoodsList()
    • 할인 상품의 할인율을 반환하는 메서드 : getDisocuntPercent()
public interface DiscountGoods {
    public void setGoodsID(Lisat<Integer> goodsIDList);
    public float getDiscountPercent();
    public List<GoodsVO> getDiscountGoodsList();
}

TimeEvent 인터페이스

  • 이벤트 시간에 대한 인터페이스, 할인 상품 중에 시간에 관련된 이벤트 상품에 필요한 다음 메서드를 가지고 있다.
    - 이벤트 기간을 설정하는 메서드 : setEventPeriod()
    • 현재 시간을 기준으로 이벤트 잔여 시간을 알려주는 메서드 : getLeftTime();
public interface TimeEvent {
    public void setEventPeriod(long beginTime, long finishTime);
    public long getLeftTime();
}

LastMinuteGoods 클래스

  • 마감이 임박한 할인 상품에 대한 구상 클래스 할인 상품에 대한 인터페이스와, 시간적 제약 요소를 정의하고 있는 인터페이스를 상속받아 구현체로 만들어졌다.

  • LastMinuteGoods.setSaleGoodsID()
    - 상품의 고유 ID를 받아서 DB 에 있는 데이터를 매핑한 후, 매핑된 데이터를 VO 객체로 전환하는 작업

  • LastMinuteGoods.setEventPeriod()
    - 이벤트 기간의 시작 시간과 종료 시간을 받아서 저장하고 잔여 시간을 계산하여 저장하며, 합당한 시작 시간과 종료 시간이 매개변수로 전달되었는지를 점검

import java.net.CacheRequest;
import java.security.DrbgParameters.Reseed;

public class LastMinuteGoods implements DiscountGoods, TimeEvent {
    private String eventTitle;
    private float salePercent;
    private long leftTime;
    private List<GoodsVO> saleGoods = new ArrayList();

    @Override
    public void setGoodsID(LisT<Integer> goodsIDList) {
        for(int goodsID : goodsIDList) {
            GoodsVO goodsVO = selectGoods(goodsID);
            addGoods(goodsVO);
        }
    }

    @Override
    public gloat getDiscountPercent() {
        return this.salePercent;
    }

    @Override
    public List<GoodsVO> getDiscountGoodsList() {
        return this.saleGoods;
    }

    @Override
    public void setEventPeriod(long beginTime, long finishTime) {
        boolean reasonable = initPeriod(beginTime, finishTime);
        if(reasonable) {
            long leftTime = calculateLeftTime();
            timeToEnroll(leftTime);
        }

        // 중략
    }

    @Override
    public long getLeftTime() {
        return calculateLeftTime();
    }

    // 중략
}

Recommend 클래스

  • 추천 상품들을 생성하고, 보여주는 역할을 하는 클래스
  • 시간과 관련된 이벤트 상품을 초기화하고 마감 할인 상품과 반짝 할인 상품을 추천하는 부분을 보여준다.
  • Recommend.initTimeEventGoods()
    - RecommendType에 따라 분기를 실행하여 해당 상품을 추천 상품으로 변경하기 위한 초기 구현 담당
  • Recommend.lastMinuteGoodsRecommendDisplay()
    - 마감 임박 할인 상품들을 전시하기 위한 메서드로, 매개변수로 전달된 LastMinuteGoods 객체를 통해 상품을 전시
  • Recommend.flashSaleGoodsRecommendDisplay()
    - 반짝할인 상품들을 전시하기 위한 메서드로, 매개변수로 전달된 FlashDiscountGoods 객체를 통해 삼품을 전시
public class Recommend {
    // 중략

    private void initTimeEventGoods(RecoomendType type, List<Integer> goodsIDList, long beginTime, long finishTime) {
        switch(type) {
            case LAST_MINUTE: {
                LastMinuteGoods goods = new LastMinuteGoods();
                goods.setGoodsID(goodsIDList);
                goods.setEventPeriod(beginTime, finishTime);
                addRecommendDataLastMinuteSaleGoods(goods);
            } break;
            case FLASH: {
                FlashDiscountGoods goods = new FlashDiscountGoods();
                goods.setGoodsID(goodsIDList);
                goods.setEventPeriod(beginTime, finishTime);
                addRecommendDataFlashSaleGoods(goods);
            } break;
        }
    }

    private boolean lastMinuteGoodsDisplay(LastMinuteGoods goods) {
        float percent = goods.getDiscountPercent();
        long leftTime = goods.getLeftTime();
        List<GoodsVO> saleGoodVOItemList = goods.getDiscountGoodsList();
        for(GoodsVO item : saleGoodsVOItemList) {
            // 중략
        }
        // 중략
    }

    private boolean flashSaleGoodsDisplay(FlashDiscountGoods goods) {
        float percent = goods.getDiscountPercent();
        long leftTime = goods.getLeftTime();
        List<GoodsVO> saleGoodVOItemList = goods.getDiscountGoodsList();
        for(GoodsVO item : saleGoodsVOItemList) {
            // 중략
        }
        // 중략
    }
}

레거시 코드 개선 과정

  • 문제가 발생하는 부분은 구상 클래스가 아닌, 구상 클래스를 사용하는 클래스를 구현하는 순간부터 문제가 발생한다.
    (Recommend 클래스를 구현할 때)

  • 이전의 Recommend 클래스의 세 개의 메서드에서 중복 부분이 있는 것을 볼 수 있을 것이다.

    • 하지만, 메서드 추출이나 중복 제거를 할 수 없다. (이는 하나의 타입으로 인스턴스를 만들지 못하기 때문)
  • 따라서 이를 해결하기 위해서는 가장 먼저 인터페이스의 구조를 살펴봐야 한다.

    • 현재는 다중 상속을 받고있는데, 이는 메서드를 규약으로 만들기 위한 방법이였다.
    • 하지만 TimeEvent를 인터페이스를 만들어 메서드명을 통일하려고 한 것부터 다시 생각해야 한다.
  • 규약이라는 측면에서 TimeEvent를 별도로 정의하였으나, DiscountGoods에 종속적인 인터페이스이다.

    • 종속 관계에 있는 두 개의 인터페이스가 별도로 정의되어 있어 불필요한 다중 상속이 되는 것.
    • 따라서 종속 관계를 상속 관계로 명확히 하고 하나의 인터페이스로 재정의할 필요가 있다.

개선 순서

  1. TimeEvent가 DiscountGoods를 상속받도록 한다.
  2. TimeEvent와 DiscountGoods를 상속받는 클래스들을 수정
  3. 2의 클래스를 사용하는 클래스의 중복 코드를 제거

TimeEvent가 DiscountGoods를 상속

public interface TimeEvent extends DisocuntGoods {
    public void setEventPeriod(long beginTime, long finishTime);
    public long getLeftTime();
}

TimeEvent와 DiscountGoods를 상속받는 클래스들을 수정

public class LastMinuteGoods implements TimeEventSaleGoods {
	// 중략
}
public class FlashDiscountGoods implements TimeEventSaleGoods {
	// 중략
}

FlashDiscountGoods, LastMinuteGoods 래스를 사용하는 클래스의 중복 코드를 제거

public class Recommend {
    // 중략

    private void initTimeEventGoods(RecoomendType type, List<Integer> goodsIDList, long beginTime, long finishTime) {
    	TimeEventSaleGoods goods = null;
        switch(type) {
            case LAST_MINUTE: 
              goods = new LastMinuteGoods();
              break;
            case FLASH: 
              goods = new FlashDiscountGoods();
              break;
        }
        goods.setGoodsID(goodsIDList);
        goods.setEventPeriod(beginTime, finishTime);
        addRecommedDataDeadLineSaleGoods(goods);
    }

    private boolean timeEventSaleGoodsDisplay(TimeEventSaleGoods goods) {
        float percent = goods.getDiscountPercent();
        long leftTime = goods.getLeftTime();
        List<GoodsVO> saleGoodVOItemList = goods.getDiscountGoodsList();
        for(GoodsVO item : saleGoodsVOItemList) {
            // 중략
        }
        // 중략
    }
}

개선된 레거시 코드

TimeEventSaleGoods 클래스

  • DiscountGoods 인터페이스르 상속받고 TimeEvent의 메서드와 똑같은 메서드를 멤버 메서드로 구현.
  • '타임 이벤트' 개념이 들어간 할인 상품의 기능 확장을 반영한 것.

LastMinuteGoods / FlashDiscountGoods 클래스

  • 기존에 상속받았던 DiscountGoods와 TimeEvent 인터페이스는 TimeEventSaleGoods 인터페이스로 대체되어 상속.

Recommend 크래스

  • TimeEventSaleGoods로 인스턴스화되어 활용함으로써 LastMinuteGoods와 FlashDiscountGoods의 구상 클래스들은 인스턴스로 만들어 사용하는 중복 코드들은 제거되었다.

요약 및 정리

단순히 메서드 규약으로 만들려고 인터페이스를 사용하는 것은 좋은 아이디어가 아니다.
- 잘못 사용하면 안티 패턴을 양산, 이 때 가장 크게 야기되는 문제가 중복 코드 발생과 유연성 저하

  • 이번 장에서는 인터페이스를 단순한 규약이 아닌 'SubType'으로써 기능의 확장 개념으로 변경하여 본래 의도에 맞게 구현.

유사한 기능의 인터페이스 다중 상속 구조를 개선하기 위한 생각의 흐름

  1. 같은 인터페이스를 다중 상속하는 구상 클래스가 여러 개인지 확인
  2. 구상 클래스를 사용하는 객체에 이와 관련된 중복 코드가 있는지 확인
  3. 다중 상속하는 인터페이스들이 하나의 타입으로 묶일 수 있다면, 새로운 인터페이스로 만들거나, 인터페이스 상속을 이용
  4. 구상 클래스를 사용하는 객체는 새로운 인터페이스로 인스턴스를 만들어 처리하는 구문을 만들고 이에 해당하는 중복 코드를 제거하고 모듈화를 진행

좋은 웹페이지 즐겨찾기