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.)