본문 바로가기
아키텍처

DDD - Entity의 개념

by pius712 2024. 8. 31.

 

읽기에 앞서,,

Entity 란 무엇인가? 라는 “정의 자체가 중요한 것이 아니다.”

요구사항을 모델링할 때, 요구사항의 특정 부분을 개념화하고 분류하는 것이 목적이고, Entity 는 이 모델링을 하기 위한 도구로서 존재한다.

우리의 도메인을 개발할 때, 이를 어떻게 식별해내고 사용할 수 있는지 염두하면서 이해하자.

정의

 

Entity 의 핵심은 식별가능성 과 변화가능성 이다.

Entity 는 시스템 내에서 고유한 식별성을 가지며, 시간에 지남에 따라 자신의 상태가 변화한다.

이는 하나의 서버내에서 국한되는 식별성이 아니라, 여러 서버와 여러 데이터 베이스에 걸쳐서도 식별이 가능해야한다.

예를들어, 유저A 가 있다고 할 때 이 유저의 ID 는 메시징 큐를 통해 전달이 되든, api 를 통해 전달이 되든 시스템 내에서 유저A 로 식별된다.

그리고, 유저가 자신의 이름을 바꾼다고 해도, 시스템 내에서는 같은 유저임을 보장한다.

class User(
	val id:Long,
	var name:String,
) {
	fun changeName(newName:String) { name = newName }
}

 

Entity 의 포착

DDD 를 배우고 적용하려다보면, 직면하는 것 중에 하나가 아래와 같은 질문이다.

그래서 이건 Entity 로 다뤄야해? 아니면 그냥 데이터를 읽어서 만들어내야해?

결국 Entity 가 무엇인지 정의를 안다고 해서, 바로 Entity 를 설계하고 만들 수 없다는 것이다.

그렇다면, 어떻게 우리는 Entity 를 포착할 수 있을까?

단순히 기획자의 단어를 주목해서

  • 명사 → 클래스
  • 동사 → 메서드

이런 방식으로 도메인 모델을 추출하면 될까?

이러한 방식으로 모델을 추출하려고 하면, 실제 도메인 모델을 잘 추출하기 어려워진다.

요구사항 분석과 식별 가능성 포착하기

우선적으로 당연하게도 요구사항을 분석을 해야한다.

그리고 이러한 분석을 이터레이션을 돌면서, 구체화해 나가는 것이 중요하다.

그 중 엔티티를 포착하기 위해서는 앞서 설명했듯 식별 가능성 에 집중해야한다.

예를들어, 아래의 요구사항이 있다고 가정해보자.

  • 유저는 캐시를 가지고 있다.
  • 상품권은 캐시로 사용할 수 있다.
  • 상품권은 여러번 선물받을 수 있다.

이 때, 상품권을 우리는 entity 로 식별해야할까? 캐시를 entity 로 만들고, 선물받은 상품권 금액을 캐시에 더하기만 하면 될까?

모호한 것 같다. 이런 경우, 우리는 도메인 전문가 혹은 기획자에게 질문을 해야한다.

  • 받은 상품권을 검색할 수 있을까요?
  • A 상품권과 B 상품권은 금액이 같다면 동일한가요? 유효기간은 없나요?

위와 같은 질문은 결국 상품권은 고유 식별성을 가지나요? 라고 이해할 수 있다.

식별하기 위한 추가 속성 추가하기

상품권을 엔티티로 만들기로 했으면, 추가적으로 식별을 위한 속성들이 필요하다.

단순히 아래와 같은 엔티티는 식별은 가능하나 당장에 검색할 방법이 없다.

→ 검색이 가능해야 상품권을 사용하든 말든 할 것이다.

class Gift(val id:Long) 

추가 식별가능한 속성 포착

  • 상품권은 구매시 선물받을 사람이 정해진다
  • 상품권은 특정 상품권이라는 제품을 구매하는 것이고, 어떤 제품으로부터 생성된 것인지 알 수 있어야한다.
class Gift(
	val id: Long,
	val fromUserId: Long,
	val toUserId: Long,
	val productId: Long,	
)

그 외의 것들에 대해서는 우선 추후에 고민하자.

예를들어, 선물은 어떻게 결제되는지 ? 선물은 어떤 결제 수단을 통해 결제되는지?

다른 컨텍스트에 속하는 이런 고민은 엔티티 설계를 어렵게 만든다.

행위 추가하기

필수적인 속성을 식별해낸뒤에는 어떠한 행위를 찾아야한다.

이때도, 요구사항 분석을 통해 행위를 도출할 수 있다.

예를들어, 아래와 같은 요구사항이 있다고 가정하자.

  • 특정 상품을 상품권을 통해 구매시, 구매금액만큼 상품권 사용가능 금액을 감소시킨다.
class Gift(
	var totalAmount:Long,
	var availableAmount:Long
) {

	fun use(usedAmount:Long) {
		if(availableAmount < usedAmount) {
			throw ExeedAmountException()
		}
		availableAmount -= usedAmount
	}
}

이 때, 값을 setter 를 노출시키는 방법 보다는 행위를 메서드로서 제공해주는 것이 좋다.

그에 대한 이유는 아래와 같다.

  • 불변식을 위배할 수 있다.
  • 의도를 명확히하기 어렵다.

만약 하나의 필드만 수정하는 경우라면, setter 를 사용할 수도 있으나, 이 경우에도 의도를 드러낼 수 있는 인터페이스를 사용하는 것이 좋다.

예를들어, 상품권은 정지될 수 있다 라는 요구사항이 추가되었다고 가정해보자.

// as-is
class Gift(
	val active: Boolean 
) 

class GiftService {

	fun deactivate(userId:Long, giftId:Long) {
		// 생략
		val gift = repository.findByIdOrNull(giftId) ?: throw RuntimeException("")
		gift.active = false
	}
}

// to-be
// as-is
class Gift(
	val active: Boolean 
) {
	fun deactivate() {
		active = false
	}
} 

class GiftService {

	fun deactivate(userId:Long, giftId:Long) {
		// 생략
		val gift = repository.findByIdOrNull(giftId) ?: throw RuntimeException("")
		gift.deactivate()
	}
}

당장 이 예제에는 차이가 없어보이지만, 이후에 아래의 정책이 추가된다고 가정해보자.

  • 남은 금액이 있는 경우 비활성화 할 수 없다

그 경우 필드에 직접 접근하는 경우,

  • 해당 코드를 찾아서 모두 수정해야함
  • 이것이 지켜지지 않는 경우 불변식을 위배할 수 있게 된다.
// 요구사항 반영
class Gift(
	val active: Boolean 
) {
	
	fun deactivate() {
		if(availableAmount > 0){
			throw BadRequestException()
		}
		active = false
	}
} 

Entity 구현

생성

Entity 를 생성할 때는 충분한 속성을 바탕으로 생성해야한다.

  • database 의 식별자 할당을 사용하지 않는다면 생성 시점에 식별자가 있어야한다.
  • 해당 Entity 를 특정 속성으로 쿼리할 때, 해당 속성은 생성시점에 존재해야한다.

그리고, 단일 엔티티 생성시에도 불변식(invariant)를 고려해야할 수 있다.

예를들어, 유저를 생성할 때 이름, 생년월일이 항상 있어야한다는 불변식을 생각해보자.

  • 생성자에 이 두개가 모두 있어야한다.
  • 두 값이 모두 null 이면 안된다.
// typescript
class User {
	private name :string;
	private birthDate :Date;

  constructor(name:string, birthDate: Date) {
	  setName(name)
	  setBirtDate(birtDate)
  }
  
  protected setName(name:string) {
    if(name == null) throw Error()
    this.name = name
  }
  
   
  protected setBirthDate(birthDate:string) {
    if(birthDate == null) throw Error()
    this.birthDate = birthDate
  }
}

유효성 검사

모델의 유효성 검사는 크게 3가지로 분류될 수 있다.

  • 개별 속성의 유효성
  • 객체 자체의 유효성
  • 객체 컴포지션의 유효성

개별 속성의 유효성 검사

개별 속성의 유효성은 일반적인 유효성과는 달리, 계약에 의한 설계에서의 assertion 으로 보는 것이 좋다.

개별 속성의 유효성은 가드(guard) 를 통해서 획득할 수 있다.

→ 이를 방어적 프로그래밍이라고도 하며, 속성의 유효성 검사를 넘어서는 너무 많은 로직을 수행해서는 안된다.

class User(
	val email: String
) {

	fun changeEmail(newEmail:String) {
        // gurad
        if(newEmail.isEmptyOrBlank()){
            throw BadRequestException()
        }

        // gurad			
        if(!정규표현식검사.isEmail(newEmail)) {
          throw BadRequestException()
        }
        email = newEmail
	}
}

객체 자체의 유효성 검사

개별 속성이 유효하다고해서, 객체 전체의 상태가 유효한 것은 아니다.

예를들어, 유저의 국적과 폰번호에 대해 생각해보자.

한국 국적의 사람은 한국 폰번호를 소유해야한다는 제약사항이 있을때, 이는 개별 속성으로 유효성을 검증할 수 없다.

일반적으로 유효성 검사는 도메인 모델 자체가 수행하지 않고 별도의 클래스로 분리하는 것이 좋다.

  • 도메인 모델의 변경주기와 유효성 검사의 변경 주기는 다르다.
  • 도메인 모델의 책임이 너무 커진다. 도메인은 자신의 행위에 대한 책임만 하면 된다.
  • 유효성 검증 객체는 같은 패키지에 위치해야한다.
// 1. 합성하는 방법
class UserValidator(
	val user:User
) {
  fun validate() {
    // 검증 로직
	}
}

// 2. 인자로 받는 방법
class UserValidator {

  fun validate(user:User) {
    // 검증 로직
  }
}

이 때, 도메인 모델을 사용하는 client 가 validator 를 호출해야할까? 아니면, 도메인 모델이 이를 사용해야할까?

이것은 나름의 장단점이 있는 것 같다.

// 1번 validator 사용
class User {
	fun validate(){
		UserValidator(this).validate()
	}
}

// 2번 validator 사용
class UserService {
	fun change() {
		val user = userRepository.findByIdOrNull(userId) ?: throw NotFoundException()
		user.change()
		userValidator.validate(user)
	}
}

객체 컴포지션의 유효성 검사

여러 객체의 묶음에 대해 유효성을 검사할 때는 도메인 서비스를 통해서, 유효성 검사를 하는 것이 좋을 수도 있다.

 

Entity 의 변경 추적

Entity 자체는 고유 식별성을 가지며, 상태가 지속적으로 변화한다.

그렇기 때문에 Entity 자체가 자신의 변경 이력을 추적할 필요는 없다.

다만, 개발을 하다보면 여러가지 이유로 Entity 의 상태 변경을 추적해야하는 경우가 생긴다.

예를들어 아래와 같은 경우, 변경을 추적할 수 없다면 곤란한 상황이 생길 수 있다.

  • 데이터 분석
  • CS 대응

물론, 모든 엔티티가 변경내역을 추적할 수 있어야하는 것은 아니다. 그렇기 때문에 이런 경우 팀내의 도메인 전문가, 기획자, 데이터 엔지니어와의 회의를 통해서 결정해야할 것이다.

그렇다면, 어떤 방식으로 변경을 추적할 수 있을까?

여러가지 방법이 있을 것인데, 대표적으로는 두가지 방식으로 구현될 수 있다.

  • 이벤트 소싱
    1. 이벤트 publish
    2. 이벤트 consume
    3. 이벤트 저장소에 저장
  • 스냅샷 테이블
    • 변경시 해당 entity 의 상태를 저장함.
  • 변경 히스토리 테이블
    • 변경된 필드의 이전 데이터와 이후 데이터를 저장함

'아키텍처' 카테고리의 다른 글

DDD - Entity 인가 아닌가?  (0) 2024.09.04
api gateway 란?  (0) 2023.12.30
설계 원칙  (0) 2023.09.03
Yagni (You aren’t gonna need it) (feat. 클린 아키텍처)  (0) 2023.07.02