설계

인터페이스, 추상 클래스, 합성 사용 시점

mint* 2024. 12. 2. 02:50
728x90

인터페이스, 추상 클래스, 합성 사용 시점

인터페이스 사용 시점

  • 서로 독립적으로 존재하지만 공통된 행위(계약)을 가진 객체들에게 적합하다.
    • 상호작용하지 않는 객체
  • 부모가 아닌 구현체 둘이 함께 고려되어야 하는 상황이 거의 없다면 사용하자.
    • 구현체 둘이 함께 고려되어야한다면 합성 사용하기

 

크리스마스) 할인 정책

// 증정 정책에 대한 공통 행위(규약)을 정의
interface Gift {
    boolean isApplicable();          
    BigDecimal calculateAmount();     
    Map<Menu, Quantity> provideGiftItems();  
    String getName();                
}

class ChampagneGift implements Gift {
    private static final int MIN_ORDER_PRICE = 120_000;
    private final Orders orders;

    @Override
    public boolean isApplicable() {
        return orders.isTotalPriceOverThan(BigDecimal.valueOf(MIN_ORDER_PRICE));
    }

    @Override
    public Map<Menu, Quantity> provideGiftItems() {
        if (isApplicable()) {
            return Map.of(Menu.샴페인, new Quantity(1));
        }
        return Collections.emptyMap();
    }
}

// 디저트 3개 이상 구매시 보너스 케이크 증정
class DessertBonusGift implements Gift {
    private static final int MIN_DESSERT_COUNT = 3;
    private final Orders orders;

    @Override
    public boolean isApplicable() {
        return orders.getDessertCount() >= MIN_DESSERT_COUNT;
    }

    @Override
    public Map<Menu, Quantity> provideGiftItems() {
        if (isApplicable()) {
            return Map.of(Menu.케이크, new Quantity(1));
        }
        return Collections.emptyMap();
    }
}
  1. 독립적으로 동작한다.
    • 샴페인 증정과 케이크 증정은 서로 영향을 주지 않는다.
      • 각 증정 정책은 자신만의 조건과 증정 품목을 가진다.
  2. 공통된 행위(인터페이스)를 가진다.
    • 모든 증정 정책은 적용 가능 여부 확인(isApplicable())
    • 증정할 상품 제공(provideGiftItems())
    • 증정 상품의 가치 계산(calculateAmount())
  3. 서로 다른 구현 방식
    • 각각 구현 방식이 다르다.
  4. 확장 용이성
    • 새로운 증정 정책 추가가 쉽다. (인터페이스만 구현하면 됨)
    • 기존 코드 수정 없이 새로운 정책 추가 가능

 

추상 클래스 사용 시점

  • 서로 밀접하게 연관되어 있고 공통된 상태와 로직을 가지며 기본 구현을 공유할 필요가 있으면 추상 클래스를 사용하는 것이 더 적절하다.
  • 부모를 상속한 구현체끼리 함께 사용되지 않고, 부모 클래스와 강한 연관이 있을때 사용하자.
    • 공통된 상태, 행위를 부모로부터 물려받음

 

크리스마스) 할인 정책

크리스마스 할인 정책들은 서로 밀접하게 연관되어 있고
공통된 상태와 로직을 가지며
기본 구현을 공유할 필요가 있어서 추상 클래스를 사용하는 것이 더 적절하다.

  1. 공통된 상태
    • dayorders는 모든 할인 정책에서 공유하는 필드이다.
    • 최소 주문 금액 조건도 공통으로 사용된다.
  2. 공통된 로직
    • isApplicable()처럼 할인 적용 가능 여부 확인 로직이 동일하다.
      • 추상 클래스에서 기본 구현을 제공하고 하위 클래스에서 재사용된다.
  3. 밀접한 관계
    • 모든 할인 정책이 주문과 날짜에 의존
    • 할인 계산과 적용 조건이 서로 연관됨

 

// 크리스마스 할인 정책들의 공통 부분을 담당하는 추상 클래스
public abstract class Discount {

    private static final int MIN_AMOUNT = 10_000;

	protected final Day day;
    protected final Orders orders;

    public Discount(final Day day, final Orders orders) {
        this.day = day;
        this.orders = orders;
    }

    // 공통된 할인 적용 조건 검사
    public boolean isApplicable() {
        return isMoreThanMinimumOrderAmount(orders) && isWithinDeadline();
    }

    // 각 할인 정책마다 다른 부분
    public abstract BigDecimal calculateAmount();
    public abstract String getName();
    protected abstract boolean isWithinDeadline();
}

// 디데이 할인
public class ChristmasDdayDiscount extends Discount {

    private static final int DISCOUNT_START_PRICE = 1_000;
    private static final int DISCOUNT_DAY_UNIT = 100;

    public ChristmasDdayDiscount(final Day day, final Orders orders) {
        super(day, orders);
    }

    @Override
    public BigDecimal calculateAmount() {
        if (isApplicable()) {
            BigDecimal discount = BigDecimal.valueOf(DISCOUNT_START_PRICE);
            BigDecimal untilChristmas = new BigDecimal(DISCOUNT_DAY_UNIT).multiply(
                    new BigDecimal(day.diffFromFirstDay()));
            return discount.add(untilChristmas);
        }
        return BigDecimal.ZERO;
    }

    @Override
    public String getName() {
        return "크리스마스 디데이 할인";
    }

    @Override
    protected boolean isWithinDeadline() {
        return !day.isExceedChristmas();
    }
}

public class DayDiscount extends Discount {

    private static final int DISCOUNT_AMOUNT = 2_023;
    private static final String WEEKDAYS_DISCOUNT = "평일 할인";
    private static final String WEEKEND_DISCOUNT = "주말 할인";

    public DayDiscount(final Day day, final Orders orders) {
        super(day, orders);
    }

    @Override
    public BigDecimal calculateAmount() {
        if (isApplicable()) {
            if (day.isWeekday()) {
                return BigDecimal.valueOf(DISCOUNT_AMOUNT * orders.countSameTypeMenu(MenuType.DESSERT));
            }
            return BigDecimal.valueOf(DISCOUNT_AMOUNT * orders.countSameTypeMenu(MenuType.MAIN));
        }
        return BigDecimal.ZERO;
    }

    @Override
    public String getName() {
        if (day.isWeekday()) {
            return WEEKDAYS_DISCOUNT;
        }
        return WEEKEND_DISCOUNT;
    }

    @Override
    protected boolean isWithinDeadline() {
        return day.isInDecember();
    }
}

 

인터페이스를 사용한다면?

interface DiscountPolicy {
    BigDecimal calculateAmount();
    boolean isApplicable();
    String getName();
}
  1. 공통 상태와 로직을 각각 구현해야 한다.
  2. 코드 중복이 발생한다.

 

강결합 -> 합성 사용하기

강한 결합 관계

두 객체가 서로 밀접하게 연관되어 함께 동작해야 하는 관계

예시: 편의점 재고 관리 시스템

일반 재고와 프로모션 재고

  • 재고 차감시 항상 두 재고를 함께 확인해야 한다.
  • 프로모션 재고를 우선적으로 사용한다.
  • 총 재고량은 두 재고의 합으로 계산한다.

 

인터페이스 사용시 문제점

interface Stock {
    void subtract(int quantity);
    int getQuantity();
}

class RegularStock implements Stock {
    private int quantity;

    @Override
    public void subtract(int quantity) {
        this.quantity -= quantity;
    }
}

class PromotionStock implements Stock {
    private int quantity;
    private Promotion promotion;

    @Override
    public void subtract(int quantity) {
        this.quantity -= quantity;
    }
}

// 사용하는 코드
class OrderService {
    public void order(Stock stock, int quantity) {
        // 문제점 1) instanceof 사용
        if (stock instanceof PromotionStock) {
            // 프로모션 재고 처리
        }

        // 문제점 2) 두 재고를 함께 고려해야 할 때 복잡해짐
        int totalQuantity = regularStock.getQuantity() + promotionStock.getQuantity();
        if (totalQuantity < quantity) {
            throw new IllegalArgumentException();
        }

        // 문제점 3) 재고 차감 우선순위 처리가 어려움
        if (promotionStock.getQuantity() > 0) {

        }
    }
}

 

문제점

  1. 타입 체크 남용 : instanceof
    • instanceof를 사용한다.
    • 새로운 재고 타입 추가시 if문 추가해야한다.
  2. 책임 분산 : stock - RegularStock, PromotionStock
    • 재고 관리 로직이 여러 곳에 흩어진다.
    • 비즈니스 로직을 수행하기 어렵다.
  3. 코드가 복잡해짐
    • 여러 객체의 상태를 동시에 관리해야 한다.

 

합성을 통해 해결하기

    • 밀접한 관계가 있는 개념들을 하나의 클래스 안에서 합성으로 관리하는 것이 좋다.
항상 함께 고려되거나, 서로 강한 결합관계를 가질때 합성을 사용하자.
public class Stock {

    private final PromotionStock promotionQuantity;
    private final RegularStock regularQuantity;

    public Stock(int promotionQuantity, int regularQuantity) {
        this.promotionQuantity = promotionQuantity;
        this.regularQuantity = regularQuantity;
    }

    // 재고 차감 로직을 Stock 클래스 한 곳에서 수행 가능
    public Stock subtract(int quantity) {
        if (getTotalQuantity() < quantity) {
            throw new IllegalArgumentException("재고 부족");
        }

        int remainingQuantity = quantity;
        int newPromotionQuantity = promotionQuantity;
        int newRegularQuantity = regularQuantity;

        // 프로모션 재고 우선 사용
        if (promotionQuantity > 0) {
            int promotionUsed = Math.min(remainingQuantity, promotionQuantity);
            newPromotionQuantity -= promotionUsed;
            remainingQuantity -= promotionUsed;
        }

        // 남은 수량은 일반 재고에서 차감
        if (remainingQuantity > 0) {
            newRegularQuantity -= remainingQuantity;
        }

        return new Stock(newPromotionQuantity, newRegularQuantity);
    }

    public int getTotalQuantity() {
        return promotionQuantity + regularQuantity;
    }
}

 

합성의 장점

class OrderService {
   public void order(Stock stock, int quantity) {
       Stock updatedStock = stock.subtract(quantity);
   }
}
  • 캡슐화
    • 재고 관리의 모든 책임이 Stock 클래스에 집중되어, 새로운 재고 타입 추가시 Stock 클래스 내부만 수정하면 된다.
    • 내부 구현을 외부로부터 숨김으로써 캡슐화를 지킨다.
  • 재고 차감 우선순위가 한 곳에서 관리됨으로써 로직을 수행하기 편하다. 

강한 결합 관계에 있는 객체들은 합성을 통해 하나의 클래스로 관리하자!

 

결론

추상 클래스 - 클래스 상속

abstract class Discount { ... }
class ChristmasDiscount extends Discount { ... }
class WeekdayDiscount extends Discount { ... }
  • 공통된 상태와 행위를 부모로부터 물려받아 사용한다.
    • 부모 클래스(Discount)와 강한 연관관계를 가진다.
  • 함께 사용되지 않을때 사용한다.

추상 클래스, 일반 클래스 사용시점

  • 일반 클래스 사용시점 : 객체 일부가 Default class로 사용될때

 

인터페이스

interface Payable { ... }
class CardPayment implements Payable { ... }
class BankTransfer implements Payable { ... }
  • 구현체들은 서로 독립적으로 동작한다.
    • 부모와도 느슨한 결합 (구현만 강제)
  • 함께 사용될 일이 거의 없을때 사용한다.

 

합성

class Stock {
    private final int promotionQuantity;
    private final int regularQuantity;
}
  • 항상 함께 고려되어야 할때
    • 서로 강한 결합관계를 가진다.
    • 두 개념(promotionQuantity, regularQuantity)을 한 클래스에서 함께 관리된다.

 


  • 인터페이스: 구현체들이 서로 독립적, 부모와도 느슨한 결합
  • 추상클래스: 구현체들은 독립적이지만, 부모와 강한 결합
  • 합성: 두 개념이 강한 결합으로 함께 관리될때
728x90