Study/java

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

에디개발자 2020. 11. 18. 12:06
반응형

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

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

 

문서 관리 시스템

목표

  • 문서 관리 기능 설계의 핵심은 상속 관계, 즉 어떻게 클래스를 상속하거나 인터페이스를 구현하는가에 달렸다. 문서관리 기능을 제대로 설계하려면 리스코프 치환 원칙을 알아야한다.
  • 언제 상속을 사용해야 하는지와 관련해서는 '상속보다 조합' 원칙도 알아야한다.
  • 유지보수가 쉽고 좋은 테스트를 만드는 방법을 활용해 기존에 배운 자동화된 코드 구현 지식을 확장한다.

설계

  • 다양한 방법으로 설계가 가능하므로 다양한 설계와 모델링 중 한 가지를 선택한다.
  • 테스트 주도 개발(TDD)은 프로그램을 시작하는 아주 좋은 방법 중 하나이며 이미 예제를 풀어보면서 사용한 방법이다.

임포터

다양한 종류의 문서를 임포트하는 것이 문서 관리 시스템의 핵심 기능이다. 파일의 확장자로 파일을 어떻게 임포트할 지 결정할 수 있다.

switch(extension) { case "letter": // logic... break; case "report": // logic... break; case "jpg": // logic... break; }

위 코드는 다른 종류의 파일을 추가할 때마다 switch문에 다른 항목을 추가해 구현해야 하기 때문에 확장성이 부족하다.

Document 클래스

다양한 방법으로 Document를 정의할 수 있지만 장단점이 있다.
가장 간단한 방법은 Map<String, String>으로 속성 이름을 값과 매핑하는 방법이다. 응용프로그램에 직접 Map<String, String>을 사용하지 않는 이유가 뭘까? 한 문서를 모델링하려고 새 도메인 클래스를 소개하는 것은 식은 죽 먹기처럼 간단히 결정할 수 있는 일이 아니라 응용프로그램의 유지보수성과 가독성을 고려해야하는 일이다.

우선 응용프로그램의 컴포넌트 이름을 구체적으로 지어야한다. 훌륭한 소프트웨어 개발팀은 유비쿼터스 언어로 자신의 소프트웨어를 작성한다.
유비쿼터스 언어는 도메인 주도 설계에서 처음 등장했다. 개발자와 사용자 모두가 사용할 수 있도록 설계, 공유된 공통 언어를 말한다.

또한 클래스로 모델을 만들어 강한 형식의 원칙을 따르도록 권장한다. 예를 들어 Document 클래스는 불변 클래스, 즉 클래스를 생성한 다음에는 클래스의 속성을 바꿀 수 없다. Importer 구현이 문서를 만들면 이후에 수정할 수 없다. 따라서 Document의 속성에서 오류가 발생하면 해당 Document를 생성한 Importer 구현을 확인하면 되므로 오류가 발생한 원인을 좁힐 수 있다.

Document가 HashMap<String, String>을 상속받도록 설계를 결정한 개발자도 있을 것 이다. HashMap은 Document 모델링에 필요한 모든 기능을 포함하므로 처음에는 이 결정이 좋아 보일 수 있지만 몇가지 문제가 있다.

  • 소프트웨어를 설계할 떄 필요한 기능은 추가하면서 동시에 불필요한 기능은 제한해야한다. Document 클래스가 HashMap을 상속하면서 응용프로그램이 Document 클래스를 바꿀 수 있도록 결정한다면 이전의 불변성으로 얻을 수 있는 모든 이득이 사라진다.
  • 요약하자면 도메인 클래스를 이용하면 개념에 이름을 붙이고 수행할 수 있는 동작과 값을 제한하므로 발견성을 개선하고 버그 발생 범위를 줄일 수 있다.

Document 객체를 다른 인터페이스와 달리 public으로 사용하지 않은 이유가 있다. 보통 자바 클래스의 생성자는 public을 사용하지만 그러면 프로젝트의 어디에서나 그 형식의 객체를 만들 수 있는 문제가 생긴다. 오직 문서 관리 시스템에서만 Document를 만들 수 있어야 하므로 패키지 영역으로 생성자를 제공하고, 문서 관리 시스템이 위치한 패키지에만 접근 권한을 준다.

DOcument 속성 및 계층

Document 클래스는 속성에 String을 사용했다. 강한 형식과는 거리가 먼 결정이지 않은가? 그렇기도 하고 그렇지 않기도 하다. 속성을 텍스트로 저장하면 텍스트로 속성을 검색할 수 있다. 또한 속성을 만든 Importer의 종류와 관계없이 모든 속성이 아주 일반적인 형식을 갖도록 만들려는 의도도 있다. 다만 응용프로그램에서 String으로 정보를 전달하는 것은 보통 좋지 않은 방법으로 알려져 있다. 이를 형식에 빗대어 문자화 형식이라 부른다.

특히 속성값을 복잡하게 사용할 떄는 다양한 속성 형식으로 파싱하는 것이 좋다.
예로 특정 반경 내 주소를 찾거나 특정 크기 이하의 높이와 너비를 가진 이미지를 검색할 때, String보다 강한 형식을 가진 속성이 훨씬 도움이 된다. 문자열보다는 정숫값의 너비를 더 쉽게 비교할 수 있기 때문이다.

Import의 구성 계층을 그대로 Document 클래스 계층에 사용할 수 있다.
예로 Report Importer는 Report 클래스의 인스턴스를 임포트한다. 이렇게 하면 상속으로 기본적인 무결성 검사를 대신할 수 있다. 즉 Report는 Document다 라고 말할 수 있고, 그 자체로도 말이 된다. 하지만 예제에서는 이 정도로 직접적인 표현을 사용하진 않는다. OOP에서는 동작과 데이터의 관점으로 클래스를 설계하기 때문이다.

리스코프 치환 원칙 (LSP)

  • 클래스 상속과 인터페이스 구현을 올바르게 사용하도록 도와준다.
  • 형식이라는 용어는 클래스나 인터페이스를 떠올리자.
  • 하위형식이라는 용어는 두 형식이 부모와 자식 관계를 이루었음을 의마한다. 즉 클래스 상속이나 인터페이스 구현이 이에 해당한다.
  • 간편하게 자식 클래스는 부모로부터 물려받은 행동을 유지해야 한다고 생각하자. 당연한 말처럼 들릴 수도 있겠지만 조금 더 자세히 LSP를 들여다보면 LSP를 네 개의 부분으로 쪼갤 수 있다.

LSP q(x)는 T 형식의 x객체를 증명할 수 있는 공식이다. 그러면 S 형식의 객체 y가 있고 S가 T의 하위형식이라면 q(y)는 참이다.

1. 하위형식에서 선행조건을 더할 수 없음

선행조건은 어떤 코드가 동작하는 조건을 결정한다. 우리는 구현한 코드가 어떻게든 실행될 것이라고 가정할 수는 없다. 예를 들어 Importer 구현은 임포트하려는 파일이 존재하며, 읽을 수 있을 것이라는 선행조건을 갖는다. 따라서 Importer를 실행하기 전 검증을 수행하는 ImportFile 메서드가 필요하다.

importFile은 file validation 하는 클래스이다.

2. 하위형식에서 후행조건을 약화시킬 수 없음

첫 번째 규후행조건은 어떤 코드를 실행한 다음에 만족해야 하는 규칙이다. 예를 들어 유효한 파일에 importFile()을 실행했다면 contents()가 반환하는 문서 목록에 그 파일이 반드시 포함되어야 한다. 즉 부모가 부작용을 포함하거나 어떤 값을 반환한다면 자식도 그래야한다.

3. 슈퍼형식의 불변자는 하위형식에서 보존됨

불변자란 밀물과 썰물처럼 항상 변하지 않는 어떤 것을 가리킨다. 상속 관계의 부모와 자식 클래스가 있을 때, 부모 클래스에서 유지되는 모든 불변자는 자식 클래스에서도 유지되어야 한다.

4. 히스토리 규칙

  • LSP에서 가장 이해하기 어려운 개념이다.
  • 기본적으로 자식 클래스는 부모가 허용하지 않은 상태 변화를 허용하지 않아야 한다. 예제의 Document는 바꿀 수 없는 불변 클래스다. 즉 Document 클래스를 인스턴스화한 다음에는 어떤 속성도 삭제, 추가, 변경할 수 없다. 모든 부모 클래스의 사용자는 Document 클래스는 메서드를 호출했을 때 어떤일이 일어날 수 있음을 인지하고 있기 때문이다. 만약 자식이 불변이 아니라면 호출자의 예상을 뒤엎을 것이다.

대안

1 임포터를 클래스로 만들기

  • 임포터의 클래스 계층을 만들고 인터페이스 대신 가장 상위에 Importer 클래스를 만드는 방법을 선택
  • 인터페이스 : 여러 개를 한 번에 구현할 수 있음 // 클래스 : 일반 인스턴스 필드와 메서드를 갖음
  • 쉽게 망가질 수 있는 상속 기반의 클래스를 피해야 한다고 설명했듯이 인터페이스를 이용하는 것이 클래스를 이용하는 것보다 명백하게 좋은 선택이다.

2. 영역, 캡슐화 선택하기

  • Imprter 인터페이스와 구현, Query 클래스는 모두 패키지 영역임을 알 수 있다. 패키지 영역은 기본 영역으로 class Query라는 구문이 나오면 패키지 영역임을 가리키며, public class Query라고 정의해야 공개 영역으로 지정된다.
  • 패키지 영역을 이용하자
  • 패키지 세부 정보를 외부로 노출한다면 리팩터링이 어려워진다. 클래스가 외부로 노출되지 않도록 패키지 영역을 적극적으로 적용하면 내부 설계를 쉽게 바꿀 수 있다.

기존 코드 확장과 재사용

  • 코드를 재사용하려면 먼저 코드를 어떤 클래스에 구현해야 한다. 다음과 같은 세 가지 방법 중 하나를 선택할 수 있는데, 각각 장단점이 있다.
    • 유틸리트 클래스
    • 상속
    • 도메인 클래스

유틸리티 클래스

  • ImportUtil 클래스를 만들어 여러 임포트에서 공유해야 하는 기능을 이 유틸리티 클래스에 구현한다.
  • 유틸리티 클래스는 단순하고 쓸만하지만 객체지향 프로그래밍의 지향점과는 거리가 멀다. 객체지향에서는 클래스로 기능을 만든다. 인스턴스를 만들고 싶다면 무조건 new Thing()을 호출하여 관련된 속성과 도작은 Thing 클래스의 메서드로 구현한다.
  • 실제 객체를 클래스로 만드는 원칙을 따르면 도메인의 정신적 모델을 코드로 구조화하고 매칭시킬 수 있어 쉽게 이해할 수 있는 응용프로그램을 만들 수 있다.
  • 이 클래스는 시간이 흐를수록 갓 클래스의 모양을 갖춰간다.

상속

어떻게 동작과 개념을 연결할 수 있을까? 상속을 이용하면 이를 구현할 수 있다. 각각의 임포터가 TextImporter 클래스를 상속받는 방법이다. TextImporter 클래스에 모든 공통 기능을 구현하고 서브클래스에서는 공통 기능을 재사용한다.

상속은 다양한 환경에서 사용할 수 있는 완벽한 지원군인다. 리스코프 치환 원칙을 배울 때 상속 관계에서 제약을 올바르게 추가하는 방법도 배웠다. 실제 관계를 상속으로 잘못 설정하는 상황도 종종 발생한다. 실제 관계를 제대로 반영하지 않은 상속은 쉽게 깨질 수 있다는 점이 문제다. 시간이 흐르고 응용프로그램이 바뀔 때, 응용프로그램을 그에 맞게 바꾸는 것보다는 변화를 추상화하는 것이 더 좋다. 일반적으로 상속관계를 코드를 재사용하는 것은 좋은 방법이 안디ㅏ.

도메인 클래스

도메인 클래스로 텍스트 파일을 모델링하는 방법이 있다. 먼저 기본 개념을 모델링한 다음, 기본 개념이 제공하는 메서드를 호출해 다양한 임포터를 만든다. 여기서 개본 개념은 뭘까? 예제에서는 텍스트 파일의 내용을 처리해야 하므로 TextFile 클래스를 사용한다. 새롭거나 창의적이지 않다는 점이 바로 핵심이다. 클래스 이름이 매우 단순 명료해 텍스트 파일을 조작하는 함수를 어디에 추가할지 쉽게 알 수 있다.

도메인 클래스 구현

문서가 항상 텍스트 파일이라는 보장이 없으므로 TextFile은 Document의 서브클래스가 아니다. TextFile은 텍스트 파일이라는 기본 개념을 모델링하는 클래스로 텍스트 파일에서 데이터를 추출하는 메서드를 포함한다.

class TextFile { private final Map<String, String> attributes; private final List<String> lines; }

테스트 위생

  • 퇴행이 발생하는 범위를 줄이기 위해 사용
  • 자동화된 테스트가 있으면 코드 리팩터링을 쉽게 할 수 있다.
  • 유지보수 편이

테스트 이름 짓기

  • 테스트 이름을 지을 떄 가독성, 유지보수성, 실행할 수 있는 문서의 역할을 고려한다. prefix로 should를 붙힌다.
  • 안티패턴은 피하자 ( test1, test ....)
  • 세가지 모범 규칙을 적용해 테스트 이름을 짓는다.
    • 도메인 용어 : 문제 도메인을 설명하거나 응용프로그램에서 문제를 지칭할 때 사용하는 용어를 테스트 이름에 사용
    • 자연어 : 모든 테스트 이름은 일반 문장처럼 쉽게 읽을 수 있어야한다. 테스트 이름은 항상 어떤 동작을 쉽게 이해할 수 있도록 묘사
    • 서술적 : 코드는 한 번 구현하면 여러 번 읽게 된다. 나중에 쉽게 읽을 수 있도록 애초에 시간을 들여 서술적인 좋은 이름을 붙이자. 좋은 이름이 생각나지 않으면 동료에게 도움을 받자.
반응형