3장 입츨굼 내역 분석기 확장판 ( 전반부 )

2024. 10. 9. 21:57스터디/실전 자바 소프트웨어개발

목표

  • 코드베이스에 유연성을 추가하고 유지보수성을 개선하는 데 도움을 주는 개방/폐쇄 원칙(OCP) 을 배워보자
  • 언제 인터페이스를 사용해야 좋을지를 설명하는 일반적인 가이드라인과 높은 결합도를 피할 수 있는 상황도 배워보자

예제

// 특정금액 이하 입출금내역 조회하기
public List<BankTransaction> findTransactionsGreaterThanEqual(final int amount) {
    final List<BankTransaction> result = new ArrayList<>();
    for(final BankTransaction bt: bankTansactions) {
        if(bt.getAmount() >= amount) {
            result.add(bt);
        }
    }
    return result;
}

또는

// 특정금액 또는 특정 월로 입출금 내역을 조회
public List<BankTransaction> findTransactionsInMonthAndGreater(final Month month, final int amount) {
    final List<BankTransaction> result = new ArrayList<>();
    for(final BankTransaction bt: bankTansactions) {
        if(bt.getDate().getMonth() == month && bt.getAmount() >= amount) {
            result.add(bt);
        }
    }
    return result;
}
  • 위 코드 구현 방식에는 문제가 있다.
    • 거래 내역의 여러 속성을 조합할수록 코드가 점점 복잡해진다.
    • 반복 로직과 비즈니스 로직이 결합되어 분리하기가 어려워진다.
    • 코드를 반복한다.

해결방법

  1. 인터페이스를 이용해 위와 같은 결합을 제거
    • BankTransactionFilter 인터페이스를 만들어 문제를 해결.
      //함수형 인터페이스
      @FunctionalInterface
      public interface BanktransactionFilter {
      boolean test(BankTransaction bt);
      }
// 개방/폐쇄 원칙을 적용 한 findTransaction 메서드
public List<BankTransaction> findTransactions(final BankTransactionFilter btf) {
    final List<BankTransaction> result = new ArrayList<>();
    for(final BankTransaction bt: bankTansactions) {
        if(btf.test(bt)) {
            result.add(bt);
        }
    }
    return result;
}

함수형 인터페이스 인스턴스 만들기

class BankTransactionIsInFebruaryAndExpensive implements BankTransactionFilter {
    @Override
    public boolean test(final BankTransaction bt) {
        return bt.getDate().getMonth() == Month.FEBRUARY 
            && bt.getAmount() >= 1_000);
    }
}

특정 BankTransactionFilter 구현으로 findTransactions() 호출하기

final List<BankTransaction> bts = bankStatemetnProcessor.findTransactions(new BankTransactionIsInFebruaryAndExpensive());
  • 확실히 더 객체지향적이고, 코드가 깔끔해졌다.

갓 인터페이스

  • 한 인터페이스에 모든 기능을 추가하는 형식
    interface BankTransactionProcessor {
      double calculateTotalAmount();
      double calculateTotalInMonth(Month month);
      double calculateTotalInJanuary();
      List<BankTransaction> findTransactions(BankTransactionFilter btf);
      ...
    }

    문제점

  • 자바의 인터페이스는 모든 구현이 지켜야할 규칙을 정의한다.
    즉 구현 클래스는 인터페이스에서 정의한 모든 연산의 구현코드를 제공해야함. 따라서 인터페이스를 바꾸면 이를 구현한 코드도 바뀐 내용을 지원하도록 갱신해야한다. 더 많은 연산을 추가할 수록 더 자주 코드가 바뀌며, 문제가 발생할 수 있는 범위도 넓어진다.
  • 월, 카테고리 같은 BankTransaction 속성이 calculateAverageForCategory(), calculateTotalInJanuary() 처럼 메서드 이름의 일부로 사용되었다.
    인터페이스가 도메인 객체 특정 접근자에 종속되는 문제가 생김.
    → 도메인 객체의 세부 내용이 바뀌면 인터페이스도 바뀌어야하기 때문에 결과적으로 구현코드도 바뀌어야한다.

지나친 세밀함

그렇다면 위에서 말했듯이 갓 인터페이스가 되면 문제이니, 작을수록 좋은걸까?

interface CalculateTotalAmount {
    double calculateTotalAmount();
}

interface CalculateAverage {
    double calculateAverage();
}

...
  • 지나치게 인터페이스가 세밀해도 코드 유지보수에 방해가 된다. 실제로 위 예제는 안티 으집도 문제가 발생한다. 더욱이 인터페이스가 너무 세밀하면 복잡도가 높아지며, 새로운 인터페이스가 계속해서 프로젝트에 추가된다.

명시적API vs 암묵적API

  • 위에서 말한 BankTransactionProcessor는 다양한 구현을 기대하지 않으므로 인터페이스의 필요성이 사라진다. 결론적으로 코드베이스에 불필요한 추상화를 추가해 일을 복잡하게 만들 필요가 없다.
    BankTransactionProcessor는 단순히 입출금 내역에서 통계정 연산을 수행하는 클래스일 뿐이다.
  • 보통 findTransactions() 메서드를 쉽게 정의할 수 있는 상황에서 findTransactionsGreaterThanEqul() 처럼 구체적으로 메서드를 정의해야 하는지 의문이 생긴다. 이러한 딜레마가 명시적 API vs 암묵적 API 이다.
  • 상황에 따라 처리하는게 베스트..?