05장. 소프트웨어에서 표현되는 모델
Introduction
- 객체 간의 연관관계를 이해하고 묘사하기는 간단하지만 그것을 실제로 구현하는 것은 잠재적으로 다루기 힘든 문제일지도 모른다.
- 상세한 구현 결정이 MODEL-DRIVEN DESIGN 을 실현하는 데 얼마나 중요한가를 연관관계를 토대로 알게 될 것
- 모델을 표현하는 세 가지 패턴, 즉 ENTITY (엔티티), VALUE OBJECT (값 객체), SERVICE (서비스) 를 구분하는 데 초점을 맞춰 설명하겠다.
- 도메인 개념을 담은 객체를 정의하는 일은 겉으로는 매우 쉬워 보여도 거기에는 의미상의 미묘한 차이로 발생할 수 있는 중대한 문제가 잠재돼 있다.
- 어떤 객체가 연속성 (continuity) 과 식별성 (identity, 각종 상태를 바탕으로 추적되거나 서로 다른 구현에 걸쳐 존재하는 것) 을 지닌 것을 의미하는가? 아니면 다른 뭔가의 상태를 기술하는 속성에 불과한가? 이것은 ENTITY 와 VALUE OBJECT 를 구분하는 가장 기본적인 방법
- SERVICE 는 클라이언트 요청에 대해 수행되는 뭔가를 의미
- 관계형 데이터베이스에 객체 모델을 저장하는 경우처럼 불가피하게 객체 모델의 무결함을 타협해야만 하는 상황도 있다.
- 마지막으로 MODULE (모듈) 에 관해 논의하면서 모든 설계 관련 의사결정은 도메인에 부여된 통찰력을 바탕으로 내려야 한다는 사실을 알게 될 것
- 기술적인 측정 수단으로 여겨지는 높은 응집도 (high cohesion) 와 낮은 결합도 (low coupling) 라는 개념은 도메인 개념에도 적용할 수 있다.
- MODEL-DRIVEN DESIGN 에서 MODULE 은 모델의 한 부분이므로 도메인의 개념을 반영해야 한다.
연관관계
- 모델의 모든 탐색 가능한 (traversable) 연관관계에 대해 그것과 동일한 특성을 지닌 메커니즘이 소프트웨어에도 있다.
- 현실세계에는 수많은 다대다 (many-to-many) 연관관계가 있으며, 그 중 상당수는 애초부터 양방향 (bidirectional) 으로 나타남
- 그러나 이러한 일반적인 형태의 연관관계는 구현과 유지보수를 복잡하게 하며, 더욱이 해당 관계의 특성에 관해서는 거의 전해주는 바가 없다.
- 연관관계를 좀 더 쉽게 다루는 방법으로는 아래의 세 가지가 있다.
- 탐색 방향을 부여
- 한정자 (qualifier) 를 추가해서 사실상 다중성 (multiplicity) 을 줄인다.
- 중요하지 않은 연관관계를 제거
- 가능한 한 관계를 제약하는 것이 중요
- 애플리케이션 요구사항에 두 방향을 모두 탐색해야 한다는 요청이 없을 경우 탐색 방향을 추가하면 상호의존성이 줄어들고 설계가 단순해짐
- 그리고 도메인을 이해하면 도메인 본연의 방향성이 드러날지도 모름
- 도메인을 더욱 깊이 이해하다 보면 굉장히 자주 “한정적인(quanlified)” 관계에 이름
- 한정자는 다중성을 일대일로 줄이며, 중요한 규칙을 명시적으로 모델에 포함시킴
- 다대다 연관관계의 탐색 방향을 제약하면 해당 연관관계는 사실상 훨씬 더 구현하기 쉬운 일대다 연관관계로 줄어듦
- 도메인의 특성이 반영되게끔 연관관계를 일관되게 제약하면 연관관계의 의사전달력이 풍부해지고 구현이 단순해지며, 나머지 양방향 연관관계도 의미를 지니게 된다.
- 물론 당면한 문제에 필요한 것이 아니거나 중요한 의미를 담고 있는 모델 객체가 아니라면 궁극적인 단순화는 연관관계를 완전히 제거하는 것
ENTITY (엔티티, 참조객체라고도 함)
- 수많은 객체은 본질적으로 해당 객체의 속성이 아닌 연속성과 식별성이 이어지느냐를 기준으로 정의됨
- 객체 모델링을 할 때 우리는 객체의 속성에 집중하곤 하는데, ENTITY 의 근본적인 개념은 객체의 생명주기 내내 이어지는 추상적인 연속성이며, 그러한 추상적인 연속성은 여러 형태를 거쳐 전달된다는 것
- 심지어 어떤 것은 다른 객체와 속성이 같아도 서로 구분해야 하는 경우도 있다.
- 잘못된 식별성은 데이터 손상으로 이어질 수 있다.
- 어떤 객체를 일차적으로 해당 객체의 식별성으로 정의할 경우 그 객체를 ENTITY 라 한다.
- ENTITY 에는 모델링과 설계상의 특수한 고려사항이 포함돼 있다.
- ENTITY 는 자신의 생명주기 동안 형태와 내용이 급격하게 바뀔 수도 있지만, 연속성은 유지해야 한다.
- 또한 사실상 ENTITY 를 추적하려면 ENTITY 에 식별성이 정의돼 있어야 한다.
- ENTITY 의 클래스 정의와 책임, 속성, 연관관계는 ENTITY 에 포함된 특정 속성보다는 ENTITY 의 정체성에 초점을 맞춰야 한다.
- 한편으로 모델 내의 모든 객체가 의미있는 식별성을 지닌 ENTITY 인 것은 아니다.
- 이러한 문제는 객체지향 언어에서 모든 객체에 “동일성(identity)” 연산이 내장돼 있다는 점 때문에 혼동되기도 한다.
- 이 연산은 두 객체의 메모리 주소나 다른 어떤 메커니즘을 기준으로 두 객체 참조가 같은 객체를 가리키고 있는지 판단한다.
- 식별성은 ENTITY 의 미묘하고 의미 있는 속성이므로 언어에서 제공하는 자동화된 기능으로 대체할 수 없다.
- 한 객체가 속성보다는 식별성으로 구분될 경우 모델 내에서 이를 해당 객체의 주된 정의로 삼아라.
- 클래스 정의를 단순하게 하고 생명주기의 연속성과 식별성에 집중하라.
- 객체의 형태나 이력에 관계없이 각 객체를 구별하는 수단을 정의하라.
- 객체의 속성으로 객체의 일치 여부를 판단하는 요구사항에 주의하라.
- 각 객체에 대해 유일한 결과를 반환하는 연산을 정의하라.
- 이러한 연산은 객체의 유일함을 보장받는 기호를 덧붙여서 정의할 수 있을지도 모른다.
- 모델이 동일하다는 것이 무슨 의미인지 정의해야 한다.
- 식별성은 원래 세상에 존재하는 것이 아니며, 필요에 의해 보충된 의미
ENTITY 모델링
- 객체를 모델링할 때 속성에 관해 생각하는 것은 자연스러운 일이며, 특히 객체의 행위에 관해 생각해 보는 것은 아주 중요
- ENTITY 의 가장 기본적인 책임은 객체의 행위가 명확하고 예측 가능해질 수 있게 연속성을 확립하는 것
- ENTITY 는 별도로 분리돼 있을 때 자신의 책임을 가장 잘 수행한다.
- 개념에 필수적인 행위만 추가하고 그 행위에 필요한 속성만 추가한다.
- 그 밖의 객체는 행위와 속성을 검토해서 가장 중심이 되는 ENTITY 와 연관관계에 있는 다른 객체로 옮긴다.
- 식별성 문제를 제외하면 ENTITY 는 주로 자신이 소유한 객체의 연산을 조율해서 책임을 완수한다.
식별 연산의 설계
- 각 ENTITY 에는 다른 객체와 구분해줄 식별성(심지어 속성이 같은 다른 객체와도 구분할 수 있는)을 만들어내는 수단이 반드시 있어야 한다.
- 식별에 사용되는 속성은 시스템의 상태(시스템이 분산돼 있거나 객체가 저장돼 있는 경우에도)와 관계없이 해당 시스템 내에서 유일해야 한다.
- 대부분의 객체 영속화 기술은 매번 데이터베이스에서 객체를 가져올 때마다 새로운 인스턴스를 생성하므로 초기 식별성은 잃어버리게 된다.
- 또한 매번 네트워크를 거쳐 객체가 전송될 때마다 목적지에서 새로운 인스턴스가 만들어지므로 또 다시 해당 객체의 식별성을 잃어버리게 된다.
- 두 객체가 개념적으로 동일한 ENTITY 를 나타내는지 어떻게 알 수 있는가?
- 식별성에 대한 정의는 모델로부터 나온다.
- 따라서 식별성을 정의하려면 도메인을 이해해야 한다.
- 때때로 어떤 데이터 속성이나 여러 속성의 조합이 시스템 내에서 유일함을 보장받거나 단순히 유일하도록 제약되기도 한다.
- 이 같은 접근법의 결과로 ENTITY 는 고유한 키를 제공받는다.
- 한 객체의 속성으로 구성되는 실질적인 고유키가 없다면 또 다른 일반적인 해법은 각 인스턴스에 해당 클래스 내에서 유일한 기호(숫자나 문자열과 같은)를 덧붙이는 것
- 일단 이러한 ID 기호가 생성되어 ENTITY 의 속성으로 저장되면 이 ID 는 불변성을 띠게 된다.
- 종종 시스템에서 이러한 ID 를 자동으로 만들어 내기도 한다.
- 이러한 ID 생성 알고리즘은 시스템 내에서 유일함을 보장해야 하는데, 병렬 처리 시스템이나 분산 시스템에서는 그렇게 하기가 쉽지 않을 수도 있으며, 유일한 ID 를 생성하는 문제는 이 책의 범위를 넘어서는 기술이 필요할지도 모른다.
- 중요한 건 그러한 식별성에 관한 문제는 모델의 구체적인 면면에 따라 달라진다는 사실을 아는 것
- 종종 식별 수단도 마찬가지로 도메인에 대한 철저한 연구가 필요
- 마지막으로 생성된 ID 가 사용자에게 중요한 경우가 있음
- ID 의 유일성이 컴퓨터 시스템의 범위를 넘어 적용돼야 할 때도 있음
- 애플리케이션에서 외부 ID 가 필요한 경우 시스템의 사용자는 유일한 ID 를 제공할 책임을 지게 되며, 해당 시스템에서는 발생한 예외 상황을 처리할 적절한 수단을 제공해야 한다.
- 그런데 이러한 모든 기술적 문제를 감안하더라도 “두 객체가 동일하다는 것이 무엇을 의미하는가?” 라는 근원적인 문제를 놓치기 쉽다.
- ID 를 가진 각 객체를 표시하거나 두 인스턴스를 비교하는 연산을 작성하기는 쉽지만 이러한 ID 나 연산이 도메인에 의미 있는 구분법에 부합하지 않는다면 문제를 더욱 혼란스럽게 만들 뿐이다.
- 이는 식별성 할당 연산에 간혹 사람이 개입할 때가 있기 때문이다.
VALUE OBJECT (값 객체)
- 개념적 식별성이 없는 객체도 많은데, 이러한 객체는 사물의 어떤 특징을 묘사한다.
- ENTITY의 식별성을 관리하는 일은 매우 중요하지만 그 밖의 객체에 식별성을 추가한다면 시스템의 성능이 저하되고, 분석 작업이 별도로 필요하며, 모든 객체를 동일한 것으로 보이게 해서 모델이 혼란스러워질 수 있다.
- 소프트웨어 설계는 복잡성과의 끊임없는 전투다.
- 그러므로 우리는 특별하게 다뤄야 할 부분과 그렇지 않은 부분을 구분해야 한다.
- 사실 이 같은 객체는 자체적인 특징을 비롯해 모델에 중요한 의미를 지닌다.
- 이것들이 바로 사물을 서술하는 객체다.
- 개념적 식별성을 갖지 않으면서 도메인의 서술적 측면을 나타내는 객체를 VALUE OBJECT 라 한다.
- VALUE OBJECT 는 설계 요소를 표현할 목적으로 인스턴스화되는데, 우리는 이러한 설계 요소가 어느 것인지에 대해서는 관심이 없고, 오직 해당 요소가 무엇인지에 대해서만 관심이 있다.
- 어떤 VALUE OBJECT 는 다른 여러 객체를 조립한 것일 수도 있다.
- VALUE OBJECT 가 ENTITY 를 참조할 수도 있다.
- 간혹 VALUE OBJECT 는 여러 객체 간에 오가는 메시지의 매개변수로 전달되기도 한다.
- VALUE OBJECT 는 종종 한 연산에서 사용할 목적으로 만들어진 후 폐기되는 것처럼 일시적인 용도로 사용되기도 한다.
- 또한 ENTITY (그리고 다른 VALUE) 의 속성으로 사용되기도 한다.
- 한 사람은 식별성을 지닌 ENTITY 로 모델링할 수 있는 반면, 그 사람의 이름은 VALUE 인것처럼 말이다.
- 모델에 포함된 어떤 요소의 속성에만 관심이 있다면, 그것을 VALUE OBJECT 로 분류하라.
- VALUE OBJECT 에서 해당 VALUE OBJECT 가 전하는 속성의 의미를 표현하게 하고 관련된 기능을 부여하라.
- 또한 VALUE OBJECT 는 불변적(immutable)으로 다뤄라.
- VALUE OBJECT 에는 아무런 식별성도 부여하지 말고 ENTITY 를 유지하는 데 필요한 설계상의 복잡성을 피하라.
- VALUE OBJECT 를 구성하는 속성은 개념적 완전성 (conceptual whole) 을 형성해야 한다.
VALUE OBJECT 의 설계
- 여러 VALUE OBJECT 의 인스턴스 가운데 어느 것을 사용하는지는 중요하지 않다.
- 이런 식으로 제약조건이 줄어들면 설계 단순화나 성능 최적화를 꾀할 수 있다.
- 하지만 여기에는 복사, 공유, 불변성에 관한 의사결정이 따른다.
- 변경을 방지해서 객체를 안전하게 공유할 수 있으려면 해당 객체가 불변적이어야 한다.
- VALUE OBJECT 는 많아지는 경향이 있으므로 성능 최적화를 위한 별도의 대안을 마련하는 것이 중요할 수 있다.
- 복사와 공유 중 어느 것이 경제성 면에서 더 나은지는 구현환경에 따라 달라진다.
- 복사의 경우 객체의 개수가 굉장히 많아져 시스템이 무거워질 수도 있지만, 공유 또한 분산 시스템에서는 느려질 수 있다.
- 공유는 다음과 같은 경우 가장 도움이 되고 문제가 적게 일어나며, 이러한 경유로 공유를 제한한다.
- 공간을 절약하거나 데이터베이스 내의 객체 수를 줄이는 것이 중요한 경우
- 통신 부하가 낮은 경우 (이를테면, 중앙집중형 서버)
- 공유 객체의 불변성이 엄격하게 지켜지는 경우
- 속성이나 객체의 불변성은 특정 언어나 환경에서만 선언할 수 있다.
- 그러나 언어에서 개념적 구분을 직접적으로 지원해 주지 않는다고 해서 그러한 구분 자체가 유용하지 않다는 의미는 아니다.
- 이것은 단지 구현에 암시적으로만 존재할 규칙을 유지하는 노력이 좀 더 필요하다는 의미일 뿐이다.
- 이러한 명명관례와 선택적 문서화를 비롯해 수많은 논의를 거쳐 개선할 수 있다.
- VALUE OBJECT 가 불변적인 한 변경관리는 단순해진다.
- 즉 완전히 교체하지 않는 한 아무것도 변경되지 않는다.
- 불변 객체는 전기 콘센트의 예처럼 마음껏 공유할 수 있다.
- 가비지 컬렉션이 믿을 만 하다면 삭제는 단순히 객체에 대한 모든 객체 참조를 폐기하는 문제에 불과하다.
- VALUE OBJECT 가 설계에서 불변적이라는 의미를 나타낼 경우 개발자들은 애플리케이션이 특정 객체의 인스턴스에 의존하지 않는다는 점을 토대로 순수하게 기술적인 근거에 따라 복사와 공유와 같은 문제에 관해 자유롭게 의사 결정을 내릴 수 있다.
- 변경 가능성을 허용하는 경우
- VALUE 가 자주 변경되는 경우
- 객체 생성이나 삭제에 비용이 많이 드는 경우
- 교체(변경이 아닌)로 인해 클러스터링이 제한되는 경우
- VALUE 를 공유할 일이 그리 많지 않거나 클러스터링을 향상시키기 위해서나 다른 기술적인 이유로 공유가 보류된 경우
- VALUE 의 구현이 변경 가능하다면 그것을 공유해서는 안된다.
- VALUE 의 공유 여부와는 관계없이 VALUE OBJECT 는 가급적 변하지 않게 설계한다.
- VALUE OBJECT 를 정의하고 그것이 불변적임을 나타나게 하는 것은 다음과 같은 일반 규칙, 즉 모델에서 불필요한 제약을 피하면 개발자가 순수하게 기술적인 성능 최적화를 마음껏 수행할 수 있다는 것을 따르는 셈
- 필수적인 제약조건을 명시적으로 정의하면 중요 행위가 변경되는 것으로부터 설계를 안전하게 유지하는 동시에 개발자들이 설계를 최적화할 수 있다.
- 하지만 그러한 설계 최적화는 종종 특정 프로젝트에서만 사용하는 기술에 매우 종속적일 때가 있다.
VALUE OBJECT 를 포함한 연관관계 설계
- 모델에 포함된 연관관계 수가 더 적고 연관관계가 단순할수록 더 나은 모델이라 할 수 있다.
- 그런데 ENTITY 간의 양방향 연관관계는 유지하기는 어려울 수 있는 반면 VALUE OBJECT 간의 양방향 연관관계는 단순히 논리적으로 타당하지 않다.
- 어떤 객체가 아무런 식별성 없이 자신을 가리키는 동일한 VALUE OBJECT ****을 역으로 가리키는 것은 아무런 의미가 없다.
- 기껏해야 한 객체가 자신을 가리키고 있는 것과 내용이 같은 어떤 객체를 가리키고 있다는 것에 불과하며, 어딘가에서는 해당 객체의 불변식을 이행해야 할 것
- VALUE OBJECT 간의 양방향 연관관계는 완전히 제거하도록 노력해야 한다.
- 모델에 그와 같은 연관관계가 필요해 보여도 그 객체를 VALUE OBJECT 로 선언하는 것은 한 번 더 고려해봐야 한다.
- 어쩌면 그 객체에 아직도 분명하게 파악하지 못한 식별성이 남아 있을지도 모른다.
SERVICE (서비스)
설계가 매우 명확하고 실용적이더라도 개념적으로 어떠한 객체에도 속하지 않는 연산이 포함될 때가 있다. 이러한 문제를 억지로 해결하려 하기보다는 문제 자체의 면면에 따라 service를 모델에 명시적으로 포함할 수 있다.
오늘날 흔히 하는 실수는 행위를 적절한 객체로 다듬는 것을 너무나도 쉽게 포기해서 점점 절차적 프로그래밍에 빠지는 것이다. 객체의 정의에 어울리지 않는 연산을 강제로 객체에 포함시킨다면 해당 객체는 자신의 개념적 명확성을 잃어버리고 이해하거나 리팩터링하기 힘들어질 것이다.
이따금 서비스는 특정 연산을 수행하는 것 이상의 의미는 없는 모델 객체로 가장해서 나타나기도 한다. 행위자는 이름 끝에 Manager와 같은 것이 붙는다.
도메인의 개념 가운데 객체로는 모델에 어울리지 않는 것이 있다.
필요한 도메인 기능을 ENTITY나 VALUE에서 억지로 맡게 하면 모델에 기반을 둔 객체의 정의가 왜곡되거나, 또는 무의미하고 인위적으로 만들어진 객체가 추가될 것이다.
- SERVICE는 모델에서 독립적인 인터페이스로 제공되는 연산으로서 ENTITY나 VALUE OBJECT와 달리 상태를 캡슐화하지 않는다.
- SERVICE는 도메인 계층에도 마찬가지로 적용될 수 있다.
서비스라는 이름은 다른 객체와의 관계를 강조한다.
- SERVICE를 정의하는 기준은 순전히 클라이언트에 무엇을 제공할 수 있느냐에 있다.
- SERVICE는 주로 활동으로 이름을 짓는다.
- SERVICE는 적절히 사용해야 하고 엔티티와 VALUE OBJECT의 행위를 모두 가져와서는 안 된다.
잘 만들어진 SERVICE의 특징
- 연산이 원래부터 ENTITY나 VALUE OBJECT의 일부를 구성하는 것이 아니라 도메인 개념과 관련돼 있다.
- 인터페이스가 도메인 모델의 외적 요소의 측면에서 정의된다.
- 연산이 상태를 갖지 않는다.
여기서 상태를 갖지 않는다는 것은 클라이언트가 특정 SERVICE 인스턴스의 개별 이력과는 상관없이 SERVICE의 모든 인스턴스를 사용할 수 있다는 의미다.
도메인의 중대한 프로세스나 변환 과정이 ENTITY나 VALUE OBJECT의 고유한 책임이 아니라면 연산을 SERVICE로 선언되는 독립 인터페이스로 모델에 추가하라. 모델의 언어라는 측면에서 인터페이스를 정의하고 연산의 이름을 UBIQUITOUS LANGUAGE의 일부가 되게끔 구성하라. SERVICE는 상태를 갖지 않게 만들어라.
SEERVICE와 격리된 도메인 계층
- 도메인 계층에 속하는 서비스와 다른 계층에 속하는 것들을 구분하고, 그러한 구분을 분명하게 유지하는 책임을 나누는 데 주의를 기울여야 한다.
- 응용 서비스를 도메인 서비스와 구분하는 것은 더 어려울 수 있다. 응용 계층은 통지를 관리할 책임이 있다.
- 기술과 관련된 SERVICE에는 업무와 관련된 어떤 것도 포함돼서는 안 된다.
수많은 도메인 SERVICE나 응용SERVICE는 ENTITY와 VALUE를 토대로 만들어져 도메인의 잠재 기능을 조직화 함으로써 실제로 뭔가가 이뤄지게 하는 시나리오와 같다.
뱅킹 애플리케이션에서 거래를 분석할 수 있게 스프레드시트 파일로 거래내역을 변환해 내보낼 수 있다면 내보내기 기능은 응용 SERVICE에 해당한다.
은행 업무 도메인에서는 파일 형식이라는 것이 아무런 의미가 없으며, 그것과 관련된 어떠한 업무 규칙도 없기 때문이다.
한 계좌에서 다른 계좌로 자금을 이체하는 기능은 도메인 SERVICE에 해당한다.
이체 기능에는 중요한 업무 규칙이 포함돼 있고, 이 경우 SERVICE가 그 자체로는 많은 일을 하지 않으며, 두 ACCOUNT 객체가 대부분의 일을 수행하도록 요청할 것이다.
또한 대부분의 개발 시스템에서는 도메인 객체와 외부 자원 간의 직접적인 인터페이스를 만든다는 것이 자연스러워 보이진 않는다. 우리는 그와 같은 외부 SERVICE를 모델의 측면에서 입력을 받아들이는 퍼사드로 만들 수 있으며, 아마도 퍼사드에서는 FUNDS TRANSFER객체를 결과로 반환할 것이다.
서비스를 여러 계층으로 분할하기
구성 단위
이 패턴은 ENTITY와 VALUE OBJECT로부터 클라이언트를 분리하는 것과 함께 도메인 계층의 인터페이스 구성 단위를 제어하는 수단으로서도 매우 가치가 있다.
구성 단위가 중간 크기인 무상태 SERVICE는 대형 시스템에서 재사용하기가 더 쉬울 수 있는데, 왜냐하면 그러한 서비스는 단순한 인터페이스 너머에 중요한 기능을 캡슐화하고 있기 때문이다.
구성 단위가 세밀한 객체는 분산 시스템에서 비효율적인 메시지 전송을 초래할 수 있다.
고도로 세분화된 상호작용의 복잡성이 결국 응용 계층에서 처리되고, 도메인 계층에서 사라진 도메인 지식이 응용 계층의 코드나 사용자 인터페이스 계층의 코드로 스며든다.
도메인 서비스를 적절히 도입하면 계층 간의 경계를 선명하게 하는데 도움될 수 있다.
클라이언트 제어와 융통성보다는 인터페이스의 단순함을 선호한다. 대형 시스템이나 분산 시스템에서 컴포넌트를 패키지화하는 데 매우 유용한 중간 구성 단위의 기능성을 제공한다. 그리고 때로는 SERVICE가 도메인 개념을 표현하는 가장 자연스러운 방법이기도 하다.
SERVICE에 접근하기
SERVICE에 접근하는 수단이 특정 책임을 나누는 설계 의사결정만큼 중요하지는 않다. SERVICE 인터페이스의 구현은 행위자 객체만으로도 충분할 수 있다. 간단한 SINGLETON을 작성해서 손쉽게 접근하게 할 수도 있다.
정교한 아키텍처는 시스템을 분산하거나 프레임워크의 기능을 활용하고자 하는 실제 요구가 있을 때만 사용해야 한다.
MODULE (모듈, 패키지라고도 함)
MODULE은 오래 전부터 확립되어 사용되고 있는 설계 요소다.
모듈화하는 가장 주된 이유는 바로 인지적 과부하 때문이다.
MODULE을 토대로 모델을 두 가지 측면에서 바라볼 수 있다.
- 사람들은 전체에 압도되지 않고도 MODULE에 들어있는 세부사항을 보거나,
- 또는 MODULE에 들어있는 세부사항을 배제한 상태에서 MODULE간의 관계를 볼 수 있다.
도메인 계층의 MODULE은 모델의 중요한 요소로 나타나 도메인의 주요한 내력을 전해야만 한다.
모든 사람들이 MODULE을 사용하지만 그중에서 MODULE을 하나의 완전한 자격을 갖춘 모델 요소로 여기는 사람은 거의 없다. 코드는 기술적 아키텍처에서 개발자에게 할당된 작업까지 온갖 범주의 것으로 나뉜다. 그러나 리팩터링을 많이 하는 개발자도 프로젝트 초기에 생각해낸 모듈에 만족하는 경향이 있다.
MODULE간에는 결합도가 낮아야 하고, MODULE의 내부는 응집도가 높아야 한다는 것은 두말하면 잔소리다.
결합도와 응집도에 대한 설명은 그것을 기술적인 측정 기준처럼 들리게 해서 연관관계와 상호작용의 배분 방법에 근거해 결합도와 응집도의 정도를 기계적으로 판단하게 만든다.
MODULE로 쪼개지는 것은 코드가 아닌 바로 개념이다. 어떤 사람이 한 번에 생각해낼 수 양에는 한계가 있으며, 일관성이 없는 단편적인 생각은 획일적인 생각을 섞어놓은 것 처럼 이해하기 어렵다.
낮은 결합도와 높은 응집도는 개별 객체에서와 마찬가지로 MODULE에도 적용되는 일반적인 설계 원칙이며, 그 원칙은 구성 단위가 큰 모델링과 설계에서는 특히 중요하다.
두 모델 요소가 서로 다른 모듈로 분리될 때 마다 각 요소는 이전에 비해 좀더 간접적인 관계에 놓이는데, 이로써 설계에서 그와 같은 모델 요소의 위치를 파악하는 부담이 늘어난다.
잘 만들어진 모델 요소는 상승효과를 내며, 적절히 선택된 MODULE은 특별히 개념적 관계가 풍부한 모델 요소를 한 곳으로 모아주는 역할을 한다.
- MODULE과 좀더 규모가 작은 요소들은 함께 발전해야 하지만 대개 그렇게 되지는 않는다. MODULE은 객체의 초기 형태를 조직화할 목적으로 사용된다.
- MODULE을 리팩터링하는 것은 클래스를 리팩터링하는 것보다 일이 더 많고, 파급효과가 더 크며, 아마도 자주 하기는 힘들것이다.
- MODULE도 정교해지고 추상적인 형태로 변화할 수 있다.
MODULE도 하나의 의사소통 메커니즘이다. 우리는 분할되는 객체의 의미에 따라 MODULE을 선택해야 한다. 어떤 클래스들을 한 MODULE안에 함께 둔다면 그것은 바로 여러분 옆에서 설계를 살펴보는 동료 개발자에게 그 클래스들을 하나로 묶어서 생각하자고 말하는 것과 같다.
MODULE의 이름은 MODULE의 의미를 전해준다.
- 시스템의 내력을 말해주는 MODULE을 골라 일련의 응집력 있는 개념들을 해당 MODULE에 담아라.
- 이렇게 하면 종종 모듈 간의 결합도가 낮아지기도 하는데, 그렇게 되지 않는다면 모델을 변경해서 얽혀 있는 개념을 풀어낼 방법을 찾아보거나,
- 의미 있는 방식으로 모델의 각 요소를 맺어줄 MODULE의 기준이 될 법한 것 중 미처 못보고 지나친 개념을 살펴보라.
- 서로 독립적으로 이해하고 논리적으로 추론할 수 있다는 의미에서 낮은 결합도가 달성되도록 노력하라.
- 높은 수준의 도메인 개념에 따라 모델이 분리되고 그것에 대응되는 코드도 분리될 때까지 모델을 정제하라.
UBIQUITOUS LANGUAGE를 구성하는 것으로 MODULE의 이름을 부여하라. MODULE과 MODULE의 이름은 도메인에 통찰력을 줄 수 있어야 한다.
개념적 관계를 살펴보는 것이 기술적 측정의 대안은 아니다.
그러나 모델에 초점을 맞춰 사고하면 지엽적인 해결책이 아닌 더욱 심층적인 해결책이 만들어진다.
MODULE을 변경할 경우 각 MODULE을 더 많이 참조해야 하고 변경의 효과가 우발적으로 퍼져나가는 경우에도 마찬가지다. 모델이 일러주는 시스템의 이력을 개발자들이 이해한다면 개발자들은 이러한 문제를 다룰 수 있다.
기민한 MODULE
MODULE은 모델과 함께 발전해야 한다.
MODULE에 대한 리팩터링이 모델과 코드에 대한 리팩터링과 함께 일어난다는 것을 의미한다.
MODULE을 변경하려면 넓은 범위에 걸친 코드를 수정해야만 한다.
이 같은 변경은 팀의 의사소통을 해치고 심지어 소스코드 관리 시스템과 같은 개발 도구를 망칠 수도 있다.
MODULE의 구조와 이름이 현재 클래스가 나타내고 있는 것보다 훨씬 전의 초기 형태의 모델을 반영하기도 한다.
구현이 어떤 개발 기술을 기반으로 하느냐와 관계없이 MODULE을 리팩터링하는 데 소요되는 작업을 최소화하고 다른 개발자들과 의사소통할 때 발생하는 혼란을 최소화하는 방법을 모색해야 한다.
인프라스트럭처 주도 패키지화의 함정
패키지화 결정의 주된 요인은 기술 관련 프레임워크에서 나온다.
매우 유용한 프레임워크 표준의 예는 인프라스트럭처 코드와 사용자 인터페이스 코드를 별도의 패키지 그룹에 두는 식으로 LAYERED ARCHITECTURE를 적용해 도메인 계층을 물리적으로 자체적인 패키지 안으로 들어가게 하는 것이다.
티어 아키텍처는 모델 객체에 대한 구현을 잘게 나눠서 서로 흩어지게 할 수도 있다.
단일 도메인 객체의 책임을 여러 객체에 걸쳐 퍼뜨린 다음, 그러한 객체를 제각기 분리된 패키지에 두는 식으로 티어를 만들어 내기도 한다.
객체의 가장 기본적인 개념 중 하나는 데이터와 해당 데이터를 대상으로 연산을 수행하는 로직을 캡슐화하는 것이다.
이 외에도 프레임워크에서는 각 티어를 일련의 분리된 패키지들에 들어가게 하고 그러한 티어를 식별하는 규약에 따라 이름을 부여할 필요가 있었다.
애플리케이션에는 일부 SERVICE에서 제공하는 행위와 함께 데이터베이스 접근 요구사항만을 기본적으로 이행하는 빈약한 도메인 모델이 포함돼 있었다.
패키지화 계획을 해야 하는 또 다른 이유는 티어 배치 때문이다.
티어 배치는 코드가 실제로 서로 다른 서버에 배포돼 있을 경우 격렬한 논쟁거리가 될 수도 있다.
유연함이 필요한 경우에만 유연함을 추구하라.
기술적인 정교함이 주도하는 패키지화 계획에는 두 가지 비용이 따른다.
패키지화를 바탕으로 다른 코드로부터 도메인 계층을 분리하라. 그렇게 할 수 없다면 가능한 한 도메인 개발자가 자신의 모델과 설계 의사결정을 지원하는 형태로 도메인 객체를 자유로이 패키지화할 수 있게 하라.
한 가지 예외적인 경우는 선언적 설계를 기반으로 코드가 만들어질 때다.
코드를 읽을 필요가 없으므로 생성되는 코드를 가까이에 있지 않게 별도의 패키지에 집어넣어 개발자가 실제로 작업해야 할 설계 요소를 어지럽히지 않는 게 좋다.
도메인 모델의 각 개념은 구현 요소에 반영돼야 한다. ENTITY와 VALUE OBJECT, 그리고 그러한 객체들 간의 연관관계는 일부 도메인 SERVICE와 조직화 MODULE과 함께 구현과 모델이 직접적으로 대응하는 지점이다.
어떤 것의 개념이 도메인 객체와 밀접하게 관련돼 있지 않다면, 그것을 도메인 객체에 추가해서는 안 된다.
수행해야 할 도메인 관련 책임과 시스템을 작동시키기 위해 관리해야 할 데이터가 있지만 그것들은 이러한 객체에 속하는 것이 아니다.
모델링 패러다임
MODEL-DRIVEN DESIGN은 현재 적용 중인 특정 모델링 패러다임과 조화를 이루는 구현 기술을 필요로 한다. 현재는 객체지향 설계가 가장 지배적인 패러다임이며, 오늘날 가장 복잡한 프로젝트에서도 객체를 사용하기 시작하고 있다.
객체지향 패러다임이 우세한 이유는 여러 가지가 있는데, 어떤 요인은 객체 본연의 특성에 기안하며, 또 어떤 것은 상황에 따라 달라지며, 그리고 그 밖에는 객체지향 패러다임이 널리 사용되고 있다는 이점에서 유래한다.
객체 패러다임이 지배적인 이유
- 객체 모델링이 복잡함과 단순함의 절묘한 조화를 이룬다는 것이다.
모델링 패러다임이 너무도 난해해서 개발자들이 숙달할 수 없을 정도라면 개발자들은 그러한 모델링 패러다임을 서툴게 사용하게 될 것이다.
객체지향 설계 원리는 대부분의 사람들도 자연스럽게 이해하는 듯하다.
객체 모델링의 개념은 단순하지만 그것은 중요한 도메인 지식을 포착할 만큼 풍부한 것으로 입증됐다. 또한 객체 모델링은 처음부터 모델을 소프트웨어에서 표현하게 해주는 도구의 지원을 받고 있기도 하다.
성숙한 인프라스트럭처와 도구 지원이 없다면 프로젝트는 기술적인 연구 개발로 벗어나 지체되고, 애플리케이션을 개발하는 데 사용해야 할 자원을 전용해서 기술적인 위험에 처할 수 있다.
현재로서는 MODEL-DRIVEN DESIGN을 시도하는 대다수의 프로젝트에서는 시스템 기반에 객체지향 기술을 사용하는 것이 현명하다. MODEL-DRIVEN DESIGN을 시도하는 프로젝트는 객체 전용 시스템으로 고착화되지는 않을 것이다.
언제까지나 객체 패러다임만 써야 한다는 의미는 아니다. 객체 모델이 수많은 실무적인 소프트웨어 문제를 해결해주지만, 캡슐화된 행위에 대한 개별 묶음으로 모델링하기에는 수월하지 않은 도메인도 있다.
객체 세계에서 객체가 아닌 것들
도메인 모델이 반드시 객체 모델이어야 하는 것은 아니다.
여기서는 모델이 논리적인 규칙과 사실로 구성돼 있었다. 모델 패러다임은 사람들의 도메인에 관한 일정한 사고방식을 다루는 것으로 여겨져 왔다.
다소 극단적으로 말하면 도메인의 상당 부분이 특정한 다른 패러다임에서 훨씬 더 자연스럽게 표현된다면 패러다임을 완전히 교체해서 다른 구현 플랫폼을 선택하는 것이 맞을지도 모른다.
패러다임을 혼합하면 개발자들이 어떤 개념을 그것에 가장 잘 어울리는 형식으로 모델링할 수 있다.
개발자가 소프트웨어에 포함된 응집력 있는 모델을 분명하게 볼 수 없다면 이 같은 여러 패러다임이 혼재하는 시스템에서 MODEL-DRIVEN DESIGN의 필요성이 늘어나더라도 MODEL-DRIVEN DESIGN이 사라질 수 있다.
패러다임이 혼재할 때 MODEL-DRIVEN DESIGN 고수하기
룰 엔진은 때때로 객체지향 애플리케이션 개발 프로젝트에서 혼용되는 기술의 예로 알맞을 것이다.풍부한 지식이 담긴 도메인 모델에는 명시적인 규칙이 포함돼 있겠지만, 그럼에도 여전히 객체 패러다임에는 규칙과 규칙 간의 상호작용을 나타내기 위한 구체적인 의미체계가 부족하기 마련이다.
규칙을 다루는 동안에는 한 모델의 관점에서 사고를 지속하는 것이 중요하다.
팀에서는 두 가지 구현 패러다임에서 모두 작용할 수 있는 단 하나의 모델을 찾아야 한다.
각 부분을 함께 유지하는 가장 효과적인 수단은 완전히 이질적인 모델을 통합할 수 있는 확고한 UBIQUITOUS LANGUAGE다. UBIQUITOUS LANGUAGE에 포함된 명칭들을 두 환경에 일관되게 적용하고 활용한다면 두 환경 사이에 벌어진 틈을 메우는 데 도움될 것이다.
이것은 그 자체로도 한 권의 책으로 낼 만한 주제다.
MODEL-DRIVEN DESIGN이 객체지향적일 필요는 없지만 MODEL-DRIVEN DESIGN은 표현력이 풍부한 모델 구성물의 구현에 분명 의존한다.
객체가 아닌 요소를 객체지향 시스템에 혼합하는 데는 경험상 다음의 4가지 법칙이 있다.
- 구현 패러다임을 도메인에 억지로 맞추지 않는다.
- 유비쿼터스 언어에 의지한다.
- UML에 심취하지 않는다.
- 회의적이어야 한다.
관계형 패러다임은 패러다임 혼합의 특수한 경우다. 가장 흔히 접하는 비객체 기술인 관계형 데이터베이스는 다른 구성요소에 비해 객체 모델과 더욱 직접적인 관련이 있는데, 이것은 관계형 데이터베이스가 객체를 구성하는 데이터의 영구 저장소 역할을 하기 때문이다.