DDD 3. 엔티티
- 카테고리 없음
- 2023. 10. 15.
요약
- 엔티티는 식별키를 기준으로 동일성이 판단되는 가변객체다.
- 특정 속성이 변하더라도 같은 객체로 판단해야하는 객체를 표현할 때 사용된다. (사람이 나이가 먹어도 같은 사람이다)
- 엔티티는 가변객체지만, 프로그램의 안정성을 위해서 필요한 부분만 가변으로 바꾸는 것이 좋다. 즉, 엔티티도 가능한 불변 객체로 남겨두는 것이 낫다.
- 같은 엔티티인지 확인하기 위해 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 생성자를 호출하는 곳에서 각각 이런 제약조건을 사용하고 있다면 변경범위가 넓어지게 된다.
다시 정리하면 도메인 모델의 변경점을 도메인 객체에 반영하기가 쉬워진다.