jongfeel / BookReview

Reviews the IT or any books slowly and steady.
MIT License
4 stars 0 forks source link

9장 암시적인 개념을 명확하게 #712

Closed jongfeel closed 5 months ago

jongfeel commented 6 months ago

9장 암시적인 개념을 명확하게

심층 모델이 강력한 이유는 사용자 행위, 문제, 문제의 해법에 대한 본질적인 지식을 간결하고 유연하게 표현하는 중심 개념과 추상화가 담겨 있기 때문이다. 심층 모델로 향하는 첫 걸음은 도메인의 본질적인 개념을 모델 내에 표현하는 것이다. 이후 지식탐구와 리팩터링이 반복되면 정제 과정을 거치는데, 이건 모델과 설계 내에 명확하게 인식되고 표현될 때에 본격적으로 할 수 있게 된다.

개발자들이 토의 중에 단서를 얻거나 설계상에 암시적으로 존재하는 개념을 인지하면 도메인 모델과 관련 코드를 대량으로 변환하게 되며, 하나 이상의 객체와 객체 간의 관계를 활용해 모델 내에 해당 개념을 명확하게 표현하게 된다.

도약은 중요 개념이 모델 내에 명확해 지고 난 후에야 나타난다. 암시적인 개념을 정제되지 않은 형태로 인식하는 것에서 시작해서 리팩터링 과정을 거쳐 반복적으로 개념에 할당된 책임을 조정하고, 다른 객체와의 관계를 변경하며, 이름도 수정해 가면 마침내 모든 것이 또렷해진다.

개념 파헤치기

개발자는 잠재적인 암시적 개념을 드러내는 단서에 민감해야 한다.

팀에서 사용하는 언어를 경청하면서, 설계와 외견이 모순이 있는 전문가의 견해를 검토하면서, 도메인과 관련된 문서를 조사하고 많은 실험 과정을 거치면서 얻는다.

이 과정에 대해 하나씩 예제와 함께 알아본다.

언어에 귀 기울여라

사용자가 보고서 상의 일부 항목에 대해 반복적으로 이야기 하거나 애플리케이션의 다른 부분에서 데이터 집합을 조합해서 사용하는데 이 과정에서 객체가 필요하다는 생각을 잘 하지 못한다. 그건 사용자가 얘기하는 용어의 의미를 이해하지 못했거나 중요하다는 사실 자체를 깨닫지 못했을 수도 있다.

그러던 어느 순간! 보고서 상의 항목 이름이 중요한 도메인 개념을 의미한다는 걸 알게 된다. 그러면 도메인 전문가는 통찰력을 얻어서 안심할 수도 있고, 아니면 여태 얘기해왔고 당연한 걸 이제 알았냐면서 별거 아니라고 생각할 수도 있다.

새로운 모델이 새기면서 논의의 품질에 변화가 생긴다. 특정 시나리오에 대한 모델 상호작용은 자연스러운 방식으로 표현하는 게 가능해진다. 도메인 모델 언어는 더 강력해지고 새로운 모델은 리팩터링을 통해 설계가 깔끔해진다.

도메인 전문가가 사용하는 언어에 귀 기울여야 한다. 특정 용어는 모델에 기여하는 개념의 실마리에 해당한다

하지만 "명사는 객체입니다"와 같은 진부한 개념을 얘기하는 건 아니다. 새로운 단어는 명료하고 유용한 개념을 찾기 위한 대화와 지식 탐구로 이어지는 것이며 설계상 어디에도 표현돼 있지 않은 어휘를 사용하고 있다면 경고의 신호이다. 개발자와 도메인 전문가 둘 다 설계상에 표현돼 있지 않은 어휘를 사용한다면? 더욱 더 위험한 경고이다.

이걸 기회로 삼아서 용어가 설계에 누락되었다면 포함시켜서 모델과 설계를 향상시키는 기회로 삼는다.

예제: 해운 모델의 누락된 개념에 귀 기울이기

화물 예약 기능으로 화물 적재/하역하는 작업 순서를 효율적으로 조직하는 "운영 지원(operation support)" 애플리케이션이다.

예약 애플리케이션은 화물 운송 계획에 항로설정 엔진(routing engine)을 사용한다. 각 운항 구간(leg)은 데이터베이스 테이블의 한 레코드로 저장되고, 화물을 운반하기로 예정된 선박 운항에 부여된 ID와 화물을 적재할 위치(location) 및 하역할 위치의 정보를 사용해서 구분한다.

image

개발자와 해운 전문가가 주고 받는 대화. (자세한 내용은 책 참고)

대화 요약: 운항일정(itinerary)이라는 개념은 예약과 운영 사이의 실제적인 연결고리가 되는 걸 알고 다이어그램을 그리기 시작

image

최종적으로 Itinerary)이 중요하다는 사실을 대화를 통해 알게 됨!

대화 이후 개발자들은 긴 토의를 거쳐 아래 다이어그램을 제안한다.

image

명시적인 Itinerary 객체로 리팩터링한 결과로 얻게 된 이점은 다음과 같다.

  1. Routing Serviced의 인터페이스를 좀더 표현력 있게 정의
  2. Routing Service에서 예약 데이터베이스 테이블로의 결합 제거
  3. 예약 애플리케이션과 운영 지원 애플리케이션 간의 관계를 명확하게 표현
  4. Itinerary로 부터 예약 보고서와 운영 지원 애플리케이션 모두에 대한 적재/하역 시간 도출이 가능, 중복 코드 제거
  5. 예약 보고서에서 도메인 로직을 제거하고 별도의 도메인 계층으로 옮김
  6. UBIQUITOUS LANGUAGE를 확장해 개발자와 도메인 전문가, 개발자간의 모델과 설계에 대한 더 정확한 논의가 가능해짐

어색한 부분을 조사하라

설명하기 어려운 프로시저나 새로운 요구사항 때문에 복잡성이 증가하면 어색한 부분을 조사해야 한다.

누락된 개념이 존재한다는 사실조차 인식하지 못할 때도 있으며 누락된 사실을 깨닫는다고 해도 모델과 관련된 문제를 어떻게 풀어야 할지 감을 못 잡을 수도 있다.

도메인 전문가가 그런 개념을 발견할 수 있게 해야 한다. 운이 좋으면 도메인 전문가가 다양한 아이디어를 통해 여러 모델을 시도할 수 있다. 차선으로 직접 아이디어를 제안해서 도메인 전문가가 납득할 수 있는 아이디어를 검증해야 한다.

예제: 이자 수익 예제 - 어려운 방식으로 접근하기

기업 대출(commercial loan)과 그 밖의 이자부 자산(interest-bearing asset)에 투자하는 가상 금융 회사 예이다. 매일 밤 배치 스크립트를 통해 그날의 모든 이자와 수수료를 계산하고, 회계 소프트웨어에 저장한다.

image

이자 계산 방식은 점점 더 복잡해져서 더 좋은 모델이 있을 거라 생각해 친한 도메인 전문가와 얘기를 해 본다.

(자세한 대화 내용은 책 참고)

대화 요약:

이자 수익(interest earned)과 상환(paymen)은 별개의 항목이며 업무 처리 방식과 거리가 멀기 때문에 미상환 이자를 추적할 필요가 없다.

image

매일 혹은 일정상 필요할 때마다 발생 이자(interest accrual)를 원장에 기입하는 걸 발생주의 회계(accrual basis accounting)를 통해 이자 계산 방식을 변경

image

수수료 등을 별도로 계산하고 쉽게 추가할 수 있게 변경

image

Calculator 클래스를 리팩터링 하고 새로운 설계에 기반을 둔 코드를 실행할 수 있었다. 최종적으로 작성한 코드의 설계는 다음과 같다.

image

새로운 모델은 몇 가지 장점이 있다.

  1. "발생" 이라는 용어를 추가, UBIQUITOUS LANGUAGE가 풍부해졌다.
  2. 상환에서 발생을 분리
  3. 어느 원장에 기업할 것인지에 대한 도메인 지식을 도메인 계층으로 옮김
  4. 업무 적합성에 맞게 수수료와 이자를 하나의 개념으로 묶어 코드 중복 제거
  5. 새로운 수수료와 이자 처리 방식을 추가하기 위한 Accrual Schedule(발생 기록표) 제공

개발자는 문제 도메인을 직접 파헤치고 해답을 찾기 위해 헌신적인 노력을 했다. 도메인 전문가가 소극적이었다면 출발을 잘못했을 것이고, 문제 해결을 위해 도메인 전문가 보다는 동료 개발자와 얘기했을 것이다.

모순점에 대해 깊이 고민하라

어떤 모순은 용어를 다르게 쓰는 데서 발생하며, 어떤 모순은 도메인을 잘못 이해하는 데서 발생한다. 또 도메인 전문가가 서로 모순되는 사실을 얘기하는 경우도 있다.

모순은 흥미롭지 않고 심오한 내용을 암시하지도 않는다. 그래도 이런 사고 패턴을 거쳐 문제 도메인의 피상적인 층을 뚫고 더 심층적인 통찰력에 이를 수 있다.

서적을 참고하라

다양한 분야에 대해 근본 개념과 일반적인 통념은 책에서 찾아볼 수 있다. 다양한 서적을 참고해서 일관성 있고 사려 깊은 관점에서 작업을 시작한다.

예제: 이자 수익 예제 - 서적을 참고해서 작업하기

앞선 예제에서 Interest Calculator(이자 계산기)를 다루기가 점점 어려워진다는 사실을 깨닫기 시작하면서 시작한다. 다시 표면 아래에 숨어 있을 것으로 추측되는 누락된 개념을 조사하려고 회의를 해도 도메인 전문가에게 의지할 수 없는 상황이다.

개발자는 서점에서 회계 입문서를 찾아 책을 훑어본다. 발생주의 회계(Accrual Basis account)에 대한 개념을 잘 설명한 부분을 보고 회계에 대한 내용을 다시 고안해낼 필요가 없었다. 그리고 다음과 같은 모델을 만들었다.

image

아직 Asset(자산)이 수직을 발생시킨다는 사실에 대한 통찰력을 얻지 못했기 때문에 Calculator가 그대로 남아 있다. 수익 발생에서 지불이라는 걸 분리하는데 성공했고 "발생(accrual)"이라를 용어를 모델과 UBIQUITOUS LANGUAGE에 추가했다.

도메인 전문가가 프로젝트를 지원하는 상황이어도 해당 분야의 체계를 잘 이해하기 위해 문헌을 찾아보는 것은 도움이 된다. 대부분 해당 분야의 일반적인 업무 관행을 체계화하고 추상화한 사상가들이 있다.

다른 대안으로 해당 도메인을 경험한 다른 소프트웨어 전문가의 책을 참고한다. 예로 < 분석 패턴: 재사용 가능한 객체 모델(Analysis Patterns: Reusable Object Models) > (Fowler 1997)의 6장을 읽었다면 앞의 결과와는 다른 방향으로 작업했을 수도 있다. 책을 읽는다고 그대로 이용할 수 있는 해법을 얻는 건 아니다. 다만 해당 분야를 경험한 사람의 정제된 경험을 비롯해 개발자가 직접 시도해볼 만한 출발점 정도는 제시할 것이다. 그러면 개발자는 바퀴를 다시 발명하는 수고를 아낄 수 있다.

위에 소개된 책의 6장은 Inventory and Accounting이라는 제목으로 회계 관련된 내용이다.

시도하고 또 시도하라

새로운 경험의 축적과 지식 탐구를 거쳐 더 훌륭한 아이디어가 떠오르면 적어도 한 번은 기존의 결과를 바꾸게 될 것이다. 모델러/설계자는 자신의 아이디어에 집착해서는 안 된다.

각 방향 선회는 모델에 심층적인 통찰력을 반영했음을 의미한다. 각 리팩터링은 더 유연하고, 더 변경하기 수월하며, 수정해야 할 곳을 바로 수정할 수 있게 모델의 상태를 유지해 준다.

선택의 여지는 없다. 실험은 유용한 것과 유용하지 않은 것이 무엇인지를 배우는 방법이다. 설계 과정에서 실수를 피하려고만 하면 더 적은 경험을 바탕으로 설계를 하게 되므로 품질이 낮은 결과를 얻게 된다. 어쩌면 여러 번의 실험을 거쳐 설계를 하는 것에 비해 시간이 더 오래 걸릴 수도 있다.

다소 불명확한 개념을 모델링하는 법

객체지향 설계 입문서에는 개념을 찾기 위해 "명사와 동사"를 조사하라고 하는데 이걸로 표현되지 않는 다른 중요한 범주의 개념도 모델내에 명시적으로 표현할 수 있다.

객체지향 설계를 시작할 때 명확한 개념으로 인식하지 못했던 세 가지 범주를 설명한다.

명시적인 제약조건

제약조건(constraint)은 특별히 중요한 범주의 모델 개념을 형성한다. 암시적인 상태로 존재하다가 명시적으로 표현하면 설계를 대폭 개선할 수 있다.

제약조건이 어떤 객체나 메서드 내에 포함되는 게 자연스러울 수 있다. Bucket 객체는 제한된 용량(capacity)을 초과해서 저장할 수 없다는 불변식을 만족해야 한다.

image

제약조건을 자체적인 메서드로 분리하면 제약조건에 의도를 드러내는 이름을 부여해서 설계 내에 제약 조건을 명확하게 표현할 수 있다. 규칙의 복잡도에 비례해 제약조건을 표현하는 메서드가 비대해지더라도 호출 메서드는 단순한 상태를 유지하고 본연의 작업에만 집중할 수 있다.

한 메서드 안에 제약조건을 표현할 수 없는 경우도 있고 메서드가 단순해도 객체의 주된 책임을 수행하는 데 필요하지 않은 정보를 해당 메서드에서 필요로 할 수 있다.

아래는 어떤 제약조건을 포함한 객체의 설계가 잘못되어 있다는 조짐을 나열한 것이다.

  1. 제약조건을 평가하려면 해당 객체의 정의에 적합하지 않은 데이터가 필요하다.
  2. 관련된 규칙이 여러 객체에 걸쳐 나타나며, 동일한 계층구조에 속하지 않는 객체 간에 중복 또는 상속 관계를 강요한다.
  3. 요구사항, 설계 레벨에서 제약조건을 맞췄지만 구현 단계에서 절차적인 코드에 묻혀 명시적으로 표현되지 않는다.

제약조건이 모델 내에 명확하게 표현돼 있지 않다면 제약조건을 명시적인 객체로 분리하거나 일련의 객체와 관게의 집합으로 모델링할 수 있다. (The Object Constraint Language: Precise Modeling with UML, Warmer and Kleppe 1999)

예제 - 설계 검토: 초과 예약 정책

1장에서 운송 수단이 처리할 수 있는 양보다 10퍼센트 많은 양의 화물을 예약하는 일반적인 해운 업무의 관행을 알아봤다.

Voyage와 Cargo 간의 관계에 대한 제약조건이 다이어그램과 코드에 명시적으로 표현됐다.

image

에서 OverbookingPolicy.isAllow() 메서드 참고

도메인 객체로서의 프로세스

절치(procedure)를 모델의 주요 측면으로 삼지 않아야 한다. 객체는 절차를 캡슐화해서 절차 대신 객체의 목표나 의도에 관해 생각하게 해야 한다.

도메인에 프로세스(process)가 있으면 이를 표현해야 한다. 프로세스가 있으면 객체를 어색하게 설계하는 경향이 있다.

SERVICE는 그런 프로세스를 명시적으로 표현하는 한 가지 방법이긴 하지만 복잡한 알고리즘을 캡슐화한다.

또 다른 접근법으로 알고리즘 자체 또는 그 일부를 하나의 객체로 만드는 것이다. 어떤 프로세스를 선택할 것인가는 곧 어떤 객체를 선택할 것인가가 되고, 각 객체는 각기 다른 STRATEGY를 표현한다.

명시적 프로세스와 숨겨야 할 프로세스를 구분하는 건 도메인 전문가가 이야기하는 프로세스인지 컴퓨터 프로그램상의 메커니즘 일부인지를 파악하면 된다.

제약조건과 프로세스는 객체지향에서 확연하게 떠오르지 않는 두 가지 넓은 범주의 모델 개념이지만, 제약사항과 프로세스를 모델의 요소로 간주하면 설계를 매우 명확하게 만들 수 있다.

SPECIFICATION은 특정한 종류의 규칙을 표현하는 매우 간결한 수단을 제공하며, 조건 로직으로부터 규칙을 분리해서 규칙이 모델 내에서 분명해지게끔 만들어준다.

SPECIFICATION(명세)

image

애플리케이션에는 규칙을 검사하는 Boolean 테스트 메서드가 있다. 규칙이 단순하면 테스트 메서드를 사용해 규칙을 처리한다.

규칙을 평가하는 코드가 많아지고 도메인 클래스와 하위시스템에 대한 의존성을 갖게 되면 코드를 리팩터링 해서 응용 계층으로 옮길 것이다. 이제 업무 모델에 내제된 규칙을 표현하지 않는 쓸모 없는 데이터 객체를 뒤로한 채 규칙은 도메인 계층과 완전히 분리된다. 규칙을 도메인 계층 내에 유지해야 하지만 규칙을 통해 평가하려는 객체에 규칙을 두기에는 적절하지 ㅏㄶ다. 규칙을 평가하는 메서드는 조건 코드로 팽창할 것이고 결국 규칙에 대한 가독성은 떨어진다.

논리 프로그래밍(logic-programming) 패러다임에서는 이를 다른 방식으로 처리한다. 규칙은 술어(predicate)로 표현할 수 있다. true, false로 평가하는 함수이며 복잡한 규칙을 표현하기 위해 and, or 연산자를 결합할 수 있다. 술어를 사용하면 규칙을 명확하게 선언할 수 있다.

업무 규칙이 ENTITY나 VALUE OBJECT가 맡고 있는 책임에 맞지 않고 규칙의 다양성과 조합이 도메인 객체의 기본 의미를 압도할 때가 있다. 그렇다고 규칙을 도메인 계층으로부터 분리하면 도메인 코드가 더 모델을 표현할 수 없게 된다.

논리 프로그래밍에서 "술어"를 제공하지만 순수 객체를 이용해서 술어 개념을 구현하는 건 어렵다. 이 방법은 너무 일반적이라 논리 프로그래밍과 같은 특별한 설계만큼 의도를 충분히 전달하지 못한다.

이런 이점을 얻기 위해 논리 프로그래밍을 완전히 구현할 필요까지는 없다. 대부분 규칙은 몇 가지 특수한 경우로 나뉘고 술어의 개념을 빌려 Boolean 결과를 내는 특별한 객체를 만들 수 있다. 덩치가 커진 테스트 메서드는 독립적인 객체로 발전할 것이다. 이런 테스트는 별도의 VALUE OBJECT로 분해할 수 있는 참/거짓 테스트다. 테스트 메서드를 보유한 객체는 정의된 술어가 어떤 대상 객체에 대해 참인지 검사하기 위해 대상 객체를 평가할 수 있다.

image

이 새로운 객체는 명세(specification) 이다. SPECIFICATION은 다른 객체에 대한 제약 조건을 기술하며, 제약조건은 있을 수 있고 없을 수 있다. 가장 기본적인 개념은 다른 객체가 SPECIFICATION에 명시된 기준을 만족하는 지 검사하는 것이다.

그러므로 특별한 목적을 위해 술어와 유사한 명시적인 VALUE OBJECT를 만들자. SPECIFICATION은 어떤 특정 객체가 특정 기준을 만족하는지 판단하는 술어다.

체납 송장 예제의 경우 체납이 의미하는 것을 명시하고 Invoice를 평가해 체납 여부를 결정하는 SPECIFICATION을 이용해 모델링할 수 있다.

image

SPECIFICATION을 이용하면 도메인 계층에 유지할 수 있다. 완전한 객체를 사용해서 규칙을 표현하므로 설계가 모델을 더 명확하게 반영할 수 있다. FACTORY는 고객의 계정이나 기업 정책 데이터베이스와 같은 외부 정보를 사용해 SPECIFICATION을 설정할 수 있다. SPECIFICATION은 단순하고 직접적인 방식으로 필요한 정보를 얻을 수 있다.


SPECIFICATION의 기본 개념은 매우 단순하며, 도메인 모델링 문제에 관해 생각할 수 있게 돕는다. MODEL-DRIVEN DESIGN을 실현하려면 개념을 표현하는 효과적인 구현 방법도 필요하다. 그러려면 패턴을 적용하는 방법을 좀더 깊이 있게 탐구해야 한다. 도메인 패턴을 단지 UML 다이어그램을 작성하기 위한 깔끔한 아이디어 정도로 치부해서는 안된다. 도메인 패턴은 모델과 구현 간에 MODEL-DRIVEN DESIGN의 개념을 유지하게 해주는 프로그래밍 문제에 대한 해법이다.

패턴을 적절히 적용하면 도메인 모델링과 관련한 다양한 범주의 문제에 접근하는 방법에 관한 전반적인 사고체계를 활용할 수 있고 효과적인 구현 방법을 선택할 때 수년간 축적된 경험을 참고할 수 있다. 패턴은 요리책이 아니다. 패턴은 자체적인 해법을 고안해낼 때 경험을 기반으로 작업에 착수할 수 있게 해주며 현재 진행 중인 작업에 과해 의사소통할 수 있는 언어를 제공한다.

SPECIFICATION 적용과 구현 내용은 핵심적인 개념만 훑어보고 지나가도 된다. 나중에 실제 패턴을 적용해야 할 상황이 생기면 상세 논의내용을 읽어보고 그 속에 담긴 경험을 활용해 본다. 그러면 현재 직면한 문제에 적합한 해결책을 고안해 낼 수 있다.

SPECIFICATION의 적용과 구현

SPECIFICATION의 주된 가치는 매우 상이해보이는 애플리케이션 기능을 하나로 통합해 준다. 객체의 상태를 아래 세 가지 상태로 표현한다면

검증(validation), 선택(selection), 요청 구축(building to order)이라는 SPECIFICATION의 세 가지 용도는 개념적인 차원에서 동일하다. SPECIFICATION이 없다면 개념적인 통일성이 없어지므로 동일한 규칙이 각기 다른 형태를 가지거나 서로 모순된 형태일 수도 있다.

검증

SPECIFICATION의 가장 단순한 용도는 검증(validation)이다.

image
class DelinquentInvoiceSpecification extends InvoiceSpecification {
    private Date currentDate;
    // An instance is used and discarded on a single date
    public DelinquentInvoiceSpecification(Date currentDate) {
        this.currentDate = currentDate;
    }
}

public boolean isSatisfiedBy(Invoice candidate) {
    int gracePeriod = candidate.customer().getPaymentGracePeriod();
    Date firmDeadline = DateUtility.addDaysToDate(candidate.dueDate(), gracePeriod);
    return currentDate.after(firmDeadline);
}

여기서 판매원이 고객에게 체납 청구서를 보낸 경우 붉은 색으로 표시하고 싶다면 클라이언트에서 다음의 메서드를 작성하면 된다.

public boolean accountIsDelinquent(Customer customer) {
    Date today = new Date();
    Specification delinquentSpec = new DelinquentInvoiceSpecification(today);
    Iterator it = customer.getInvoices().iterator();
    while (it.hasNext()) {
        Invoice candidate = (Invoice) it.next();
        if (delinquentSpec.isSatisfiedBy(candidate)) return true;
    }
    return false;
}
선택(또는 질의)

검증 외에 다른 케이스로 특정 조건을 기반으로 객체 컬렉션의 일부를 선택하는 것이다. 이 경우에도 SPECIFICATION이라는 동일한 개념을 적용할 수 있지만 구현상 차이는 있다.

일반적인 업무 시스템은 관계형 데이터베이스에 데이터를 저장한다. 그리고 이전 챕터에서 설명한 것처럼 상이한 기술과 교차되는 지점에서는 모델의 초점을 잃어버리기 쉽다.

관계형 데이터베이스는 강력한 검색 기능을 제공한다. SPECIFICATION의 모델은 유지하면서 모델의 초점을 잃어버리는 문제를 해결하려면 관계형 데이터베이스의 능력을 활용한다. SQL을 사용하면 매우 자연스러운 방식으로 SPECIFICATION을 작성할 수 있다.

다음은 검증 규칙을 담은 동일한 클래스에 질의문(query)을 캡슐화한 간단한 예이다.

public String asSQL() {
    return
    "SELECT * FROM INVOICE, CUSTOMER" +
    " WHERE INVOICE.CUST_ID = CUSTOMER.ID" +
    " AND INVOICE.DUE_DATE + CUSTOMER.GRACE_PERIOD" + " < " + SQLUtility.dateAsSQL(currentDate);
}

SPECIFICATION은 도메인 객체에 대한 질의 접근을 제공하고 데이터베이스에 대한 인터페이스를 캡슐화하는 기본요소 매커니즘에 해당하는 REPOSITORY와 자연스럽게 어울린다.

image

이 설계에는 문제가 있는데 테이블 구조가 DOMAIN LAYER에 노출된다는 점이다. 테이블 구조는 도메인 객체와 관계형 테이블 간의 관계를 책임지는 매핑 계층 내부로 격리해야 한다.

일부 객체지향 매핑 프레임워크는 질의문을 모델 객체와 속성을 사용해 표현하는 방법을 제공하고 인프라스트럭처 계층에서 실제로 실행될 SQL문을 생성한다.

인프라스트럭처를 사용할 수 없다면 질의 메서드를 Invoice Repository에 추가해 SQL을 도메인 객체 밖으로 빼낼 수 있다.

REPOSITORY에 선택(selection) 규칙을 포함하지 않으려면 질의문을 일반화된 방식으로 표현해야 하며 이런 질의는 규칙을 담지 않지만 규칙을 만들어내는 컨텍스트에서 결합되거나 그런 컨텍스트에 위치할 수 있다.

public class InvoiceRepository {
    public Set selectWhereGracePeriodPast(Date aDate) {
        //This is not a rule, just a specialized query
        String sql = whereGracePeriodPast_SQL(aDate);
        ResultSet queryResultSet = SQLDatabaseInterface.instance().executeQuery(sql);
        return buildInvoicesFromResultSet(queryResultSet);
    }
    public String whereGracePeriodPast_SQL(Date aDate) {
        return "SELECT * FROM INVOICE, CUSTOMER" +
        " WHERE INVOICE.CUST_ID = CUSTOMER.ID" +
        " AND INVOICE.DUE_DATE + CUSTOMER.GRACE_PERIOD" +
        " < " + SQLUtility.dateAsSQL(aDate);
    }
    public Set selectSatisfying(InvoiceSpecification spec) {
        return spec.satisfyingElementsFrom(this);
    }
}

Invoice Specification의 asSql() 메서드는 satisfyingElementsFrom(InvoiceRepository)로 대체되며, Delinquent Invoice Sepcification은 다음과 같이 구현된다.

public class DelinquentInvoiceSpecification {
    // Basic DelinquentInvoiceSpecification code here
    public Set satisfyingElementsFrom(InvoiceRepository repository) {
        //Delinquency rule is defined as:
        // "grace period past as of current date"
        return repository.selectWhereGracePeriodPast(currentDate);
    }
}

이 경우 SQL 문은 REPOSITORY 내부에 위치하며 SPECIFICATION은 어떤 질의문을 사용해야 하는지 제어한다. 체납 개념을 구성하는 본질적인 규칙은 SPECIFICATION에 선언돼 있다.

REPOSITORY는 이런 경우에만 사용되는 특수한 질의문을 포함한다. 이 방식은 수용할 만하지만 체납된 Invoice에 대한 기간 초과 Invoice의 상대적인 개수에 따라서는 REPOSITOY 메서드를 좀더 일반화된 상태로 유지할 수 있는 중재안이 SPECIFICATION을 좀더 자기 설명적(self-explanatory)으로 유지하면서 더 나은 성능을 보일 수 있다.

public class InvoiceRepository {
    public Set selectWhereDueDateIsBefore(Date aDate) {
        String sql = whereDueDateIsBefore_SQL(aDate);
        ResultSet queryResultSet = SQLDatabaseInterface.instance().executeQuery(sql);
        return buildInvoicesFromResultSet(queryResultSet);
    }
    public String whereDueDateIsBefore_SQL(Date aDate) {
        return "SELECT * FROM INVOICE WHERE INVOICE.DUE_DATE < " + SQLUtility.dateAsSQL(aDate);
    }
    public Set selectSatisfying(InvoiceSpecification spec) {
        return spec.satisfyingElementsFrom(this);
    }
}
public class DelinquentInvoiceSpecification {
    //Basic DelinquentInvoiceSpecification code here
    public Set satisfyingElementsFrom(InvoiceRepository repository) {
        Collection pastDueInvoices = repository.selectWhereDueDateIsBefore(currentDate);
        Set delinquentInvoices = new HashSet();
        Iterator it = pastDueInvoices.iterator();
        while (it.hasNext()) {
            Invoice anInvoice = (Invoice) it.next();
            if (this.isSatisfiedBy(anInvoice))
                delinquentInvoices.add(anInvoice);
        }
        return delinquentInvoices;
    }
}

여기서는 더 많은 양의 Invoice를 메모리로 적재한 후 메모리상에서 원하는 Invoice를 선택해야 하므로 성능 저하를 겪는다. 더 좋은 책임의 분할을 위해 수용 가능한 비용인지는 상황에 따라 다르다. 기본적인 책임을 유지하고 SPECIFICATION과 REPOSITORY 간의 상호작용을 구현하는 방법은 다양하다.

성능 향상이나 보안 강화를 위해 저장 프로시저(stored procedure)로 구현할 수도 있다. SPECIFICATION은 저장 프로시저에 전달할 수 있는 매개변수만 포함할 것이다. 구현 방법에 포함된 모델 간에는 차이가 없지만, 구현 방법마다 질의문을 작성하고 유지하는 방법이 얼마나 번거로운가에 따른 차이는 있다.

이런 논의는 SPECIFICATION과 데이터베이스를 결합하는 데 따르는 표면상의 문제만을 다루는 것에 불과하다. 마틴 파울러의 < 엔터프라이즈 애플리케이션 아키텍처 패턴(Patterns fo Enterprise Application Architecture) > 에서 미(Mee)와 히아트(Hieatt)는 SPECIFICATION을 사용해 REPOSITORY를 설계하는 것과 관련된 일부 기술적인 쟁점을 다루고 있다.

요청 구축(생성)

수많은 컴퓨터 프로그램은 뭔가를 생성해내며, 생성되는 것들은 명시돼 있어야 한다. 페이지상에 단어를 정확히 어디에 배치할 것인가는 명세를 만족시키는 범위 내에서 문사 작성 프로그램에 의해 이뤄진다.

처음에는 명백해 보이지 않을 수도 있지만 검증(validation)과 선택(selection)에 적용했던 SPECIFICATION과 개념상 다르지 않다. 아직 존재하지 않는 객체에 대한 기준을 명시하는 것이다. 여기서는 SPECIFICATION에 명시된 조건을 만족하는 완전히 새로운 객체나 집합을 새로 만들어내거나 재구성하는 것이 목적이다.

SPECIFICATION을 사용하지 않는다면 원하는 객체를 생성하기 위한 절차나 일련의 명령이 포함된 생성기(generator)를 작성할 수도 있다.

서술적인 SPECIFICATION을 사용해서 생성기의 인터페이스를 정의하면 생성할 결과물을 명시적으로 인터페이스에 포함시킬 수 있다. 이 접근법에는 여러 가지 이점이 있다.

예제: 화학 창고 포장기

요구조건

다양한 화학물질을 대형 컨테이너에 보관하는 창고가 있다. 어떤 화학물질은 화학작용이 일어나지 않으므로 아무 컨테이너에 보관해도 된다. 화학 작용이 일어아는 일부 화학물질은 통풍 컨테이너(ventilated containers)에 보관해야 한다. 폭발성이 있는 화학물질은 특별히 강화 컨테이너(armored containers)에 보관해야 한다. 특별한 규칙을 따르면 화학물질을 조합해 한 컨테이너에 함께 보관할 수 있다.

목적

화학물질을 효율적이고 안전하게 컨테이너에 저장하는 방법을 찾는다.

Figure 9.16. A model for warehouse storage image

검증 문제에 초점을 맞춰 규칙을 명확하게 만드는 방법에 집중하고 최종 구현을 테스트하는 용도로 활용한다.

화학물질을 보관하기 위해 컨테이너가 갖춰야 할 SPECIFICATION은 다음과 같다.

Chemical Container Specification
TNT Armored container
Sand
Biological Samples Must not share container with explosives
Ammonia Ventilated container

이 규칙을 Container Specification(컨테이너 명세)으로 작성하면 포장된 컨테이너에 관련된 구성 정보를 가지고 컨테이너가 제약조건을 만족하는지 검사할 수 있다.

Container Features Contents Specification Satisfied?
Armored 20 lbs. TNT 500 lbs. sand ✔️
50 lbs. biological samples ✔️
Ammonia

Container Specification의 isSatisfied() 메서드는 컨테이너가 필요로 하는 Container Feature(컨테이너 특성)의 만족 여부를 확인한다.

public class ContainerSpecification {
    private ContainerFeature requiredFeature;

    public ContainerSpecification(ContainerFeature required) {
        requiredFeature = required;
    }

    boolean isSatisfiedBy(Container aContainer) {
      return aContainer.getFeatures().contains(requiredFeature);
    }
}

폭발성 화학물질을 구성하는 클라이언트 코드 예제

tnt.setContainerSpecification(new ContainerSpecification(ARMORED));

Container(컨테이너) 객체의 isSafelyPacked() 메서드는 Container에 보관된 Chemical(화학 물질)의 모든 ContainerFeature가 Containe에 포함돼 있는지 여부를 확인한다.

boolean isSafelyPacked(){
    Iterator it = contents.iterator(); while (it.hasNext()) {
        Drum drum = (Drum) it.next();
        if (!drum.containerSpecification().isSatisfiedBy(this))
            return false;
    }
    return true;
}

재고 데이터베이스의 데이터를 사용해서 위험함 경우를 보고하는 모니터링 애플리케이션을 작성한다.

Iterator it = containers.iterator();
while (it.hasNext()) {
    Container container = (Container) it.next();
    if (!container.isSafelyPacked())
         unsafeContainers.add(container);
}

실제적인 작업은 포장기(packer)를 설계하는 일이다. 도메인에 대한 이해와 SPECIFICATION 기반 모델을 바탕으로 여러 Drum과 Container 묶음 규칙을 만족하는 상태로 포장해줄 SERVICE를 대상으로 다음과 같이 명확하고 간단한 인터페이스를 정의할 수 있다.

public interface WarehousePacker {
    public void pack(Collection containersToFill, Collection drumsToPack) throws NoAnswerFoundException;

    /* ASSERTION: At end of pack(), the ContainerSpecification of each Drum shall be satisfied by its Container.
    If no complete solution can be found, an exception shall be thrown. */
}

예제: 동작하는 창고 포장기 프로토타입

창고 포장 소프트웨어를 동작하게 만드는 최적화 로직을 작성하는 건 쉽지 않은 일이다. 사용자에게 의미 있는 행위가 포함된 인터페이스를 보여주고 적절한 피드백을 받을 수 없었기 때문이다.

개발 프로세스 진행을 원할하게 할 간단한 Packer 구현을 개발해야 병렬 작업이 가능하고 실제로 동작하는 종단간 시스템(end-to-end system)을 통해 피드백 고리를 만들 수 있다는 사실을 깨달았다.

public class Container {
    private double capacity;
    private Set contents; //Drums

    public boolean hasSpaceFor(Drum aDrum) {
        return remainingSpace() >= aDrum.getSize();
    }

    public double remainingSpace() {
        double totalContentSize = 0.0;
        Iterator it = contents.iterator();
        while (it.hasNext()) {
            Drum aDrum = (Drum) it.next();
            totalContentSize = totalContentSize + aDrum.getSize();
        }
        return capacity– totalContentSize;
    }

    public boolean canAccommodate(Drum aDrum) {
        return hasSpaceFor(aDrum) && aDrum.getContainerSpecification().isSatisfiedBy(this);
    }
}

public class PrototypePacker implements WarehousePacker {

    public void pack(Collection containers, Collection drums) throws NoAnswerFoundException {
        /* This method fulfills the ASSERTION as written. However, when an exception is thrown, Containers' contents may have changed. Rollback must be handled at a higher level. */
        Iterator it = drums.iterator();
        while (it.hasNext()) {
            Drum drum = (Drum) it.next();
            Container container = findContainerFor(containers, drum);
            container.add(drum);
        }
    }

    public Container findContainerFor(Collection containers, Drum drum) throws NoAnswerFoundException {
        Iterator it = containers.iterator();
        while (it.hasNext()) {
            Container container = (Container) it.next();
            if (container.canAccommodate(drum))
                return container;
        }
        throw new NoAnswerFoundException();
    }
}

이 코드는 개선의 여지가 있다. 모래도 특수 컨테이너에 들어갈 수 있으므로 위험 화학물질을 담기 전에 공간이 모자를 수 있다. 대다수 최적화 문제는 완벽하게 해결할 수는 없으며, 여기 까지가 언급한 규칙을 반영한 내용이다.

**동작하는 프로토타입을 이용한 개발 정체 해소**

한 팀이 다른 팀의 코드를 기다려야 하는 상황이고
양 팀이 작성한 컴포넌트를 사용하려면 전체적으로 통합될 때 까지 기다려야 한다.
이런 정체 현상 때는 MODEL-DRIVEN DESIGN 프로토타입을 개발해서 완화할 수 있다.
구현을 인터페이스로부터 분리해 프로젝트가 병렬로 진행될 수 있는 유연성을 제공한다.
적절한 시기에 구현을 효과적으로 해서 프로토타입을 대체하면 된다.
그 전에는 시스템의 모든 부분들은 개발 단계 동안에 상호작용할 수 있는 상태가 된다.

개발팀은 프로토타입을 써서 외부 시스템과 전반적인 통합을 포함해 속도를 높여 개발을 할 수 있게 된다.

여기서는 더 정교한 모델을 사용해서 실제 "작동하는 가장 간단한 것(simplest thing that could possibly work)"이라는 목표를 달성하는 예제를 살펴봤다. MODEL-DRIVEN 접근법을 소극적으로 따랐다면 이해하고 개선하기 어려웠을 것이며, 프로토타입을 만드는 데 더 오랜 시간이 걸렸을 것이다.

jongfeel commented 6 months ago

서적을 참고하라 부분에서 마지막 단락

책을 읽는다고 그대로 이용할 수 있는 해법을 얻는 건 아니다.

이 내용이 너무 마음에 든다. 책을 읽는 건 내용을 이해한 것이지 그 해법을 그대로 적용해야 하는 건 아니기 때문이다. 그래서 미리 경험한 사람의 내용을 토대로 출발점 정도를 제시할 수 있다는 점에서 책을 읽고 적용한다는 개념에 대해 잘 이해하고 있고 나도 그렇게 생각하고 있다.

jongfeel commented 5 months ago

SPECIFICATION 설명에서

그러려면 패턴을 적용하는 방법을 좀더 깊이 있게 탐구해야 한다. 도메인 패턴을 단지 UML 다이어그램을 작성하기 위한 깔끔한 아이디어 정도로 치부해서는 안된다.

도메인 패턴을 적용하려면 방법을 이해하려는 노력이 필요하지 아이디어를 다이어그램으로 표현하는 게 전부가 아니라는 깊은 통찰!

jongfeel commented 5 months ago

SPECIFICATION 선택(질의) 에서

엔터프라이즈 애플리케이션 아키텍처 패턴(Patterns fo Enterprise Application Architecture)에서 미(Mee)와 히아트(Hieatt)는 SPECIFICATION을 사용해 REPOSITORY를 설계하는 것과 관련된 일부 기술적인 쟁점을 다루고 있다.

을 언급한다.

그 내용은 chapter 13 Object-Relational Metadata Mapping Patterns에서 Repository 내용이다.

재미있는 건 Repository 설명의 참고 자료에 이 책이 언급된다는 점이다. 이 책의 앞에서도 이미 REPOSITORY를 다루고 있어서 참고 내용이라고 한 것으로 보인다.