본문 바로가기

DDD/도메인 주도 설계 철저 입문

도메인 엔티티란? - 「도메인 주도 설계 철저 입문」 3장 (1)

 

 

 

본 포스트 시리즈는 「도메인 주도 설계 철저 입문」책을 요약한 내용입니다.

 

 

이전 글 보기

 


03장 생애주기를 갖는 객체 - 엔티티

 

(1) 엔티티란?

 

 

동일성이란

엔티티는 값 객체와는 다르게 "동일성"을 통해 식별이 가능하다. 동일성이란 영어로는 "identity"로, 사전을 찾아보면 다음과 같은 뜻이 있다. 

iden·tity
1.신원
2.독자성
3.유사성; 동질감

 

신원은 각각의 인간을 고유하게 식별한다는 의미가 있다. 사람은 이름, 키, 나이, 체중 등 다양한 속성을 가지지만 그 모든 것들이 바뀐다고 해서 한 사람이 다른 사람이 되는 것은 아니다. 여기서 이름, 키, 나이, 체중 등은 엔티티의 "속성"이고, 그 사람이 누구인지를 뜻하는 신원을 엔티티의 "동일성(identity)"라고 할 수 있다.

 

 

값 객체와 엔티티의 차이

값 객체는 속성으로 식별되는 반면, 엔티티는 동일성으로 식별된다. 속성으로 식별된다는 것은 무슨 뜻일까? 속성이 하나라도 달라지면 다른 존재가 되고, 속성이 모두 일치하면 완전히 같은 존재로 취급된다는 뜻이다. 한편, 동일성으로 식별된다는 것은 무슨 뜻일까? 속성과는 무관하게, 동일성이 다르면 다른 존재, 동일성이 같아야 같은 존재가 된다는 뜻이다. 

  값 객체 엔티티
식별 기준 속성 동일성
예시 사람의 이름, 키, 나이, 체중 등 사람의 신원

 

 

엔티티의 성질 3가지

  1. 가변이다.
  2. 속성이 같아도 구분할 수 있다.
  3. 동일성을 통해 구별된다.

 

엔티티는 가변이다

값 객체와는 달리 엔티티는 가변성을 갖는 객체이다. 시간이 지남에 따라 나이 같은 속성이 변하듯이, 엔티티의 속성도 변할 수 있다. 이름을 바꿀 수 있는 사용자 클래스 코드는 다음과 같다. 

class User {
    private String name;

    public User(String name) {
        changeName(name);
    }

    public void changeName(String name) {
        if (name == null) throw new IllegalArgumentException("Name cannot be null");
        if (name.length() < 3) throw new IllegalArgumentException("사용자명은 3글자 이상이어야 함");

        this.name = name;
    }
}

 

 

changeName 메서드를 통해 User 엔티티는 이름이라는 속성을 수정할 수 있다. 값 객체와 다른 점은 값 객체는 불변성을 갖기 때문에 다른 값을 대입해서(=교환해서) 수정했지만, 엔티티는 객체를 교환하는 대신 객체의 행동을 통해 수정한다.

 

 

불변일 수록 좋은 엔티티

하지만 엔티티가 가변이라고 해서 모든 속성을 가변으로 만들 필요는 없다. 가변 객체는 다루기가 까다롭기 때문에 가능하면 불변으로 만드는 것이 바람직하다.

 

 

속성이 같아도 구별할 수 있다.

값 객체는 속성이 같으면 완전히 같은 것으로 취급하지만, 엔티티는 속성이 같아도 다를 수 있다. 예를 들어 보자. 값 객체 FullName은 FirstName과 LastName으로 이루어져 있다. 이때 `{"John", "Smith"}`와 `{"John", "Smith"}` 는 동일한 값 객체이다. 하지만 세상에는 이름이 같은 사람이 많이 있어도 모두 같은 사람은 아니다. 사람은 속성만으로 구별하지는 않는다. 사람에 빗댈 수 있는 엔티티는 식별자(identity)를 통해 구별한다. 

 

User 객체에 식별자를 추가한 코드는 다음과 같다.

class User {
    private final UserId id;
    private String name;

    public User(UserId id, String name) {
        if (id == null) throw new IllegalArgumentException("ID cannot be null");
        if (name == null) throw new IllegalArgumentException("Name cannot be null");

        this.id = id;
        this.name = name;
    }
}
class UserId {
    private String value;

    public UserId(String value) {
        if (value == null) throw new IllegalArgumentException("Value cannot be null");

        this.value = value;
    }
}

 

이로써 이름이 완전히 동일한 User가 있다해도 이 UserId 비교를 통해 동일한 User인지 아닌지 구별할 수 있다. 그리고 인스턴스가 생성된 이후에 식별자가 변하지 않도록 userID는 final 키워드를 써서 정의했다.

 

 

엔티티의 동일성 비교

식별자만 추가해서는 의미가 없다. 식별자를 가지고 엔티티의 동일성을 비교하는 행동을 정의해주어야 한다. equals 메서드를 오버라이드한 코드는 다음과 같다.

class User {
    private final UserId id;
    private String name;

    public User(UserId id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        User other = (User) obj;
        return id.equals(other.id); // id만으로 비교
    }

    @Override
    public int hashCode() {
        return id.hashCode();
    }
}

 

값 객체는 모든 속성이 비교 대상이었지만, 엔티티는 식별자만으로 동등 비교를 한다.

 

 

요약

1. 엔티티란 식별자로 구별이 가능한 객체를 말한다. 속성으로 구별하는 값 객체와는 다르다.

2. 엔티티는 속성을 수정하는 행동을 가지고 있음으로써 속성이 변할 수 있다.

3. 엔티티는 변하지 않는 식별자를 가지고 있다.

4. 엔티티는 식별자를 비교하여 동등 비교를 한다.