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'의 중복 코드를 제거
 
 
 
- 이 밖에도 단순한 규약으로만 만들어진 인터페이스는 전혀 의도하지 않은 클래스에 상속되어 사용되기도 하고, 인터페이스를 사용하지 않고 구현체에 직접 메서드를 구현하는 등의 부작용을 낳기도 한다.
- 이러한 부작용이 생기는 가장 큰 이유는 상속받은 구현체를 사용하는 객체가 인터페이스가 아닌, 구현체 인스턴스를 사용하기 때문.
 
 
- 결국 규약으로 만들어진 인터페이스는 처음 의도와는 다르게 가독성을 떨어뜨리는 주범이 되고, 시간이 흐를수록 애물단지가 된다.
개선방향
규약으로 만들어진 다중 상속이 구조가 아닌, 인터페이스가 인터페이스를 상속하도록 구조 변경
다중 상속
- 인터페이스 상속을 사용하는 구현체 중 '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에 종속적인 인터페이스이다. - 종속 관계에 있는 두 개의 인터페이스가 별도로 정의되어 있어 불필요한 다중 상속이 되는 것.
- 따라서 종속 관계를 상속 관계로 명확히 하고 하나의 인터페이스로 재정의할 필요가 있다.
 
개선 순서
- TimeEvent가 DiscountGoods를 상속받도록 한다.
- TimeEvent와 DiscountGoods를 상속받는 클래스들을 수정
- 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'으로써 기능의 확장 개념으로 변경하여 본래 의도에 맞게 구현.
유사한 기능의 인터페이스 다중 상속 구조를 개선하기 위한 생각의 흐름
- 같은 인터페이스를 다중 상속하는 구상 클래스가 여러 개인지 확인
- 구상 클래스를 사용하는 객체에 이와 관련된 중복 코드가 있는지 확인
- 다중 상속하는 인터페이스들이 하나의 타입으로 묶일 수 있다면, 새로운 인터페이스로 만들거나, 인터페이스 상속을 이용
- 구상 클래스를 사용하는 객체는 새로운 인터페이스로 인스턴스를 만들어 처리하는 구문을 만들고 이에 해당하는 중복 코드를 제거하고 모듈화를 진행
Author And Source
이 문제에 관하여(Java Refactoring -12, 유사한 기능의 인터페이스 다중 상속 구조 개선), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@tyur89/Java-Refactoring-12-유사한-기능의-인터페이스-다중-상속-구조-개선저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
                                
                                
                                
                                
                                
                                우수한 개발자 콘텐츠 발견에 전념
                                (Collection and Share based on the CC Protocol.)