Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

5장 소프트웨어에서 표현되는 모델 #606

Closed
Tracked by #585
jongfeel opened this issue Dec 2, 2023 · 4 comments
Closed
Tracked by #585

5장 소프트웨어에서 표현되는 모델 #606

jongfeel opened this issue Dec 2, 2023 · 4 comments
Assignees
Labels
2023 2024 Domain-Driven Design 도메인 주도 설계 - 소프트웨어의 복잡성을 다루는 지혜

Comments

@jongfeel
Copy link
Owner

jongfeel commented Dec 2, 2023

5장 소프트웨어에서 표현되는 모델

모델과 구현은 상세 수준에서 연결돼야 한다. 객체 간의 연관관계(association)를 설계하고 이해해서 묘사하기는 간단하지만 그것을 실제로 구현하는 것은 잠재적으로 다루기 힘든 문제이다. 그래서 상세한 구현 결정이 MODEL-DRIVEN DESIGN을 실현하는 데 얼마나 중요한가를 아는 게 중요하다.

도메인 개념을 담은 객체를 정의하는 일은 쉬워 보여도 의미상의 차이로 발생할 수 있는 문제가 잠재되어 있다.
모델 요소의 의미를 명확하게 하고 특정 종류의 객체를 도출하기 위한 설계 수행체계에 부합하는 일정한 구분법이 나타났다.

객체에 연속성(continuity)과 식별성(identity)을 가진다는 걸 의미하는 것과, 다른 상태를 기술하는 특성인가에 대해서 ENTITY와 VALUE OBJECT를 구분하는 가장 기본적인 방법이다. 객체를 정의할 때 한 패턴이나 다른 뭔가를 분명하게 따르게 한다면 그 객체는 명확해지고 견고한 설계를 위한 구체적인 설계 결정을 내리는 데 도움이 된다.

객체보다는 행동(action)이나 연산(operation)으로 더 명확하게 표현되는 것도 있다. 이건 ENTITY나 VALUE OBJECT로 하기 보다는 SERVICE로 표현하는 게 낫다. SERVICE는 클라이언트 요청에 대해 수행되는 뭔가를 의미한다. 도메인에서 SERVICE는 소프트웨어에서 수행해야 하는 것에 해당하는데 상태를 주고받지는 않는 활동을 모델링 하는 경우가 여기에 해당한다.

MODULE에 대해 살펴보면서 모든 설계 관련 의사결정은 도메인에 부여된 통찰력을 바탕으로 내려야 한다는 사실을 알 수 있다. 기술적인 측정 수단으로 높은 응집도와 낮은 결합도라는 개념은 도메인 개념에도 적용할 수 있다.

연관관계

모델 내의 모든 탐색 가능한(traversable) 연관관계에 대해 그것과 동일한 특성을 지닌 메커니즘이 소프트웨어에도 있다.

연관관계를 쉽게 다루는 방법으로 아래 세 가지가 있다.

  1. 탐색 방향 부여
  2. 한정자(qualifier)를 추가해서 사실상 다중성(multiplicity)을 줄인다.
  3. 중요하지 않은 연관관계 제거

가능한 관계를 제약한다. 양방향은 두 객체가 모두 알고 있어야만 이해할 수 있다. 두 방향을 모두 탐색해야 한다는 요건이 없을 경우 탐색 방향을 추가하면 상호의존성이 줄어들고 설계가 단순해진다. 도메인을 이해한다면 도메인 본연의 방향성이 드러난다.

실용적인 관점에서 국가에서 대통령의 양방향 관계에서 단방향(unidirectional) 연관관계를 제거할 수 있다. 이러한 정제 과정을 통해 실제적인 설계를 만들 수 있고, 실제 도메인에 통찰력을 반영하게 된다. 연관관계에서 한 방향이 다른 것에 비해 훨씬 더 의미 있고 중요하다는 점을 포착한 것이다.

image

도메인을 깊이 이해하다 보면 자주 "한정적인(qualified)" 관계에 이른다. 대통령은 한 나라에 한 번에 한 명만 있다는 사실을 알 수 있으므로 한정자는 다중성을 일대일로 줄이고 중요한 규칙을 명시적으로 모델에 포함시킨다.

image

도메인의 특성이 반영되게 연관관계를 일관되게 제약하면 연관관계의 의사전달력이 풍부해지고 구현이 단순해지며, 나머지 양방향 연관관계도 의미를 지니게 된다. 관계의 양방향성이 도메인의 의미적 특징에 해당하고, 애플리케이션 기능에 양방향성이 필요하면 두 가지 탐색방향을 모두 유지하는 건 그와 같은 사실을 나타내는 셈이다.

당명한 문제에 필요한 게 아니거나 중요한 의미가 없는 모델 객체가 아니면 궁극적인 단순화는 연관관계를 완전히 제거하는 것이다.

예제 - 증권계좌 연관관계

image

Brokerage Account를 구현하면 다음과 같다.

public class BrokerageAccount {
    String accountNumber;
    Customer customer;
    Set investments;
    // Constructors, etc. omitted
    public Customer getCustomer() {
        return customer;
    }
    public Set getInvestments() {
        return investments;
    }
}

데이터베이스에서 동일하게 모델과의 일관성을 유지한 채로 데이터를 가져와야 한다면 테이블은 아래와 같을 것이다.

TABLE: BROKERAGE_ACCOUNT

ACCOUNT_NUMBER CUSTOMER_SS_NUMBER

TABLE: CUSTOMER

SS_NUMBER NAME

TABLE: INVERSTMENT

ACCOUNT_NUMBER STOCK_SYMBOL AMOUNT

데이터베이스와 연결된 Brokerage Account는 다음과 같다.

public class BrokerageAccount {
    String accountNumber;
    String customerSocialSecurityNumber;
    // Omit constructors, etc.
    public Customer getCustomer() {
        String sqlQuery =
            "SELECT * FROM CUSTOMER WHERE" +
            "SS_NUMBER='" + customerSocialSecurityNumber + "'";
        return QueryService.findSingleCustomerFor(sqlQuery);
    }
    public Set getInvestments() {
        String sqlQuery =
            "SELECT * FROM INVESTMENT WHERE" +
            "BROKERAGE_ACCOUNT='" + accountNumber + "'";
        return QueryService.findInvestmentsFor(sqlQuery);
    }
}

Brokerage Account와 Investment 간의 다중성을 줄이고 연관관계를 한정해서 모델을 정제하면 증권계좌당 하나의 투자 종목만 관리할 수 있다.

image

어떤 특별한 규칙이든 연관관계에 대한 제약조건이 발견되면 해당 제약조건은 모델과 구현에 포함돼야 한다.
다시 Brokergage Account는 다음과 같이 구현할 수 있다.

public class BrokerageAccount {
    String accountNumber;
    Customer customer;
    Map investments;
    // Omitting constructors, etc.
    public Customer getCustomer() {
        return customer;
    }
    public Investment getInvestment(String stockSymbol) {
        return (Investment) investments.get(stockSymbol);
    }
}

SQL을 포함하면 다음과 같다.

public class BrokerageAccount {
    String accountNumber;
    String customerSocialSecurityNumber;
    //Omitting constructors, etc.
    public Customer getCustomer() {
        String sqlQuery = "SELECT * FROM CUSTOMER WHERE SS_NUMBER='" +
            customerSocialSecurityNumber + "'";
        return QueryService.findSingleCustomerFor(sqlQuery);
    }
    public Investment getInvestment(String stockSymbol) {
        String sqlQuery = "SELECT * FROM INVESTMENT " +
            "WHERE BROKERAGE_ACCOUNT='" + accountNumber + "'" +
            "AND STOCK_SYMBOL='" + stockSymbol + "'";
        return QueryService.findInvestmentFor(sqlQuery);
    }
}

모델에 포함된 연관관계를 신중하게 정제하고 제약하면 MODEL-DRIVEN DESIGN으로 나가는데 크게 도움이 된다.

ENTITY (엔티티, 참조 객체)

image

수많은 객체는 본질적으로 해당 객체의 속성이 아닌 연속성과 식별성이 이어지느냐를 기준으로 정의된다.

많은 것들이 속성이 아닌 식별성에 의해 정의된다. 사람의 이름은 바뀔 수도 있고 금융관계는 일정하지 않다. 사람에게 변하지 않는 속성은 없지만 식별성은 여전히 지속된다. 5살의 나와 지금의 나는 같은 사람인가? 와 같은 질문은 효과적인 도메인 모델을 찾는 데 중요하다. 애플리케이션을 사용하는 사람은 5살이었을 때의 나와 지금의 내가 동일 인물인지 중요하게 생각할까?

객체 모델링을 할 때 객체의 속성에 집중하지만, ENTITY의 근본적인 개념은 객체의 생명주기 내에 이어지는 추상적인 연속성이며, 추상적인 연속성은 여러 형태를 거쳐 전달된다.

어떤 객체를 일차적으로 해당 객체의 식별성으로 정의할 경우 그 객체를 ENTITY라 한다.
ENTITY에는 모델링과 설계상의 특수한 고려사항이 포함돼 있다.
ENTITY는 자신의 생명주기 동안 형태와 내용이 바뀔 수 있지만 연속성은 유지해야 한다.
ENTITY를 추적하려면 ENTITY에 식별성이 정의돼 있어야 한다.
ENTITY의 클래스 정의와 책임, 속성, 연관관계는 ENTITY에 포함된 특정 속성보다는 ENTITY의 정체성에 초점을 맞춰야 한다.

ENTITY는 생명주기 내내 이어지는 연속성과 애플리케이션 사용자에게 중요한 속성과는 독립적인 특징을 가진다.

한편으로 모델 내의 모든 객체가 의미 있는 식별성을 지닌 ENTITY인 건 아니다. 문제는 객체지향 언어에서 모든 객체에 "동일성(identity)" 연산이 내장되어 있다는 점 때문에 혼동된다. == 연산은 두 객체의 메모리 주소를 통해 두 객체 참조가 같은 객체를 가리키고 있는지 판단한다. 이런 점에서는 모든 객체 인스턴스는 식별성을 지녔다고 볼 수 있다. 이런 식별 매커니즘은 다른 애플리케이션 도메인에서는 의미하는 바가 그리 크지 않다. 식별성은 ENTITY의 미묘하고 의미 있는 속성이므로 언어에서 제공하는 자동화된 기능으로 대체할 수 없다.

식별성은 특정 소프트웨어 시스템 이외의 은행 거래나 아파트 입주와 같은 경우에도 중요한 의미를 지닌다. 간혹 식별성은 컴퓨터 프로세스 식별자와 같이 시스템 맥락에서만 중요한 의미를 지니기도 한다.

그러므로

한 객체가 속성 보다는 식별성으로 구분될 경우 모델 내에서 해당 객체의 주된 정의로 삼아라. 정의를 단순하게 하고 생명주기의 연속성과 식별성에 집중하라. 객체의 형태나 이력에 관계 없이 각 객체를 구별하는 수단을 정의하라. 객체의 속성으로 객체의 일치 여부를 판단하는 요구사항에 주의해야 하고 객체에 대해 유일한 결과를 반환하는 연산을 정의하라. 모델에서 식별성을 구분하는 방법과 일치해야 한다. 모델이 동일하다는 것이 무슨 의미인지 정의해야 한다.

식별성은 원래 세상에 존재하는 것이 아니라, 필요에 의해 보충된 것이다. 현실세계의 같은 사물이라도 도메인 모델에서 ENTITY로 표현되거나 표현되지 않을 수도 있다.

ENTITY 모델링

ENTITY의 가장 기본적인 책임은 객체의 행위가 명확하고 예측 가능해질 수 있게 연속성을 확립하는 것이다. ENTITY는 별도로 분리돼 있을 때 자신의 책임을 가장 잘 수행한다. ENTITY의 속성이나 행위에 집중하기 보다는 ENTITY 객체를 해당 ENTITY 객체의 가장 본질적인 특징만으로 정의한다. 개념에 필수적인 행위만 추가하고 그 행위에 필요한 속성만 추가한다.

그림 5.5에서 고객ID는 Customer라는 ENTITY의 유일한 식별자이고 전화번호와 주소는 Customer를 찾거나 일치 여부를 판단하는데 사용한다. 이름의 경우는 간혹 식별성을 판단하는 수단의 일부로 사용한다. 이 예제에서는 전화번호와 주소를 Customer로 옮겼지만 실제 프로젝트에서는 고객의 일치 여부나 고객을 구분하는 방법에 따라 달라지게 된다.

image

식별 연산의 설계

ENTITY는 다른 객체와 구분해줄 식별성을 만들어내는 수단이 반드시 있어야 한다. 식별에 사용되는 속성은 시스템의 상태와 관계 없이 해당 시스템 내에서 유일해야 한다.

기술적인 문제를 단순화하는 프레임워크를 사용한다고 해도 두 객체가 개념적으로 동일한 ENTITY를 나타내는지 알 수 없다. 식별성에 대한 정의는 모델로부터 나온다. 따라서 식별성을 정의하려면 도메인을 이해해야 한다.

한 객체의 속성으로 구성되는 실질적인 고유키가 없다면 각 인스턴스에 해당 클래스 내에서 유일한 기호를 덧붙이는 방법을 쓴다. 이 ID가 생성되서 ENTITY의 속성으로 저장되면 불변성을 가진다. 가끔 기술 관련 프레임워크가 이렇게 하는 데 도움이 되며, 그렇지 못한 경우에는 공학적 노력만 기울이면 된다.

식별성에 관한 문제는 모델의 구체적인 면면에 따라 달라진다는 사실을 알고 있어야 한다. 식별 수단도 마찬가지로 도메인에 대한 철저한 연구가 필요하다.

자동으로 ID가 생성되는 경우에는 ID를 볼 필요가 없을 수도 있다. 내부적으로만 필요한 경우일 수 있는데, 사용자가 사람 이름으로 레코드를 찾을 수 있는 연락처 관리 애플리케이션과 같은 경우가 있다.

생성된 ID가 사용자에게 중요한 경우는, 택배 번호 조회라던가 항공권, 호텔 예약에도 예약 거래 식별자 확인번호가 필요할 수 있따.

ID의 유일성이 컴퓨터 시스템의 범위를 넘어 적용되는 경우는 개별 컴퓨터 시스템을 갖춘 두 병원 간에 의료 기록을 교환하는 경우이다. 각 병원에서 자체적으로 ID를 스는 경우 통합이 쉽지 않으므로 대개 정부 기관과 같이 다른 기관에서 발행하는 식별자를 사용한다.

비디오 대여점처럼 덜 공식적인 경우에는 전화번호를 식별자로 사용하는데, 전화 번호 공유나 변경, 도용의 문제가 있다.

애플리케이션에서 외부 ID가 필요한 경우 시스템 사용자는 유일한 ID를 제공할 책임을 지게 되고, 해당 시스템에서는 발생하는 예외 상황을 처리할 적절한 수단을 제공해야 한다.

기술적 문제를 감안해도 "두 객체가 동일하다는 것이 무엇을 의미하는가?" 라는 근본적인 문제를 놓치기 쉽다. ID나 연산이 도메인에 의미 있는 구분법에 부합하지 않는다면 문제를 더 혼란스럽게 만든다.

VALUE OBJECT (값 객체)

image

개념적 식별성이 없는 객체는 사물의 어떤 특징을 묘사한다.

대개 모델에서 가장 눈에 잘 띄는 객체는 ENTITY이며, ENTITY의 식별성을 관리하는 일은 모든 도메인 객체에 식별성을 할당하려고 고려해 보는 것은 자연스러운 일이다. 일부 프레임워크에서는 모든 객체에 고유 ID를 할당하기도 한다.

의미 있는 식별성의 정의하고, 분산 시스템에 걸쳐 있거나 데이터베이스 저장소에 들어 있는 객체를 손쉽게 추적하는 수단을 마련하려면 분석적인 노력이 필요하다. 인위적으로 만들어지는 식별성은 오해를 불러일으킬 수 있다. 이렇게 되면 모델이 뒤죽박죽이 되서 모든 객체가 한 덩어리로 뭉쳐진다.

객체에 식별성을 추가한다면 시스템의 성능이 저하되고 분석 작업이 별도로 필요하며, 모든 객체를 동일한 것으로 보이게 해서 모델이 혼란스러워질 수 있다.

소프트웨어 설계는 복잡성과의 끊임없는 전투이므로 특별하게 다뤄야 할 부분과 아닌 부분을 구분해야 한다.

객체를 단순히 식별성 없는 것으로만 생각하면 도구상자나 어휘에 추가할게 그리 많지 않게 된다. 이런 객체는 자체적인 특징을 비롯해 모델에 중요한 의미를 지니는데 이게 사물을 서술하는 객체다.

개념적 식별성을 갖지 않으면서 도메인의 서술적 측면을 나타내는 객체를 VALUE OBJECT라 한다. VALUE OBJECT는 설계 요소를 표현할 목적으로 인스턴스화되는데, 이러한 설계 요소가 어느 것인지에 대한 것 보다는 해당 요소가 무엇인지에 더 관심이 있다.


"주소"는 VALUE OBJECT 인가?

우편 주문 회사에서 물건을 보낸 주소가 필요한데, 같은 곳에 사는 사람이 같은 회사에 물건을 주문해도 같은 곳에 살고 있다는 사실은 중요하지 않다.
이 경우 주소는 VALUE OBJECT다.

우편 서비스의 배송 경로는 계층 구조 형태로 만들 수 있다. 주소 객체는 계층구조상 부모로 부터 우편번호를 도출해낼 수 있는데 우편 서비스에서 배달 구역을 재할당하기로 하면 거기에 속하는 모든 주소는 부모 계층의 주소를 따라 바뀌게 된다.
이 경우 주소는 ENTITY다.

전기 설비 회사에서 주소는 전선 및 전기 공급의 목적지에 해당한다. 전기 점검을 하게 되면 점검 목적지를 파악해야 하는데
이 경우 주소는 ENTITY다.
하지만 모델에서 주소 속성이 포함된 ENTITY인 "거주지"와 설비 점검을 연관시킨다면
이 경우 주소는 VALUE OBJECT일 것이다.


색(color)은 개발 시스템의 기반 라이브러리에서 제공되는 VALUE OBJECT의 한 예이다.

어떤 VALUE OBJECT는 다른 여러 객체를 조립한 것일 수 있다. 주택 설계 소프트웨어에서 창문 양식마다 각각의 객체를 만들어 낼 수 있다. 창문 양식은 높이/너비와 함께 창문 객체에 통합할 수 있고 속성이 바뀌고 결합되는 방식을 관장하는 규칙도 "창문" 객체에 통합될 수 있다.

VALUE OBJECT가 ENTITY를 참조할 수도 있다. 두 도시가 연결된 Route 객체가 있다면 그것이 참조하는 세 객체인 두 도시와 하나의 고속도로가 모두 ENTITY라고 해도 VALUE에 해당한다.

VALUE OBJECT는 여러 객체 간에 오가는 메시지의 매개변수로 전달되기도 한다.
VALUE OBJECT는 한 연산에서 사용할 목적으로 만들어진 후 폐기되는 것처럼 일시적인 용도로 사용한다.
ENTITY(그리고 다른 VALUE)의 속성으로 사용되기도 한다. 한 사람을 식별성을 가진 ENTITY로 해도 이름은 VALUE로 할 수 있다.

모델에 포함된 어떤 요소의 속성에만 관심이 있다면 그건 VALUE OBJECT로 분류한다. VALUE OBJECT가 전하는 속성의 의미를 표현하게 하고 관련 기능을 부여한다. VALUE OBJECT는 불변(immutable)으로 다룬다. VALUE OBJECT에는 아무런 식별성도 부여하지 말고 ENTITY를 유지하는 데 필요한 설계상의 복잡성을 피한다.

VALUE OBJECT를 구성하는 속성은 개념적 완전성(conceptual whole)을 형성해야 한다.
개념적 완전송은 워드 커닝햄의 WHOLE VALUE 패턴이다.

image

VALUE OBJECT의 설계

제약 조건을 줄이면 설계 단순화나 성능 최적화를 노릴 수 있다. 하지만 여기에는 복사, 공유, 불변성에 관한 의사결정이 따른다.

두 사람의 이름이 같다고 해서 두 사람이 동일 인물이 되거나 상호 교체할 수 있는 것은 아니다. 이름을 나타내는 객체는 교환이 가능한데, 이름은 이름의 철자만 중요하기 대문에 그렇다. 그래서 한 Person 객체에서 다른 Person 객체의 Name을 복사할 수 있다.

만약 동일한 Name 객체가 두 Person 객체 간에 공유가 가능할 수도 있는데, 두 Person 객체의 행위나 식별성은 아무것도 변경할 필요가 없다. 하지만, 한 사람의 이름이 바뀌면 다른 사람의 이름이 바뀌기 때문에 이런 변경의 방지를 위해 해당 객체가 불변적이어야 한다.

VALUE OBJECT는 많아지는 경향이 있기 때문에 성능 최적화를 위한 별도의 대한을 마련하는 것이 중요할 수 있다.

복사와 공유 중 어느 것이 경제성 면에서 더 나은지는 구현 환경에 따라 달라진다. 복사는 객체의 개수가 많아져서 시스템이 무거워지고 느려질 수 있다. 두 장비간에 객체 복사본이 전달되는 경우라면 각 장비마다 복사복이 독립적으로 남는다. 한 인스턴스를 공유하는 경우는 객체의 참조만 전달되기 때문에 상호작용이 발생할 때마다 메시지가 해당 객체로 되돌아와야 한다.

공유는 아래의 경우에 도움이 되고 문제가 적게 일어나며, 이런 경우로 공유를 제한한다.

  • 공간을 절약하거나 데이터베이스 내의 객체 수를 줄이는 것이 중요한 경우
  • 통신 부하가 낮은 경우 (중앙집중형 서버)
  • 공유 객체의 불변성이 엄격하게 지켜지는 경우

언어에서 ENTITY의 식별성 연산 같은 개념적 구분을 직접적으로 지원해주지 않는다고 해서 그런 구분 자체가 유용하지 않다는 의미는 아니다. 구현에 암시적으로만 존재할 규칙을 유지하는 노력이 좀 더 필요하다는 의미이다. 이건 명명관례와 선택적 문서화를 비롯해 수많은 논의를 거쳐 개선할 수 있다.

VALUE OBJECT가 불변적인 한 변경관리는 단순해진다. 완전히 교체하지 않는 한 아무것도 변경되지 않는다.


특별한 경우: 변경 가능성을 허용하는 경우

불변성은 공유와 객체 참조 전달을 안전하게 만들어 구현을 상당히 단순하게 만들어준다. 속성값이 바뀌면 기존 VALUE OBJECT가 아닌 다른 VALUE OBJECT를 사용한다. 그런데도 성능 문제로 VALUE OBJECT를 변경하도록 허용할 때가 있는데 변경 가능한 구현에 영향을 주는 요인은 다음과 같다.

  • VALUE가 자주 변경되는 경우
  • 객체 생성이나 삭제에 비용이 많이 드는 경우
  • 교체(변경이 아닌)로 인해 클러스터링이 제한되는 경우
  • VALUE를 공유할 일이 많지 않거나 클러스터링을 향상시키기 위해 다른 기술적인 이유로 공유가 보류된 경우

VALUE의 공유 여부와 관계없이 VALUE OBJECT는 가급적 변하지 않게 설계한다.


필수적인 제약조건을 명시적으로 정의하면 중요행위가 변경되는 것으로부터 설계를 안전하게 유지하는 동시에 개발자들이 설계를 최적화할 수 있다. 그런 설계 최적화는 특정 프로젝트에서만 사용하는 기술에 매우 종속적일 때가 있다.

예제: VALUE OBJECT를 활용한 데이터베이스 최적화

한 객체가 다른 여러 객체에서 참조되고 있다면 그런 객체 가운데 일부는 가까이에 위치하지 않기에 데이터를 가져오는 데 물리적인 연산이 추가로 필요하게 된다. 동일한 인스턴스에 대한 객체 참조를 공유하기보다는 해당 인스턴스의 사본을 만드는 식으로 여러 ENTITY의 속성 역할을 하는 VALUE OBJECT는 각 ENTITY가 사용하고 있는 것과 같은 페이지에 저장될 수 있다. 동일한 데이터에 여러 개의 사본을 저장하는 이 같은 기법을 "역정규와(denormalization)"이라고 하며, 저장 공간이나 유지보수의 단순함보다는 접근 시간이 더 중요한 경우에 종종 사용한다.

VALUE OBJECT를 포함한 연관관계 설계

VALUE OBJECT 간의 양방향 연관관계는 제거하도록 노력해야 한다. 모델에 그와 같은 연관관계가 필요해 보여도 그 객체를 VALUE OBJECT로 선언하는 것은 한번 더 고려해봐야 한다.

SERVICE (서비스)

image

설계가 명확하고 실용적이어도 개념적으로 객체에 속하지 않는 연산이 포함될 때가 있다.
이런 문제를 억지로 해결하기보다는 문제 자체의 면면에 따라 SERVICE를 모델에 명시적으로 포함할 수 있다.

ENTITY나 VALUE OBJECT에서 찾지 못하는 도메인 연산이 있는데, 일부는 본질적으로 사물이 아닌 활동activity나 행동action이다. 모델링 패러다임은 객체이므로 이런 연산도 객체와 어울리게 해야 한다.

행위를 적절한 객체로 다듬는 것이 어렵다보니 점점 절차적 프로그래밍에 빠지는 것이 흔히 하는 실수이다. 객체에 어울리지 않는 연산을 강제로 객체에 포함시킨다면 그 객체는 자신의 개념적 명확성을 잃어버리고 이해하거나 리팩터링 하기 힘들어질 것이다.

도메인 개념에서 객체로 어울리지 않는 모델이 있다. 필요한 도메인 기능을 ENTITY나 VALUE에 맡게 하면 모델에 기반을 둔 객체의 정의가 왜곡되거나 무의미하고 인위적으로 만들어진 객체가 추가될 것이다.

SERVICE는 모델에서 독립적인 인터페이스로 제공되는 연산으로서 ENTITY나 VALUE OBJECT와 달리 상태를 캡슐화하지 않는다. 기술적인 프레임워크에서 SERVICE는 흔히 사용하는 패턴이지만 SERVICE는 도메인 계층에도 마찬가지로 적용될 수 있다.

서비스라는 이름은 다른 객체와의 관계를 강조한다. ENTITY나 VALUE OBJECT와 달리 SERVICE를 정의하는 기준은 순전히 클라이언트에 무엇을 제공할 수 있느냐에 있다. 그래서 SERVICE는 주로 활동으로 이름을 짓는다. SERVICE의 매개변수와 결과는 도메인 객체여야 한다.

어떤 연산이 도메인에서 중요한 위치를 차지한다면 모델 내에 SERVICE로 선언해서 독립적인 연산이 잘못된 곳으로 가지 않게 한다.

잘 만들어진 SERVICE는 아래와 같은 특징을 가진다.

  1. 연산이 원래부터 ENTITY나 VALUE OBJECT의 일부를 구성하는 것이 아니라 도메인 개념과 관련돼 있다.
  2. 인터페이스가 도메인 모델의 외적 요소의 측면에서 정의된다.
  3. 연산이 상태를 갖지 않는다.

상태를 갖지 않는다는 건 SERVICE의 모든 인스턴스를 사용할 수 있다는 의미이다. SERVICE는 도메인 객체와 달리 자신의 행위에 영향을 줄 수 있는 상태를 갖지 않는다.

도메인의 중대한 프로세스나 변환 과정은 SERVICE의 독립 인터페이스의 연산으로 모델에 추가하라.
모델의 언어 측면에서 인터페이스를 정의하고 연산의 이름을 UBUQUITOUS LANGUAGE의 일부가 되게 한다.
SERVICE는 상태를 갖지 않게 만든다.

SERVICE와 격리된 도메인 계층

문헌상에 논의되는 SERVICE는 대부분 순수하게 기술적이며 인프라스트럭처 계층에 속한다.

수많은 도메인 SERVICE나 응용 SERVICE는 ENTITY와 VALUE를 토대로 만들어져 도메인의 잠재 기능을 조직화해 실제 뭔가 이뤄지게 하는 시나리오와 같다.

계좌 이체와 같은 기능은 도메인 SERVICE에 해당한다. 자금 이체 기능은 중요한 업무 규칙이 포함돼 있고, "자금 이체"는 중요한 은행업무 도메인의 용어이기 때문이다.

Partitioning Services into Layers

Application Funds Transfer App Service
  • Digests input (such as an XML request).
  • Sends message to domain service for fulfillment.
  • Listens for confirmation.
  • Decides to send notification using infrastructure service.
Domain Funds Transfer Domain Service
  • Interacts with necessary Account and Ledger objects, making appropriate debits and credits.
  • Supplies confirmation of result (transfer allowed or not, and so on).
    Infrastructure Send Notification Service
    • Sends e-mails, letters, and other communications as directed by the application.

    구성 단위

    이 패턴은 ENTITY와 VALUE OBJECT로 부터 클라이언트를 분리하는 것과 함께 도메인 계층의 인터페이스 구성 단위를 제어하는 수단으로서도 가치가 있다.

    구성 단위가 중간 크기인medium-grained 무상태 SERVICE는 대형 시스템에서 재사용하기 쉬울 수 있는데, 단순한 인터페이스 너머에 중요한 기능을 캡슐화하고 있기 때문이다.
    구성 단위가 세밀한fine-grained 객체는 분산 시스템에서 비효율적인 메시지 전송을 초래할 수 있다.

    이 패턴은 클라이언트 제어와 융통성보다는 인터페이스의 단순함을 선호한다. 이는 대형 시스템이나 분산 시스템에서 컴포넌트를 패키지화하는 데 매우 유용한 중간 구성 단위의 기능성을 제공한다. 때로 SERVICE가 도메인 개념을 표현하는 가장 자연스러운 방법이기도 하다.

    SERVICE에 접근하기

    분산 시스템 아키텍처는 SERVICE에 대한 특수한 공개 매커니즘을 비롯해 해당 SERVICE의 사용과 관련된 규약을 제공하며, 배포 및 접근 기능도 포함돼 있다. 하지만 언제나 이런 프레임워크를 사용하는 건 아니기도 하고, 단순히 관심사의 분리를 위한 것이라면 프로젝트가 지나치게 복잡해질 가능성이 있다.

    SERVICE 인터페이스 구현은 "행위자" 객체만으로도 충분할 수 있고, 간단한 INGLETON을 작성해서 손쉽게 접근하게 할 수도 있다. 코딩 규약을 토대로 단순히 SERVICE 인터페이스이 전달 매커니즘에 불과하다는 점을 명확하게 드러낼 수도 있다. 정교한 아키텍처는 시스템을 분산하거나 프레임워크의 기능을 활용하고자 하는 실제 요구가 있을 때만 사용해야 한다.

    MODULE (모듈, 패키지)

    MODULE은 오래 전부터 확립되어 사용하고 있는 설계 요소이다. 모듈화를 하는 주된 이유는 인지적 과부하cognitive overload 때문이다. 전체에 압도되지 않고 MODULE에 들어 있는 세부 사항을 보거나, MODULE에 들어 있는 세부사항을 배제한 상태에서 MODULE 간의 관계를 볼 수 있다.

    도메인 계층의 MODULE은 모델의 중요한 요소로 나타나 도메인의 주요한 내력을 전해야만 한다.

    MODULE을 많이 사용하지만 하나의 완전한 자격을 갖춘 모델 요솔 여기는 사람은 거의 없다. 리팩터링을 많이 하는 개발자도 프로젝트 초기에 생각해낸 모듈에 만족하는 경향이 있다.

    MODULE 간에는 결합도가 낮아야 하고 MODULE 내부는 응집도가 높아야 한다. MODULE로 쪼개지는 것은 코드가 아닌 개념이다. 어떤 사람이 한 번에 생각해낼 수 있는 양에는 한계가 있으며 (따라서 결합도를 낮춰야 한다), 일관성이 없는 단편적인 생각은 획일적인 생각을 섞어 놓은 것처럼 이해하기 어렵다(따라서 응집도를 높여야 한다).

    잘 만들어진 모델 요소는 상승효과synergy를 내며, MODULE은 특별히 개념적 관계가 풍부한 모델 요소를 한 곳으로 모아주는 역할을 한다. 관련 책임을 지는 객체의 높은 응집도는 모델링과 설계 업무를 한 사람이 쉽게 다룰 수 있는 복잡함의 측정 기준인 단일 MODULE로 모이게 해준다.

    MODULE과 규모가 작은 요소들은 함께 발전해야 하지만 보통 그렇게 되지 않는다. MODULE은 객체의 초기 형태를 조직화할 목적으로 사용된다. 그러고 나면 객체는 기존 MODULE이 정의한 범위 안에 머무를 수 있는 방식으로 변화한다. 도메인을 이해하는 바가 바뀔 때마다 이를 MODULE에도 반영하면 MODULE안의 객체도 더 자유롭게 발전할 수 있다.

    MODULE도 하나의 의사소통 매커니즘이다. 분할되는 객체의 의미에 따라 MODULE을 선택해야 한다. 어떤 클래스들을 한 MODULE 안에 함께 두면 그건 클래스들을 하나로 묶어서 생각하자고 말하는 것과 같다. 모델이 어떤 이야기라면 MODULE은 이야기의 각 장에 해당한다. MODULE 이름 역시 의미를 전달하며 UBIQUITOUS LANGUAGE에 들어간다.

    그러므로

    시스템의 내력을 말해주는 MODULE을 골라 응집력 있는 개념들을 해당 MODULE에 담아라.
    모듈간의 결합도가 낮아지지 않는다면 모델을 변경해서 얽혀 있는 개념을 풀 방법을 찾거나 모델의 각 요소를 맺어줄 MODULE의 기준이 될 개념을 찾아보자.
    서로 독립적으로 이해하고 논리적으로 추론할 수 있다는 의미에서 낮은 결합도가 달성되도록 노력하자.
    높은 수준의 도메인 개념에 따라 모델이 분리되고 그것에 대응되는 코드도 분리될 때까지 모델을 정제하라.
    UBIQUITOUS LANGUAGE를 구성하는 것으로 MODULE의 이름을 부여하라. MODULE의 이름은 도메인에 통찰력을 줄 수 있어야 한다.

    모델에 초점을 맞춰 사고하면 지엽적인 해결책이 아닌 심층적인 해결책이 만들어진다.

    기민한 MODULE

    MODULE은 모델과 함께 발전해야 한다. 이건 MODULE에 대한 리팩터링이 모델과 코드에 대한 리팩터링과 함께 일어난다는 것을 의미한다. MODULE을 변경하려면 넓은 범위에 걸친 코드를 수정해야만 한다.

    MODULE을 선택할 때 초기에 한 불가피한 실수로 결합도가 높아지면 리팩터링을 수행하기가 어려워진다. 리팩터링을 자주 수행하지 않는다면 상황은 점점 나빠질 것이다. 고통을 참고 경험을 바탕으로 문제가 있는 부분의 모듈을 재조직해야만 문제를 해결할 수 있다.


    예제: 자바 언어의 패키지 코딩 관례

    자바에서는 널리 통용되는 코딩 관례로 의존성 임포트를 개별 클래스에 선언해야 한다.

    ClassA1
    import packageB.ClassB1;
    import packageB.ClassB2;
    import packageC.ClassC1;
    import packageC.ClassC2;
    ...

    한번에 전체 패키지를 임포트할 수 있는 방법도 있는데, 이렇게 하면 패키지명을 변경하는 노력도 줄어들고 패키지가 응집력 있는 단위라는 의도가 반영되기도 한다.

    ClassA1
    import packageB.;
    import packageC.
    ;

    이 기법은 특정 MODULE에 대한 의존성이 만들어진다는 의도를 전해주기도 한다.

    어떤 개별 클래스가 실제로 다른 패키지의 특정 클래스에 의존하고 현 위치의 MODULE이 그 밖의 MODULE에 개념적으로 의존하지 않는 것처럼 보인다면 클래스를 옮기거나 MODULE 자체를 다시 고려해봐야 한다.


    인프라스트럭처 주도 패키지화의 함정

    패키지와 결정의 주된 요인은 기술 관련 프레임워크에서 나온다. 어떤 것은 도움외 디지만 어떤 것은 받아들이지 않을 필요가 있다.

    유용한 프레임워크 예제는 인프라스트럭처 코드와 사용자 인터페이스 코드를 별도로 패키지 그룹에 두는 식으로 LAYERED ARCHITECTURE를 적용해 도메인 계층을 물리적으로 자체 패키지 안으로 들어가게 하는 것이다.

    한편으로 티어 아키텍처(tiered architecture)는 모델 객체에 대한 구현을 잘게 나눠서 서로 흩어지게 한다. J2EE 일반적인 관행은 세션 빈과 엔티티 빈으로 나누는 것인데 업무 로직과 데이터의 분리로 인해 객체 모델의 응집력을 잃어버린다. 객체의 기본 개념은 데이터와 연산을 수행하는 로직을 캡슐화하는 것인데 이런 식으로 티어거 나눠진 구현은 단일 모델 요소의 구현을 구성하는 컴포넌트로 보일 수 있어서 하나의 개념적 ENTITY로 생각하려면 많은 노력이 들게 된다. 또 모델과 설계 간의 연결을 잃어버리게 된다.

    기술적 정교함이 주도하는 패키지 계획에는 두 가지 비용이 따른다.

    • 프레임워크의 분할 관례로 개념적 객체를 구현하는 요소가 서로 떨어져 있으면 코드에서 모델이 드러나지 않는다.
    • 다시 합칠 수 있는 만큼 분할되어 있지 않은데 프레임워크에서 그렇게 분할된 결과를 모조리 사용하면 도메인 개발자들은 모델을 의미 있는 조각으로 나누는 능력을 잃어버린다.

    기술 환경에서 필수적이거나 실질적으로 개발에 도움이 되는 최소한의 분할 규칙만 선택한다.

    여러 서버에 코드를 분산하는 것이 실제로 의도했던 바가 아니라면 동일한 객체는 아니더라도 하나의 개념적 객체를 구현하는 코드는 모두 같은 MODULE에 둬야 한다.

    고전 명제인 높은 응집도/낮은 결합도 관점에서 봐도 같은 결론에 도달할 수 있다. 업무 로직을 구현하는 객체와 데이터베이스 접근을 하는 객체 간의 연관관계는 넓은 범위에 존재하므로 결합도가 매우 높다고 볼 수 있다.

    프레임워크 설계, 회사 프로젝트의 관레로 도메인 객체 본연의 응집도가 불분명해져서 MODEL-DRIVEN DESIGN이 훼손될 수 있는 또 다른 함정도 있지만 결론은 같다. 필요한 패키지를 제한하거나 패키지가 많아지면 도메인 모델의 요구에 맞게 조정된 다른 패키지화 계획을 이용할 수 없게 된다.

    패키지화를 바탕으로 다른 코드로부터 도메인 계층을 분리하라.
    그렇게 할 수 없다면 도메인 개발자가 자신의 모델과 설계 의사결정을 지원하는 형태로 도메인 객체를 자유롭게 패키지화할 수 있게 하라.

    도메인 모델의 각 개념은 구현 요소에 반영돼야 한다. ENTITY와 VALUE OBJECT, 그리고 그러한 객체들 간의 연관관계는 일부 도메인 SERVICE와 조직화 MODULE과 함께 구현과 모델이 직접적으로 대응하는 지점이다. 구현은 모델 요소에 직접적이고 분명하게 매핑돼야 한다. 그렇지 않으면 코드를 정리한 다음 되돌아가 모델을 변경하거나 모델과 코드를 모두 변경해야 한다.

    네 가지 패턴은 객체 모델에 대한 기본 요소를 제공해주지만 MODEL-DRIVEN DESIGN이라고 해서 반드시 모든 걸 객체라는 틀에 맞춰야 한다는 의미는 아니다. 도구의 지원을 받는 다른 모델 패러다임도 있어서 실용적으로 따져봐야 한다. 이런 도구와 기법은 다른 대안이라기 보다는 결국 MODEL-DRIVEN DESIGN으로 가는 수단이다.

    모델링 패러다임

    현재는 객체지향 설계가 가장 지배적인 패러다임이다. 객체지향 패러다임이 우세한 이유는 여러 가지가 있는데 객체 본연의 특성, 상황에 따라, 널리 사용되고 있다는 이점에서 유래한다.

    객체 패러다임이 지배적인 이유

    객체 패러다임을 선택하는 이유는 객체 모델링이 복잡함과 단순함의 절묘한 조화를 이루고 있기 때문이다.

    객체 모델링의 개념은 단순하지만 중요한 도메인 지식을 포착할 만큼 풍부한 것으로 입증됐다. 또 객체 모델링은 처음부터 모델을 소프트웨어에서 표현하게 해주는 도구의 지원을 받고 있기도 하다.

    대부분의 신기술은 대중적인 객체지향 플랫폼에 통합될 수 있는 수단을 제공한다. 이런 수단은 통합을 더 용이하게 하고 다른 모델링 패러다임에 기반을 둔 하위 시스템과 융합될 수 있게 만들어 준다.

    객체지향 기술은 비교적 성숙해졌다. 공통 인프라스트럭처 요구사항은 실무에서 사용되는 기성 솔루션으로도 해결할 수 있다. 도구들이 주요 벤더 혹은 여러 벤더에 의해 만들어지거나 안정화된 오픈소스 프로젝트에서 만들어지고 있다.

    현재는 MODEL-DRIVEN DESIGN을 시도하는 대다수의 프로젝트에서 시스템 기반에 객체지향 기술을 사용하는 것이 현명하다.

    캡슐화된 행위에 대해 개별 묶음으로 모델링하기에 수월하지 않은 도메인도 있다. 지나치게 수학적인 도메인이나 객체지향 패러다임에 어울리지 않는 포괄적인 논리적 추론이 중심이 되는 도메인이 여기에 해당한다.

    객체 세계에서 객체가 아닌 것들

    도메인 모델이 반드시 객체 모델이어야 하는 건 아니다. 프롤로그(Prolog)로 구현된 MODEL-DRIVEN DESIGN의 경우는 모델이 논리적인 규칙과 사실로 구성돼 있다.

    프로젝트에서는 어떤 모델 패러다임도 적용할 수 있지만 다른 어떤 패러다임으로 훨씬 더 쉽게 표현할 수 있는 도메인 구성요소도 있다. 그러면 개발자들은 약간 부자연스러운 객체를 다른 일관된 모델이 더 잘 받아들일 수 있다. - 문제 도메인의 상당 부분이 다른 패러다임에서 훨씬 더 자연스럽게 표현된다면 패러다임을 완전히 교체해서 다른 구현 플랫폼을 선택하는 것이 맞을지도 모른다.

    개발자가 소프트웨어에 포함된 응집력 있는 모델을 분명하게 볼 수 없다면 이 같은 여러 패러다임이 혼재하는 시스템에서 MODEL-DRIVEN DESIGN의 필요성이 늘어나더라도 MODEL-DRIVEN DESIGN이 사라질 수 있다.

    패러다임이 혼재할 때 MODEL-DRIVEN DESIGN 고수하기

    룰 엔진은 객체지향 애플리케이션 개발 프로젝트에서 혼용되는 기술의 예로 알맞다. 객체지향 패러다임에는 규칙과 규칙 간의 상호작용을 나타내기 위한 구체적인 의미체계가 부족하다. 객체로 캡슐화하면 전체 시스템에 걸쳐 전역적인 규칙을 적용하기가 부자연스러워진다. 로직 패러다임은 객체의 강점과 약점을 보충하기에 적당하다.

    두 규칙을 다루는 동안에는 한 모델의 관점에서 사고를 지속하는 것이 중요하다. 두 가지 구현 패러다임에서 모두 작용할 수 있는 하나의 모델을 찾아야 한다. 그렇지 않으면 데이터와 규칙 간의 연관관계가 단절된다. 그리고 엔진상의 규칙은 도메인 모델의 규칙보다는 작은 프로그램에 훨씬 가까워진다.

    각 부분을 유지하는 효과적인 수단은 이질적인 모델을 통합할 수 있는 확고한 UBIQUITOUS LANGUAGE다. UBIQUITOUS LANGUAGE에 포함된 명칭을 두 환경에 일관되게 적용하고 활용한다면 두 환경 사이에 벌어진 틈을 메우는 데 도움될 것이다.

    여기서의 목표는 MODEL-DRIVEN DESIGN을 굳이 포기할 필요가 없으며 그걸 유지하는 노력이 가치 있음을 보여주는 데 있다.

    객체가 아닌 요소를 객체지향 시스템에 혼합하는 데는 다음의 4가지 법칙이 있다.

    • 구현 패러다임을 도메인에 억지로 맞추지 않는다.
    • 유비쿼터스 언어에 의지한다.
    • UML에 심취하지 않는다.
      • UML에 집착해서 그리기 쉬운 방향으로 모델을 왜곡한다. 다른 그리기 방식이나 간단한 문장으로 써놓는 편이 객체를 바라보는 특정 관점을 나타내는 도식 방법을 완곡하게 바꾸는 것보다 낫다.
    • 회의적이어야 한다.
      • 도구가 실제 역할을 하고 있는지 파악한다. 규칙이 필요하다고 해서 비싼 룰 엔진이 필요한 건 아니다.

    여러 패러다임이 혼재하는 데서 오는 부담을 안기 전에 지배적인 패러다임 내에서 선택 가능한 방안을 살펴보는 게 낫다.
    어떤 도메인 개념이 분명한 객체로 스스로 드러내지 않더라도 보통 해당 패러다임 내에서는 모델링 될 수 있기 때문이다.

    관계현 패러다임은 패러다임 혼합의 특수한 경우이다. 비객체 기술은 관계형 데이터베이스는 다른 구성요소에 비해 객체 모델과 더 직접적인 관련이 있는데, 관계형 데이터베이스가 객체를 구성하는 데이터의 영구 저장소 역할을 하기 때문이다.

    @jongfeel
    Copy link
    Owner Author

    jongfeel commented Dec 9, 2023

    ENTITY의 식별성에 대해 얘기하는 부분은 Java나 C#과 같은 객체지향 언어에서의 equals, hashCode를 통해 식별할 수 있는 걸 정의하라는 얘기로 들린다.

    @jongfeel
    Copy link
    Owner Author

    SERIVCE에서 다음 문구가 아주 마음에 든다.

    오늘날 흔히 하는 실수는 행위를 적절한 객체로 다듬는 것을 너무나도 쉽게 포기해서 점점 절차적 프로그래밍에 빠지는 것이다.
    

    사용하는 언어는 객체지향 언어라도 해도 객체지향적인 프로그래밍을 하는 것이 쉽지 않기 때문에
    많이 공감이 된다.

    @jongfeel
    Copy link
    Owner Author

    기민한 MODULE 부분에서
    초기에 결합도가 높아지면 리팩터링을 수행하기 어려워진다고 했는데 그 정도 수준에서 리팩터링을 할 생각을 못할 수도 있다고 본다.
    하지만 이후에 팩트를 짚어줬는데, 리팩터링을 하지 않으면 상황은 나빠지게 도고 고통을 참고 해야 문제 해결이 된다는 점에서
    고통스러운 경험을 해보지 않고는 알 수 있는 부분이 아니라는 점에서 기억을 해 두고 싶다.

    @jongfeel
    Copy link
    Owner Author

    jongfeel commented Jan 6, 2024

    패러다임이 혼재할 때 MODEL-DRIVEN DESIGN 고수하기 에서
    UML에 심취하지 않는다 부분이 있는데 심취하는 사람을 많이 보지 못했다는 측면
    또, 한국과 미국의 개발 환경 20년이 흐른 시점에서 책을 보고 있는 관점 등을 고려해야 할 것이다.

    UML로 표현하면 좋다는 입장이긴 하나, 나 역시도 꼭 UML을 고집해야 하는 건 아니다.
    오히려 이해하기 쉬운 그림을 그리고 설명을 더 명확하게 문장으로 적는 게 좋고
    또 멘토링 해줄 때도 그렇게 얘기하기 때문에
    책에 설명한 입장은 나도 매우 동의하는 부분이다.

    @jongfeel jongfeel closed this as completed Jan 6, 2024
    @github-project-automation github-project-automation bot moved this from In Progress to Done in 2023 jongfeel's study tasks Jan 6, 2024
    @jongfeel jongfeel added the 2024 label Jan 6, 2024
    # for free to join this conversation on GitHub. Already have an account? # to comment
    Labels
    2023 2024 Domain-Driven Design 도메인 주도 설계 - 소프트웨어의 복잡성을 다루는 지혜
    Projects
    No open projects
    Development

    No branches or pull requests

    1 participant