Study/java

실전 자바 소프트웨어 개발 정리 - 2

에디개발자 2020. 11. 15. 14:24
반응형

모든 소스는 github에 올려두었습니다.

나를 닮았다고 한다....

입출금 내역 분석기 확장판

목표

  • 코드베이스에 유연성을 추가하고 유지보수성을 개선하는 데 도움을 주는 개방/패쇄 원칙(open/closed principle (OCP)) 을 배운다.
  • 언제 인터페이스를 사용해야 좋을지를 설명하는 일반적인 가이드라인과 높은 결합도를 피할 수 있는 기법도 배운다.
  • 자바에서 언제 API에 예외를 포함하거나 포함하지 않을지를 결정하는 자바의 예외 처리 방법을 배운다.
  • 메이븐, 그레이들 같은 검증된 빌드 도구를 이용해 자바 프로젝트를 시스템적으로 빌드하는 방법도 배운다.

개방/패쇄 원칙

  • 특정 금액 이상의 모든 입출금 내역을 검색하는 메서드를 구현해보자.
  • 간단한 findTransactions() 메서드를 포함하는 BankTransactionFinder 클래스를 따로 만들 수 있다. 하지만 2장에서 이미 BankTransactionProcessor 클래스를 선언했다. 그럼 어떻게 해야할까? 지금과 같은 상황에서는 메서드를 추가하려고 클래스를 새로 만들어도 크게 좋은 점이 없다. 새로 클래스를 추가한 탓에 여러 이름이 생기면서 다양한 동작 간의 관계를 이해하기가 어려워지고 전체 프로젝트가 복잡해지기 때문이다.
  • 이런 메서드는 일종의 처리 기능을 담당하므로 BankTransactionProcessor 클래스 안에 정의하면 나중에 관련 메서드를 조금 더 쉽게 찾을 수 있다.
  • BankTranscationFilter 인터페이스는 BankTransaction의 선택 조건을 결정한다. BankTransactionFilter 인터페이스를 이용하도록 findTransactions() 메서드를 리팩터링한다.
  • 변경 없이도(Closed) 확장성은 개방(Open)된다.

함수형 인터페이스

  • 한 개의 추상 메서드를 포함하는 인터페이스를 함수형 인터페이스라고 한다.
  • 자바 8에서는 이와 같은 문제를 더욱 쉽게 해결할 수 있도록 java.util.function.Predicate<T>라는 제네릭 인터페이스를 제공한다.

인터페이스 문제

갓 인터페이스

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

지나친 세밀함

  • 인터페이스는 작을수록 좋은 걸까???
  • 지나치게 인터페이스가 세밀해도 코드 유지보수에 방해가 된다. 기능이 여러 인터페이스로 분산되므로 필요한 기능을 찾기 어려운 현상을 안티 응집도라 한다. 자주 사용하는 기능을 쉽게 찾을 수 있어야 유지보수성도 좋아진다.
  • 인터페이스가 너무 세밀하면 복잡도가 높아지며, 새로운 인터페이스가 계속해서 프로젝트에 추가된다.

명시적 API vs 암묵적 API

개방/패쇄 원칙을 적용하면 연산에 유연성을 추가하고 가장 공통적인 상황을 클래스로 정의할 수 있다.

  • 명시적 API
    • findTransactionsGreaterThanEqual() 같은 메서드는 자체적으로 어떤 동작을 수행하는지 잘 설명되어 있고, 사용하기 쉽다. API의 가독성을 높이고 쉽게 이해하도록 메서드 이름을 서술적으로 만들었다. 하지만 이 메서드의 용도가 특정상황에 국한되어 각 상황에 맞는 새로운 메서드를 많이 만들어야 하는 상황이 벌어진다.
  • 암묵적 API
    • findTransactions() 같은 메서드는 처음 사용하기가 어렵고, 문서화를 잘해놓아야한다. 하지만 거래 내역을 검색하는 데 필요한 모든 상황을 단순한 API로 처리할 수 있다.

도메인 클래스 vs 원싯값

  • 원싯값
    • BankTransactionSummarizer의 인터페이스를 간단하게 정의하면서 double이라는 원싯값을 결과로 반환하는데, 이는 일반적으로 좋은 방법이 아니다. 원싯값은 다양한 결과를 반환할 수 없어 유연성이 떨어지기 떄문이다.
  • 도메인 클래스
    • double 값싸는 새 도메인 클래스 Summary를 만들면 원싯값 문제를 해결할 수 있다. 새 클래스에서 필요한 필드와 결과를 언제든 추가할 수 있다. 또한 이 기법을 이용하면 도메인의 다양한 개념간의 결합을 줄이고, 요구 사항이 바뀔 때 연쇄적으로 코드가 바뀌는 일도 최소화할 수 있다.

도메인 객체 소개

숫자

  • 결과값으로 double을 반환하면 가장 간단하게 프로그램을 구현할 수 있지만 요구 사항이 바뀔 떄 유연하게 대처할 수 없다.

컬렉션

  • Iterable을 반환하면 상황에 맞춰서 처리하기 때문에 유연성을 높일 수 있다. 이때 유연성은 좋아지지만 오직 컬렉션만 반환해야 하는 제약이 따른다.

특별한 도메인 객체

  • 사용자가 내보내려는 요약 정보를 대표하는 새로운 개념의 객체를 만들 수 있다. 도메인 객체는 자신의 도메인과 관련된 클래스의 인스턴스다. 도메인 객체를 이용하면 결합을 꺨 수 있다. 새로운 요구 사항이 생겨서 추가 정보를 내보내야 한다면 기존 코드를 바꿀 필요 없이 새로운 클래스의 일부로 이를 구현할 수 있다.

더 복잡한 도메인 객체

  • Report 처럼 조금 더 일반적이며 거래 내역 컬렉션 등 다양한 결과를 저장하는 필드를 포함하는 개념을 만들 수 있다. 사용자의 요구 사항이 무엇이며 더 복잡한 정보를 내보내야 하는지 여부에 따라 사용할 도메인 객체가 달라진다. 어떤 상황이든 Report 객체를 생산하는 부분과 이를 소비하는 부분이 서로 결합하지 않는다는 큰 장점이 있다.

적절하게 인터페이스를 정의하고 구현하기

인터페이스의 나쁜 예

public interface Exporter{ void export(SummaryVo summaryVo); }

public interface Exporter{ 
	void export(SummaryVo summaryVo); 
}

이렇게 정의하면 다음과 같은 문제가 발생한다.

  • void 반환 형식은 아무 도움이 되지 않고, 기능을 파악하기도 어렵다. 메서드가 무엇을 반환하는지 알 수 없기 때문이다. export() 메서드 자체가 아무것도 반환하지 않으므로 다른 구현 메서드에서 어떤 작업을 진행하고, 이를 기록하거나 화면에 출력할 가능성이 크다. 인터페이스로부터 얻을 수 있는 정보가 아무것도 없다.
  • void를 반환하면 어서션으로 결과를 테스트하기도 매우 어렵다. 예상한 결과 실제 결괏값을 어떻게 비교할까? 안타깝게도 void를 반환하면 아무 결과도 없다.

인터페이스의 좋은 예

public interface Exporter { String export(SummaryVo summaryVo); }

public interface Exporter { 
	String export(SummaryVo summaryVo); 
}

예외 처리

예외를 사용해야 하는 이유

  • 고전적인 C 프로그래밍에서는 수많은 if 조건을 추가해 암호 같은 오류 코드를 반환했다. 하지만 그 방법에는 여러 단점이 존재한다.
    • 전역으로 공유된 가변 상태에 의존해 최근에 발생한 오류를 검색해야한다.
    • 어떤 값이 실제 값인지 아니면 오류를 가리키는 값인지 구분하기가 어렵다.
    • 제어 흐름이 비즈니스 로직과 섞이면서 코드를 유지보수하거나 프로그램의 일부를 따로 테스트하기도 어려워진다.

총정리

  • 개방/패쇄 원칙을 이용하면 코드를 바꾸지 않고도 메서드나 클래스의 동작을 바꿀 수 있다.
  • 개방/패쇄 원칙을 이용하면 기존 코드를 바꾸지 않으므로 코드가 망가질 가능성이 줄어들며, 기존 코드의 재사용성을 높이고, 결합도가 높아지므로 코드 유지보수성이 개선된다.
  • 많은 메서드를 포함하는 갓 인터페이스는 복잡도와 결합도를 높인다.
  • 너무 세밀한 메서드를 포함하는 인터페이스는 응집도를 낮춘다.
  • API의 가족성을 높이고 쉽게 이해할 수 있도록 메서드 이름을 서술적으로 만들어야한다.
  • 연산 결과로 void를 반환하면 동작을 테스트하기 어려워진다.
  • 자바의 예외는 문서화, 형식 안정성, 관심사 분리를 촉진한다.
  • 확인된 예외는 불필요한 코드를 추가해야 하므로 되도록 사용하지 않는다.
  • 너무 자세하게 예외를 적용하면 소프트웨어 개발의 생선성이 떨어진다.
  • 노티피케이션 패턴을 이용하면 도메인 클래스로 오류를 수집할 수 있다.
  • 예외를 무시하거나 일반적인 Exception을 잡으면 근본적인 문제를 파악하기가 어렵다.
  • 빌드 도구를 사용하면 응용프로그램 빌드, 테스트, 배포 등 소프트웨어 개발 생명 주기 작업을 자동화할 수 있다.
  • 요즘 자바 커뮤니티에서는 빌드 도구로 메이븐과 그레이들을 주로 사용한다.
반응형