-
입출금 내역 분석기(2)언어/객체지향 2024. 5. 26. 19:20
1. 목표
- 기존 입출금 내역 분석기를 개선
- 코드 베이스 유연성 및 유지보수 개선을 위해 개방/폐쇄 원칙 적용
- 언제 인터페이스를 사용해야 좋을지 일반적 가이드라인 및 높은 결합도 피할 수 있는 기법 학습
- 자바 예외처리 방법
2. 확장된 입출금 내역 분석기 요구사항
- 특정 입출금 내역을 검색할 수 있는 기능 ex 주어진 날짜 범위 또는 특정 범주의 입출금 내역얻기
- 검색 결과의 요약 통계를 텍스트,HTML 등 다양한 형식으로 만들기
3. 개방/폐쇄 원칙
- 기존 코드를 변경하지 않으면서, 기능을 추가할 수 있도록 설계 되어야 한다는 원칙
- 확장에는 개방적이고, 수정에는 폐쇄적 (확장은 새로운 기능 추가를 의미한다.)
- 즉, 기능 추가 요청이 오면, 클래스 확장 통해 손쉽게 구현하면서 확장에 따른 수정은 최소화
- OCP는 추상화를 의미한다고 보면 된다. 즉, 다형성과 확장을 가능케 하는 객체지향의 장점을 극대화하는 설계 원칙이다.
- OCP를 위해 다음 절차를 생각해보자
- 변경(확장)될 것과 변하지 않는 것을 엄격하게 구분한다.
- 두 모듈이 만나는 지점(한 클래스를 사용하는 지점) 추상화(추상클래스 or 인터페이스)를 정의한다.
- 구현체에 의존하기보다 추상화에 의존하도록 코드를 작성한다.
3.1 입출금 내역 분석기에 적용
- 특정 금액 이상의 모든 입출금 내역을 검색하는 메서드를 구현한다고 하자
- 이때 해당 메서드를 어디에 포함시켜야할까? 해당 메서드를 처리하는 클래스를 만드는 것은 비효율적
- 이미 BankTeansactionProcessor가 정의되어 있음으로, 여기에 해당 메서드를 포함시키자
public List<BankTransaction> findTransactionsGreaterThanEqual(final int amount){ final List<BankTransaction> result = new ArrayList<>(); for(final BankTransaction bank : bankList){ if(bank.getAmount() >= amount){ result.add(bank); } } return result; }
- 해당 메서드는 잘 작동하지만, 생각해 볼 지점이 있다. 특정 월에 입출금 내역 혹은 특정 금액 이상의 입출금 내역을 구하는 로직이 조건문만 다르고 똑같다.
- 이때 특정 월, 특정 금액으로 입출금 내역을 검색하라는 요구사항이 추가되면 코드 복붙이 일어날 것이다.
- 이에 따라 processor가 개방/폐쇄 원칙에 알맞다고 보기 힘들다.
- 새로운 요구사항 등장시 코드가 복잡해진다.
- 반복 로직과 비즈니스 로직이 결합되어 분리하기 어려워진다.
- 코드를 반복한다.
- 먼저, 비즈니스 로직과 반복 로직을 인터페이스를 통해 분리하자
- BankTransaction의 선택 조건을 검사하는, BankTransactionFilter 인터페이스를 추가하자
@FunctionalInterface public interface BankTransactionFilter { boolean test(BankTransaction bankTransaction); }
- 반복 if문에 이를 적용시켜, 손쉽게 기능 확장이 가능하다.
public List<BankTransaction> findTransactions(final BankTransactionFilter bankTransactionFilter){ //변경이 발생하지 않는 부분 final List<BankTransaction> result = new ArrayList<>(); for(final BankTransaction bank : bankList){ //변경이 발생하는 지점 중 두 모듈이 접촉하는 부분 인터페이스화 if(bankTransactionFilter.test(bank)){ result.add(bank); } } return result; }
- find에 손쉽게 조건을 추가해서 쉽게 확장 가능해진다.
- 개방 폐쇄 원칙을 통해
- 기존 코드를 바꾸지 않으므로, 기존 코드가 잘못될 가능성이 줄어든다.
- 코드가 중복되지 않으므로, 기존 코드 재사용성이 높아진다.
- 결합도가 낮아지므로, 코드 유지보수성이 좋아진다.
4. 인터페이스 문제
- 개방 폐쇄 원칙을 위해 인터페이스를 남발하는 것은 좋지 못하다.
//갓 인터페이스 interface BankTransactionProcessor{ double calulateTotalAmount(); double calulateTotalAmountInMonth(Month month); //.. List<BankTranscation> findTransactions(BankTransactionFilter bankFilter); } // 지나치게 작은 인터페이스 interface CalculateTotalAmount{ double caluateTotalAmount(); } interface CalculateAverager{ double calculateAverage(); }
- 갓 클래스가 되는 것을 피하듯 갓 인터페이스가 되는 것 또한 피해야한다.
- 모든 연산이 명시적인 API 정의에 포함되어, 인터페이스가 복잡해진다.
- 자바의 인터페이스는 모든 구현이 지켜야 할 규칙을 정의한다. 즉, 구현 클래스는 인터페이스를 무조건 구현해야한다. 따라서, 인터페이스가 복잡해질 수록 더 자주 코드가 바뀔 수 있는 위험이 있다.
- 또한, 인터페이스가 특정 도매인 객체의 특정 속성에 종속적이게 되는 문제점도 발생할 수 있다.
- 지나치게 작은 인터페이스도 지양해야한다. 안티 응집도 문제가 발생한다.
- 기능이 여러 인터페이스에 분산되어, 필요 기능을 찾기 힘들어 질 수 있다. 자주 사용하는 기능일 수록 찾기 쉬워야 유지보수성이 좋아진다.
- 개방/폐쇄 원칙을 적용하면 연산에 유연성을 추가하고, 공통적인 상황을 클래스로 정의할 수 도있다. 이 관점에서 BankTransactionProcessor를 인터페이스화해서 다양한 구현체를 뽑아야할 필요가 없기 때문에 불필요한 추상화를 해서 복잡하게 일을 만들 필요가 없다. 해당 클래스는 단순히 입출금 내역에서 통계적 연산을 수행하는 클래스일 뿐이다.
4.1 명시적 API vs 암묵적 API
- findTransactions()와 findTransactionsGreaterThanEqual()로 메서드를 정의할 수 있는 상황에서 뭘 사용해야할지 고민할 수 도 있다. (일반적 vs 구체적)
- 이를 명시적 API와 암묵적 API 제공 문제라고 부른다.
- 먼저 구체적인 API 이름은 가독성이 좋지만, 변경 사항 발생 시 매번 새로 추가해야한다
- 일반적 API 이름은 새로 추가할 필요는 없지만, 사용자가 판단이 어려워 문서화를 잘해야한다.
* 명시적 API : 메서드로 정의된 이름으로 쉽게 추적 가능 해당 기능 외 수행하지 않는 메서드,
암묵적(묵시적) API : 메서드로 정의된 이름 외 다양한 추가기능 수행할 수 있음
5. 다양한 형식으로 내보내기
- 다양한 형식으로 내보내야한다면, 요약 정보를 대표하는 도메인 객체를 먼저 만들자
public class SummaryStatistics { private final double sum; private final double max; private final double min; private final double average; public SummaryStatistics(final double sum, final double max, final double min, final double average){ this.sum = sum; this.max = max; this.min = min; this.average = average; } public double getSum() { return sum; } public double getMax() { return max; } public double getMin() { return min; } public double getAverage() { return average; } }
- 다양한 형식으로 결과를 제공하기 위해 개방/폐쇄 원칙에 맞춰 아래와 같은 인터페이스를 정의하자
public interface Exporter { String export(SummaryStatistics summaryStatistics); }
- 반환값이 void가 아닌 String인 이유는 void로 두면, 해당 메서드가 뭘 반환했는지 정보를 얻기 어렵기 때문이다.
- 또한, 테스트를 어렵게 만들 수 있다. 따라서, 뭐로 반환했는지 정보를 출력하기 위해 String을 리턴하도록 했다.
6. 예외 처리
자바는 두가지 예외 클래스가 존재한다.
확인 (Exception): 회복해야하는 대상이다. 반드시 try/catch처리를 해줘야한다.
미확인 (Error/RuntimeException) : 프로그램을 실행하면서, 언제든 발생할 수 있는 예외이다. 확인된 예외와 달리 메서드 시그니처에 명시적으로 오류를 선언하지 않으면, 호출자도 이를 꼭 처리할 필요가 없다.
6.1 예외의 패턴과 안티패턴
- 미확인 예외와 확인된 예외 선택하기
- 둘을 선택할 때 예외 발생시 프로그램이 회복하도록 강제할 것인지, 아닌지 생각해보면 된다.
- 예를 들어 일시적 오류라면 동작 오류 혹은 사용자가 회복할 수 없는 오류
- 비즈니스 로직 검증(잘못된 형식이나 연산 등)은 미확인 예외로 처리한다.
- 사용자가 회복 시킬 수 없는 에러 (저장 공간이 꽉찼다던가 등) 미확인 예외로 지정한다.
- 예외 패턴
- 안티패턴
- 과도하게 세밀하거나 과도하게 덤덤한 패턴
-
- 메서드에 지정된 예외는 모두 사용자 정의 예외이다. 너무 세세하게 예외를 지정하면, 모든 예외를 처리해야하는 사용자 입장에서 생산성이 떨어질 수 있다.
- 그렇다고 모든 예외상황을 하나의 Exception에서 처리하는 것도 사용자에 혼동을 줄 수 있다.
-
public class OverlySpecificValidator{ public boolean validate throws DescriptionTooLongException, InvalidDateFromat,DateIntheFutureException { //.. } }
- 노티피케이션 패턴
- 너무 많은 미확인 예외를 사용하는 상황에 적합하다.
- 도메인 클래스로 오류를 수집한다.
public class Notification { private final List<String> errors = new ArrayList<>(); public void addError(final String message){ errors.add(message); } public boolean hasErrors(){ return !errors.isEmpty(); } public String errorMessage(){ return errors.toString(); } public List<String> getErrors(){ return this.errors; } }
- 해당 객체를 가지고, 예외를 던지지 않고, 수집해서 Notification을 전달하여 처리하도록 한다.
public Notification validate(){ final Notification notification = new Notification(); if(this.description.length()>100){ notification.addError("the description is too long"); } final LocalDate parsedDate; try{ parsedDate = LocalDate.parse(this.date); if(parseDate.isAfter(LocalDate.now()){ notification.addError("date cannot be in the future"); } }catch(DateTimeParseException e){ notification.addError("Invalid format for date"); } //.. return notification }
- 예외 사용시 주의점
- 예외 문서화도 엄청 중요하다 @throws를 통해 예외에 대해 잘 정리해두자
- 특정 구현에 종속된 예외를 던지는 것은 위험하다
- ex OracleException 같이 예외를 던지면, 코드도 오라클에 종속됨
- 예외로 흐름을 제어하지 않는다. 이는 예외를 남용하는 나쁜 사례이다.
- 예외를 사용해서 특정 루프를 탈출하도록 하는 짓
7. 예외 대안 기능
- 특정 로직에서 문제 발생시 null 사용 -> 절대 사용하지 말자.(호출자에게 아무런 정보도 제공하지 않는다)
- null 객체 패턴
- 객체가 존재하지 않을 때 null 래퍼런스 반환하는 대신 인터페이스를 구현하는 객체를 반환하는 기법
- Optional<T>
- 값이 없는 상태를 표현하는 내장 데이터 형식이다.
- 값이 없는 상태를 명시적으로 처리하는 다양한 메서드 집합을 제공해서 좋다!
- Try<T>
- 성공하거나 실패할 수 있는 연산을 가리키는 Try<T>데이터 형식도 있다.
- Optional<T>와 비슷하지만, 값이 아니라 연산에 적용한다.
- JDK에서 제공하는 기능이 아니므로, 외부라이브러리를 사용해야한다.
'언어 > 객체지향' 카테고리의 다른 글
입출금 내역 분석기 (1) - 응집도와 결합도에 따른 클래스 분리 (0) 2024.05.24 객체 지향과 디자인 패턴 - 다형성과 추상 타입 (0) 2023.08.13 객체지향과 디자인패턴 2 (0) 2023.08.13 객체지향과 디자인패턴 1 (0) 2023.08.11 객체지향의 사실과 오해 3 (0) 2023.08.02