그림1

 

God Class에 대한 용어 정리

god class는, 꼭 크기가 커야만(가지고 있는 필드가 많아야만) god class인 것은 아닙니다.

여러 layer에 걸쳐 사용되고 있다면, 또는 2개 이상의 책임을 가지고 있다면, 그 클래스를 사용(의존)하고 있는 클래스가 그 만큼 많다는 의미이고, 이는 곧 god class (또는 god class 유망주)입니다.

 

god class는 크게 2가지 유형으로 나누어 볼 수 있습니다.

  1. (왼쪽) domain 책임은 제대로 나눴으나 여러 layer에 걸쳐 사용되는 경우 = 유형 1
  2. (오른쪽) domain 책임을 제대로 나누지 못해 1개 이상의 책임을 가지는 경우 = 유형 2
  3. 유형 1 + 유형 2 = 유형3

이 글에서 주요하게 다루고자 하는 내용은 유형 1 God Class에 해당합니다.

 

DTO와 Domain Model을 분리해야 하는 이유

Q. 굳이 왜 DTO와 Domain Model을 분리해야 하나요?

A. Domain Model과 DTO를 분리하지 않는다면, 하나의 거대한 범용 class를 여러 layer에 걸쳐(presentation-domain-persistence) 사용하게 됩니다. (Domain Model의 역할과 DTO 역할을 모두 수행하는 god class)

 

```java

/**

 * 여러 layer에 걸쳐 사용되면서 단일 책임 원칙을 위배한 범용 클래스 예시

 */

class Card {

    /**

     * domain layer에서 필요로 하는 필드

     * presentation layer에서도 쓸거라, 요청/응답에 이 필드가 포함되지 않도록 해야 하니 @JsonIgnore

     * 여러 layer의 애너테이션이 한데 섞임

     */ 

    private final String name;

    @JsonIgnore

    private final String number; 

 

    /* presentation layer에서 필요로 하는 필드 (request) */

    @NotBlank

    private final String search1;

    private final String search2;

 

    /**

     * presentation layer에서 필요로 하는 필드 (response)

     * 이 필드는 Domain Model 'Company'에 들어가는게 맞지만, 응답으로 Card가 나가고 있으니 추가

     * A 컨트롤러 통해 반환 할 때는 필드가 채워져있고, B 컨트롤러 통해 반환 할 때는 null (민감 정보 제외)

     */ 

    @JsonProperty("cardCompanyAddress")

    private final String companyAddress;


    /**

     * persistence layer에서 필요로 하는 필드

     * Card와 Member는 별도의 Domain Model임에도

     * 모종의 이유(성능) 때문에 query에서 member와 card 정보를 한 번에 가져와야 한다면

     * member(의 일부)를 class Card에 추가해버리는 경우가 잦다.

     * (약한 참조를 위한 key를 가지고 있는 정상적인 경우와는 다르므로 주의. 약한 참조 자체는 정상)

     */

    private final String memberId;

    private final String memberName;

    private final String memberAge;

 

    /* method 생략 */

}

 

class Member {

    private final String id;

    private final String name;

    private final String age;

    private final String email;

 

    /* method 생략 */

}

```

  • 이렇게 구성된 클래스는 생각보다 흔히 보입니다.
  • 예제는 조금 극단적인 케이스이기는 하지만, 어떤 필드가 어느 클래스에 위치해야 하는지가 조금만 덜 명확한 상황이어도 잘못된 길로 빠지기 쉽습니다. (클래스 분리하는 것 보다 필드 추가가 간단합니다. 강력한 유혹.)
  • persistence에서 필요로 하는 필드의 경우
    • 주로 n:1 관계에서, n에 해당하는 클래스에 이런 패턴이 많이 보입니다. (예제의 Card 처럼)
    • 반면 1에 해당하는 클래스는 List n 형태의 필드를 가지도록 구성하는 경우가 잦아 이런 문제가 자주 보이지는 않습니다.

 

Q. god class는 왜 나쁜가요? 그냥 위와 같이 layer에 걸쳐 범용적으로 쓸 수 있는 클래스 하나를 공통으로 쓰는게 더 간단한거 아닌가요?

특정 상황에서만 값이 존재하는 필드

  • persistence layer에서 필요로 하는 필드에 대하여, member 정보를 가져오지 않고 card 정보만 가져오는 쿼리가 별도로 존재한다면, Card라는 객체는 어떤 경우에는 member* 필드까지 다 초기화 되고, 어떤 경우에는 member*를 제외한 나머지 필드만 초기화됩니다.
  • 시스템에 항상 완벽하게 초기화 된 상태의 객체만 존재하도록 하는 것은 현실적으로 불가능합니다.
    • 그러나 [use-case에 따라 일부 필드가 영영 초기화되지 않는 불완전한 상태의 객체]와 [초기화 되기 전에 잠시 불완전한 상태를 가지며 초기화 되면 완전해지는 객체] 그리고 [특정 필드에 값이 없는 경우가 개념적으로 타당한 객체]는 구분해야 합니다.

 

초기화 되는 시점과 사용되는 시점이 필드군 마다 제각각이라 유지보수 어려움

  • 하나의 클래스를 다용도로 쓰다 보니 ➞ 도대체 이 필드는 언제 초기화 돼서 어디에 쓰이는 것인가?를 파악하기가 곤란하고 ➞ 수정 시 영향 범위 파악과 추적이 어렵다 ➞ 유지보수 비용 증가로 이어집니다.
    • 전 layer에 걸쳐 사용되니, 수정 할 때 전 layer를 모두 고려해야 해서 집중력이 분산됩니다. layering의 큰 이점 중 하나는 관심 분리를 통한 작업 대상 layer로의 집중인데, 이러한 이점을 상쇄해버립니다.
    • TC라도 있으면 다행이지만... 없다면 이렇게 커져버린 god class를 분리하는 것 또한 쉬운 일이 아닙니다.
  • 또한 이렇게 하나의 객체를 부분 부분 나눠서 조금씩 여러번 초기화 하는 것은 final을 제거하고 싶은 충동을 유발합니다.
    • 여전히 final을 쓸 수 있지만, setter로 상태 바꿔 필요한 필드만 추가로 초기화하는게 뭔가 더 깔끔해 보이기 때문입니다.
    • 그래서 보통 이런 god class 들은 대부분의 필드가 final이 아닌데, 이는 웬만하면 불변으로 다루자는 컨셉을 따를 수 없게 만듭니다.

 

유형 1 god class는 (유형 1 + 유형 2 = 유형3) god class를 유발

  • 최초에는 적어도 도메인에서 만큼은 2가지 책임을 가지지 않도록 그럭저럭 잘 나뉘어진 상태에서 시작한 class라고 하더라도, 전 layer에 걸쳐 사용되고 있다면 필드가 추가되며 점차 다른 도메인 책임 까지 애매하게 넘보게 됩니다.
    • e.g., "member.name이 필요한데 card.memberName이 있으니 Member 말고 이걸 쓰자"
    • 이렇게 점차 필드를 땜질하듯 하나 씩 추가하게 되면서 수직, 수평으로 확장(악순환)이 일어납니다.
    • 책임이 모호한 컴포넌트 들로 구성된 시스템은 개발자의 혼란을 가중시킵니다. "이 기능은 도대체 어디에 위치시켜야 하는가?"

 

anemic domain model을 유발

  • 로직을 모두 Service에 넣고, Domain Model은 메서드 없이 필드만 가지고 있는 구조
  • Domain Model에 대해서 - anemic domain model 참조

 

여러 layer의 애너테이션이 한데 섞임

  • 한 눈에 이해하기 어렵습니다

 

해결책 - 유형 1 god class를 피하기 위해서는 DTO와 Domain Model을 분리해야 합니다.

  • 유지보수가 쉬운 시스템의 기본은, 수정이 발생했을 때 필요한 부분만 수정 해도 문제가 해결되는 시스템입니다. 즉, 불필요한 수정이 발생하지 않아야 합니다.
  • 필요한 부분만 수정 해도 문제가 해결되는 시스템이 되려면, 개발에 사용되는 구성요소(객체지향에서는 Class)들이 적절한 책임과 단위로 나뉘어 있어야 합니다. ➞ 수직 방향의 책임 뿐만 아니라, 수평 방향의 책임도 분리해야 합니다.
  • 객체 지향 5대 원칙 : SOLID - 단일 책임 원칙 등이 이를 뒷받침 합니다.
  • 클린 코드의 저자 Robert C. Martin, 리팩터링과 아키텍쳐의 대가 Martin Fowler 도 책임에 따라 작게 쪼개는 설계, layering 기반 관심 분리의 중요성에 대해 얘기하고 있습니다.

 

변경 대상 클래스 숫자가 늘어나면 나쁜 설계 아닌가요?
  • 1개의 god class를 3개의 DTO와 Domain Model로 쪼갰다고 가정합시다. 이 때 3개 클래스 모두 변경을 유발하는 하나의 수정 케이스를 떠올리며, 변경 대상 클래스가 1개에서 3개로 늘어나니 이 것은 유지보수 포인트가 늘어나는 나쁜 설계가 아닌가 하는 생각이 들 수 있습니다. 그러나 중요한 것은, 클래스의 물리적인 개수가 아닙니다.
  • 지향점은 수정 시 변경 대상 클래스의 수가 적어지는 설계가 아니라, 필요한 부분만 집중해서 변경할 수 있게끔 변경의 범위를 제한해주는 설계, 책임 이외의 사유로 도메인 모델을 망치지 않는 설계 입니다.
  • [리팩터링 2판] 3장 Bad Smells in Code - 뒤엉킨 변경, 산탄총 수술 참고

 

그렇다면 항상 DTO와 Domain Model을 분리하는 것이 좋은 설계인가요?

앞서 얘기한 것 처럼 DTO와 Domain Model을 분리하는 것은 유형 1 god class를 예방하는데는 도움이 됩니다. 하지만 ‘Good design is all about trade-offs’ 입니다. 실용주의적 관점에서도 생각해 볼 필요가 있습니다.

 

관점 1 - 무엇이 더 일반적인가?

  • 꽤나 복잡한 애플리케이션 시스템이라면 일반적으로 4가지 케이스가 모두 존재할 것입니다.
    • presentaion DTO와 domain model을 분리해야만 하는 케이스
    • presentaion DTO와 domain model을 분리하지 않아도 되는 케이스
    • persistence DTO와 domain model을 분리해야만 하는 케이스
    • persistence DTO와 domain model을 분리하지 않아도 되는 케이스
  • 경험적으로 ‘무엇이 더 일반적인가?’를 생각해보는 것이 도움이 됩니다.
    • presentation DTO와 domain model은 분리해야 하는 경우가 많습니다.
      • 분리하지 않아도 되는 케이스의 대부분은, 나중에 분리가 필요해지는 케이스이므로 결과적으로는 분리하게 됩니다.
      • 표현과 도메인 모델은 불일치 가능성이 높습니다.
    • persistence DTO와 domain model은 굳이 분리하지 않아도 되는 경우가 많습니다.
      • 대부분은 쿼리 join 정도로도, Domain Model에 변형을 가하지 않고 잘 매핑 하도록 만들 수 있습니다.
      • 이는 즉 persistence layer의 요구사항 때문에 Domain Model을 망치게 될 가능성이 적다는 것입니다.
      • 분리해야만 하는 경우 예시)

 

관점 2 - god class 유발 가능성

  • 사실, 분리가 필요해지는 순간 그 즉시 DTO와 Domain Model을 분리 할 수 있다면, DTO와 Domain Model의 구조가 일치하는 경우 굳이 미리 분리 할 필요는 없을지도 모릅니다.
  • 그러나 현실적으로 이게 가능한 경우는 많지 않습니다. (모두가 분리가 필요한 시점에 잘 분리했다면 god class는 존재하지 않았을 겁니다)
  • 우리는 혼자 개발하는 것이 아니라 다양한 동료와 함께 합니다. 구성원들 모두가 객체지향과 layering의 중요성을 인지하고 있으며 언제든 자신있게 아키텍쳐를 개선하고 수정 할 수 있는 사람들이라고 가정하는 것은 너무 낙관적입니다. 특히, 새로 팀에 합류한 인원은 보통은 구조가 잡혀 있는 대로 따라서 개발하게 됩니다.
  • 코드리뷰에서 이런 부분을 보완 할 수 있겠습니다만, 아키텍쳐가 익숙한 기존 인원이라고 하더라도 매번 수정이 발생할 때 마다 이런 부분에 주의를 기울이는 것 보다는, 차라리 처음 코드를 작성하는 사람이 DTO, Domain Model를 분리해서 구조를 잡아두는 것이 DTO(또는 Domain Model)의 오남용(e.g., god class) 예방 차원에서는 더 나을 수도 있습니다.

 

이러한 관점들을 고려하여 느슨한 원칙을 세운다면 이런 식이 됩니다.

  • presentation DTO와 domain model은 항상 분리합니다.
    • 어차피 분리해야만 하는 케이스가 대부분 입니다. 일부 꼭 분리하지 않아도 되는 케이스를 분리하지 않음으로써 얻을 수 있는 이익 보다 설계 구조의 일관성 유지, Domain Model을 망칠 가능성에 대한 차단이 더 중요해 보입니다.
    • 어떻게 표현할지는 비즈니스와 완전히 분리된 또 다른 영역으로 구분해서 생각하는 것이 좋습니다.
  • persistence DTO와 domain model은 필요한 경우에만 분리합니다.
    • 분리해야만 하는 케이스가 드물기 때문에, 모두 분리하도록 강제한다면 이를 불편하고 불필요한 작업이라 느끼게 될 가능성이 큽니다. ( 마틴파울러 - 'presentation DTO 조차 표현과 도메인 간의 불일치가 있을 때만 만들 가치가 있다' )
    • 하지만 구성원들 모두가 ‘필요한 경우 즉시 분리한다’는 원칙을 이해하고 있어야 하고, 코드리뷰 시 이 것이 잘 지켜지고 있는지 신경써서 검증해야 합니다.
  • external DTO와 domain model도 항상 분리합니다.
    • 외부 API와의 변수명 불일치(용어 불일치, 축약어 사용, snake_case 등) 및 타입 불일치 때문에 어차피 분리해야만 하는 케이스가 대부분입니다.
    • 분리 하지 않아도 되는 케이스여도, presentation DTO와 같은 사유로 일관적으로 분리해주는 것이 좋아보입니다.

 

어떻게 분리하나요?

  • DTO가 필요한 이유는 Domain Model을 Domain Model 답게 유지하기 위함 이므로, 목적에 부합하도록 만듭니다.
  • 보통은 DTO를 layer에 따라 패키지로 묶어서 관리하는 것이 편해보입니다.
    • 현재 작업 중인 클래스가 어떤 layer의 DTO인지, Domain Model 인지 파악하기 쉽습니다.
    • 현실적인 사유로 DTO를 분리하지 않는다고 하더라도, 패키지 만큼은 꼭 나누는 것이 좋아보입니다.
  • layer 분리와 그에 따른 DTO 분리는 필요하지만, 예를 들어 presentation DTO를 반드시 presentation layer에서만 사용 할 수 있도록 제한하는 것은 부작용이 있을 수 있습니다.

 

package 관리

[마틴파울러] Layering 관련 글 모음  - '도메인을 최우선으로 나누고...' 부분 참고

* 도메인을 너무 작게 나누면 두개 이상의 도메인에서 공통으로 사용하는 클래스를 어디에 위치시킬지 애매해질 수 있습니다.

 

```

{domain}
  ㄴ model
  ㄴ dto
    ㄴ presentation
    ㄴ persistence  // 필요한 경우에만 만듭니다
    ㄴ external
{domain2}
  ㄴ ...

```

하지만 이미 시스템이 위와 같은 구조로 되어 있지 않은 경우가 많은데, 그렇다면 무리해서 바꿀 필요는 없어보입니다.

 

절충안

```

model
  ㄴ {domain}
    ㄴ presentationdto
    ㄴ persistencedto
    ㄴ externaldto
  ㄴ {domain2}
    ㄴ ...

```

 

Business 외부와 상호작용 할 때 뿐만 아니라, Business 내부에서도 DTO를 사용하는 것은 어떤가요?

  • https://martinfowler.com/bliki/LocalDTO.html
    • DTO <> Model Mapping 작업에 드는 비용 때문에 불필요한 DTO는 권장하지 않습니다.
    • 마틴파울러는 DTO를 [1. layer의 경계를 넘어 데이터를 전달하기 위한 데이터 뭉치, 2. Domain Model과 Mapping하게 되는 대상]으로 바라보고 있습니다.
  • 하지만 메서드 파라미터 개수가 너무 많아져서(예를 들어, 8개) 메서드 시그니처가 너무 지저분한 경우 이를 묶어줄 필요가 있습니다. ([리팩터링 2판] 3장 Bad Smells in Code - 3.10 데이터 뭉치)
  • 여기서는 용어의 정리가 필요합니다. 이렇게 business layer 내에서 같이 다니는 데이터를 묶어 준 것은, DTO가 아니라 VO라고 생각하는 것이 더 적합합니다.