DDD 3. 엔티티

    요약

    • 엔티티는 식별키를 기준으로 동일성이 판단되는 가변객체다. 
      • 특정 속성이 변하더라도 같은 객체로 판단해야하는 객체를 표현할 때 사용된다. (사람이 나이가 먹어도 같은 사람이다) 
    • 엔티티는 가변객체지만, 프로그램의 안정성을 위해서 필요한 부분만 가변으로 바꾸는 것이 좋다. 즉, 엔티티도 가능한 불변 객체로 남겨두는 것이 낫다.
    • 같은 엔티티인지 확인하기 위해 equals()는 식별키를 비교하도록 구현해야한다.
    • 도메인 객체를 엔티티로 사용하는 기준은 생애주기를 가지는지 여부가 크다
    • 환경에 따라 엔티티도 될 수 있고, 값객체도 될 수 있다. 
    • 도메인 객체를 선언했을 때 장점
      • 도메인 객체 자체가 도메인 모델을 설명해 줌. (도메인 규칙을 도메인 클래스만 보고 이해할 수 있게 됨.) 
      • 도메인 모델의 변경점을 도메인 객체레 손쉽게 반영할 수 있게 됨. (아닌 경우, 변경 지점이 넓어짐) 

     


    3.1 엔티티

    • 엔티티는 속성이 아니라 식별자를 기준으로 동일성을 판단하는 도메인 객체다.
      • 사람의 이름이 바뀌어도 그 사람이 그 사람이라는 사실을 변하지 않는 것과 같음.
      • 사람의 이름이 같아도 같은 사람은 아니다. (동명이인)
    • 엔티티는 가변 속성을 가진다. (값객체는 불변함)
      • 사람의 나이가 변해도 그 사람은 그 사람이다. 

    위와 같은 특성을 가지는 도메인 객체들은 엔티티라고 할 수 있다. 

     


    3.2 엔티티의 성질

    • 가변 객체다. 
    • 속성이 같아도 서로 다른 객체일 수 있다.
    • 식별키를 통해서 구별한다.

    엔티티의 성질은 위와 같다. 각 성질에 대해서 조금 더 살펴보면 다음과 같다.

     

    가변 객체

    @RequiredArgsConstructor
    static class User {
        private int age;
        private final String name;
        
        public void changeAge(int age) {
            this.age = age;
        }
    }
    • 엔티티는 가변 객체다. 사람은 시간이 지날 때 마다 나이를 먹는다. 마찬가지로 사람이라는 엔티티의 '나이' 속성도 가변속성이 될 수 있다. 
    • 값객체에서는 Setter를 제공해주지 않아서 값을 바꿀 수 없었지만, 엔티티는 Setter를 제공해서 값을 바꿀 수 있도록 한다. 

    위에서 사람이 나이를 먹어가는 것을 표현할 수 있도록 changeAge() 라는 Setter 계열의 메서드를 제공한다. 이를 통해 User 클래스는 가변 객체가 된다. 

     

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

    // 엔티티로 사용 -> 식별키 추가 필요함. 
    @RequiredArgsConstructor
    static class User {
        // 식별키 추가.
        private final Long userId;
        private int age;
        private final String name;
    
        public void changeAge(int age) {
            this.age = age;
        }
    }
    • 값객체는 가진 필드가 모두 같으면 같은 것으로 판단했다. 그러나 엔티티는 식별키를 기준으로 동일성을 비교하기 때문에 필드가 같은 값을 가지는 것을 아무런 의미가 없다. 
    • 엔티티의 동일성을 비교할 수 있도록 '식별키'인 userId를 추가했다. 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return userId.equals(user.userId); // 식별키만으로 같은 엔티티인지 비교.
    }

    User 엔티티가 서로 같은 엔티티인지 확인하기 위해서 equals()를 호출할 때는 같은 userId(식별키)를 가졌는지를 살펴봐야한다. 

     


    3.3 엔티티의 판단 기준 - 생애주기와 연속성

    값객체 / 엔티티를 이용해서 도메인 객체를 나타낼 수 있다. 그렇다면 엔티티와 값객체를 판단하는 기준은 무엇이 있을까?

    • 엔티티는 생애주기를 가지는 객체다.
      • 예를 들어 회원은 회원가입을 했다가(생성) 탈퇴(소멸)하는 객체다. 즉, 생성과 소멸의 생애주기를 가진다.

    생명주기를 가지는 도메인 객체를 엔티티로 판단하면 된다. 예를 들어 회원가입할 때, 우리 사이트 입장에서 '회원 이메일'은 하나의 값처럼 느끼게 될 것이다. 왜냐하면 우리 사이트에서 생성되고 삭제되는 것이 아니기 때문이다. 

     


    3.4 값 객체도 되고 엔티티도 될 수 있는 모델

    모든 엔티티는 상황에 따라 값객체도 될 수 있고, 엔티티도 될 수 있다. 아래 두 가지 상황을 고려해보자. 

    • 자동차의 타이어 (값객체로 사용)
      • 자동차의 타이어는 일반적으로 갈아끼우는 대상이다. 서로 바꿔써도 문제가 없기 때문에 값객체로 사용할 수 있다.
    • 타이어 공장의 타이어 (엔티티로 사용)
      • 공장에서 생성된 타이어는 일련번호를 가지고 있고, 일련번호를 통해 객체를 식별하는 것이 중요하다. 타이어 공장에서 타이어를 만들고(생성), 폐기하면(소멸) 일종의 생애주기를 가진다고 볼 수 있다. 

     

     


    3.5 도메인 객체를 정의할 때 장점

    엔티티와 값객체는 각각 도메인 모델을 나타내는 도메인 객체다. 도메인 객체를 정의하면 어떤 장점이 있을까? 

    • 도메인 모델 파악에 도움을 줄 수 있음.
    • 도메인 모델에 변경점이 있을 때 도메인 객체에 반영하기 쉬움. 

    각각에 대해서 좀 더 살펴보자.

     

    도메인 파악에 도움됨.

    static class User {
        private final Long userId;
        private int age;
        private final String name;
    
        public User(Long userId, int age, String name) {
            if (name.length() < 3) {
                throw new IllegalArgumentException("name은 3글자보다 커야 합니다.");
            }
            
            ...
        }
    ...
    }

    위 코드를 바탕으로 도메인 파악에 도움이 된다는 내용을 이해해본다. User 도메인 클래스가 있다.

    • User 생성자에 name.length() < 3으로 제약조건이 걸려있다. 개발자는 이 코드를 읽으며, User라는 사용자 도메인 모델의 규칙에 대해서 이해할 수 있게 된다. 

    만약 User 도메인의 모든 규칙이 User 클래스의 메서드 상에 표현되어있다면, 개발자는 굳이 도메인의 규칙을 파악하기 위해서 문서를 읽을 필요가 없다. 

    static class User {
        private final Long userId;
        private int age;
        private final String name;
    
        public User(Long userId, int age, String name) {
            this.userId = userId;
            this.age = age;
            this.name = name;
        }
    ...
    }

    반면 제약조건이 표현되어있지 않다면, 개발자는 도메인 클래스로부터 도메인 모델에 대한 어떠한 정보도 얻을 수 없게 된다. 이 때는 도메인 모델의 규칙을 이해하기 위해 개발자는 문서를 읽어야 한다.

     

    도메인 모델에 변경점이 있을 때 반영하기 쉬움.

    잘 만들어진 도메인 클래스에는 해당 도메인에 필요한 규칙, 메서드들이 함께 선언되어있다. 즉, 코드가 응집되어있다. 이런 도메인 클래스들은 도메인의 변경이 발생했을 때, 변경 지점을 쉽게 파악할 수 있고 변경범위도 적어서 도메인 모델의 변경점을 반영하기 쉽다. 

    public User(Long userId, int age, String name) {
        if (name.length() < 3) {
            throw new IllegalArgumentException("name은 3글자보다 커야 합니다.");
        }
    
        this.userId = userId;
        this.age = age;
        this.name = name;
    }
    • name이 3글자보다 커야하던 것이 6글자보다 커야한다로 도메인 모델의 규칙이 바뀌었다고 가정해보자. 위의 예시처럼 도메인 객체에 코드가 응집되어 있다면 이 규칙을 반영하기 위해 개발자는 User 생성자 내부의 코드만 수정하면 된다.
    • 만약 User 생성자 내부에 코드가 없고, User 생성자를 호출하는 곳에서 각각 이런 제약조건을 사용하고 있다면 변경범위가 넓어지게 된다. 

    다시 정리하면 도메인 모델의 변경점을 도메인 객체에 반영하기가 쉬워진다. 

    댓글

    Designed by JB FACTORY