Entity와 VO

2 분 소요

이 글은 항상 논쟁거리가 되는 Entity, VO가 각각 무엇을 의미하는지 토론이 벌어질 때 마다 추가 되는 글입니다.

틀리다고 생각하시는 부분이 있으면 지적 부탁드립니다.

Entity

Entity는 어떤 속성으로 같음을 판단하는 객체?

예를 들어 User라는 Entity가 id(PK), name, email, password를 갖는다고 치면 데이터베이스에서는 id로 user를 구분한다.

@Entity
@NoArgsConstructor
@Getter
@EqualsAndHashCode(of = "id")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Convert(converter = EmailConverter.class)
    private Email email;

    @Convert(converter = PasswordConverter.class)
    private Password password;

    @Convert(converter = NameConverter.class)
    private Name name;
}

이 User 객체에서 Id를 제외하고 다른 필드들이 변경 되더라도 Id가 동일하다면 동일한 엔티티다. 엔티티는 속성이 바뀔 수 있기 때문에 가변 객체다.

Value Object

값 자체로 같음을 판단한다. 불변 객체다.

위 코드에서 이어 간다면,

@EqualsAndHashCode(of = "email")
public class Email {
    private static final String EMAIL_REGEX = "^[_a-zA-Z0-9-.]+@[.a-zA-Z0-9-]+\\.[a-zA-Z]+$";

    private final String email;

    private Email(final String email) {
        this.email = validateEmail(email);
    }

    public static Email of(final String email) {
        return new Email(email);
    }
}

이런 VO가 있을 수 있겠다. 여기서는 email이 같으면 같은 객체이다.

또 다른 예시를 들어보면 만약 레이싱 게임 코드를 작성하는데, MovedCar라는 VO가 있고 그 VO는 이름과 현재 위치를 갖는다. 그 이름과 현재 위치가 같다면 두 객체는 같은 객체이다.

public class MovedCar {
    private final String name;
    private final int currentPosition;

    public MovedCar(final String name, final int currentPosition) {
        this.name = name;
        this.currentPosition = currentPosition;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        MovedCar movedCar = (MovedCar) o;
        return currentPosition == movedCar.currentPosition &&
                Objects.equals(name, movedCar.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, currentPosition);
    }
}

MovedCar car1 = new MovedCar("MyCar", 10);

MovedCar car2 = new MovedCar("MyCar", 10); 이 경우 car1 과 car2는 동일한 객체다.

테스트 코드로 다음을 확인해볼 수 있다.

public class MovedCarTest {
    @Test
    @DisplayName("동등성 확인")
    void equal() {
        MovedCar car1 = new MovedCar("MyCar", 10);
        MovedCar car2 = new MovedCar("MyCar", 10);

        assertEquals(car1, car2);
//        assertSame(car1, car2);
    }
}

실제 객체 레퍼런스 자체는 다르지만(동일성), 동등한 객체(동등성)임을 알 수 있다.(위 코드에서 assertSame 부분의 주석을 풀면 테스트 실패가 된다.)

assertEqauls(expected, actual) 부분을 본다면, 아래와 같다.

// Assertions.java
public static void assertEquals(Object expected, Object actual) {
    AssertEquals.assertEquals(expected, actual);
}

// AssertEquals.java
static void assertEquals(Object expected, Object actual) {
    assertEquals(expected, actual, (String) null);
}

// AssertionUtils.java
static boolean objectsAreEqual(Object obj1, Object obj2) {
    if (obj1 == null) {
        return (obj2 == null);
    }
    return obj1.equals(obj2);
}

위와 같이 equals로 비교하고 있다는 것을 알 수 있다. 반면 assertSame(expected, actual)을 따라가보면 아래와 같다.

// Assertions.java
public static void assertSame(Object expected, Object actual) {
    AssertSame.assertSame(expected, actual);
}

// AssertSame.java
static void assertSame(Object expected, Object actual) {
    assertSame(expected, actual, (String) null);
}
static void assertSame(Object expected, Object actual, String message) {
    if (expected != actual) {
        failNotSame(expected, actual, message);
    }
}

위 처럼 !=로 비교함을 알 수 있다.

왜 VO와 Entity의 구분이 중요할까

같은 속성을 가진 두 개의 Entity가 있다고 해보자. 두 객체가 다른 id를 가지고 있다면 둘은 다른 객체이다. 그러나 같은 속성을 가진 두 개의 VO는 같은 객체이다.

그리고 Entity는 존재하는 동안 다른 속성들이 계속해서 바뀔 수 있다. 하지만 계속 같은 엔티티다.

그러나 VO는 값이 바뀐다면 다른 객체가 된다. 그러니까 돈이라는 객체가 있다고 한다면, 그 돈을 사용해서 얼마를 지불하고 거스름돈을 받을 것이다. 그러면 그 거스름돈은 처음에 내가 지불했던 돈과는 다른 값 객체가 되어서 돌아오는 것이다.

참고 자료: What is the difference between Entities and Value Objects?

태그: ,

카테고리:

업데이트:

댓글남기기