사실 이 두가지 개념을 어떻게 풀어 나가야 할지 참으로 난해하다. 왜냐하면 'Entity'와 'Value Object'는 개발자가 어떤 관점에서 바라보느냐에 따라 이리도 될 수 있고 저리도 될 수 있는 참 모호한 개념이기 때문이다.
Entity
일반적으로, entity란 존재하는 것, 즉 실체를 의미한다. 이 용어의 어원은 라틴어의 ens에서 나왔으며, 사물의 존재와 그것의 품질 사이에 구별을 짓는다. entity가 되기 위해 필요한 일은, 그저 존재하는 것이 전부이다. 어떤 것이 존재한다는 사실은, 그것이 다른 존재나 entity로부터 분리되어있음을 암시하는 듯하다. 프로그래밍이나 공학에서, 그리고 아마도 많은 다른 상황들에서, 이 용어는 명확한 사물이든, 이름이 붙여져 있지 않은 추상화된 아이디어든 관계없이 단일체를 인식하는데 사용된다.
굉장히 비슷한 성격을 갖고 있는 두 개념이기에 우선 공통부분을 살펴보고 왜 'Entity'와 'Value Object'가 구분될 수 있는지 살펴보자. 우선 'Entity'와 'Value Object'는 둘다 서비스 내에서 유일함을 인정받는 오브젝트이며 정확히 구분될 수 있는 식별성을 갖고 있다. 그리고 둘 다 하나 이상의 속성을 가질 수 있으며 이러한 고유 상태값은 데이터베이스에 기록되어 보존할 수 있다.
그 뿐만이 아니다.'Entity'와 'Value Object'는 서로가 서로를 참조하는 집합체가 될 수도 있는데, 'Entity'가 'Value Object'를 감싸안은 'AGGREGATE'(집합체)가 될 수도 있고 반대로 'Value Object'에서 'Entity'를 참조할 수도 있다.
이렇게만 놓고 본다면 둘은 굉장히 비슷한 성격을 가지고 있는데다 그 역할이 거의 흡사해, 구분 짓는 것의 의미가 없어보일지도 모른다. 실제로 많은 개발자들이 빈약한 도메인 오브젝트를 일괄적으로 'VO'라는 네이밍('Value Object'의 줄임말)을 짓는 것을 보면 이런 구분을 잘 이해하지 못하는 경우도 비일비재한 듯 싶다.
물론 빈약한 도메인을 활용하는 'MVC' 아키텍쳐에서 굳이 'Entity'와 'Value Object'를 구분지어줄 필요는 없으므로 불필요한 성격분류를 할 필요는 없다. 다만 도메인 주도 설계에 큰 관심이 있고 실제로 서비스에 적용해보고자 할 때에는 둘의 성격을 반드시 구분지어줄 필요가 있다.
Value Object
'ValueObject'는 'J2EE'의 패턴 중 하나로 일정한 입력값을 정리하여 보관 및 유지해 두는 오브젝트이다. J2EE 패턴에서는 'Trasfer Object'라고도 하는데 본래에는 'EJB'에서 원격 메소드 호출의 회수를 줄임과 동시에 퍼포먼스의 저하를 막는 목적으로 개발된 패턴이다.
그렇다면 본격적으로 둘의 성격이 어떻게 구분지어질 수 있는지 살펴보자. 궁극적으로 둘의 성격은 하나의 조건으로 정확하게 분리될 수 있는데 그 조건은 다름 아닌 연속성이다. (연속성이란 식별 가능한 오브젝트의 속성들이 필요에 따라 변경될 수도 있다는 것을 의미한다.)
보다 빠른 이해를 위해 카페에서 커피잔을 들고 있는 한 남자를 상상해보자. 우리는 이 남자와 커피잔 모두 구분지어 질 수 있는 (식별성을 가진) 오브젝트가 될 수 있다는 사실을 알 수 있다. 여기서 너무 디테일한 시간적인 연속성은 제외하고 남자와 커피잔을 코드 상 오브젝트라 인지하면서 바라보았을 때, 남자라는 오브젝트는 입고 있는 옷이라던가 헤어스타일, 기분 같은 것들이 지속적으로 변경될 수 있다는 점을 알 수 있다.
반대로 우리는 커피잔이라는 오브젝트가 변할 수가 없다는 사실 또한 알고 있다. (물론 '변할 수 있다'라는 주장도 가능하다. 일단 필자의 이야기를 끝까지 들어보자!) 커피잔이라는 오브젝트는 커피잔 자체가 사라지고 새로운 커피잔 오브젝트가 등장하지 않는 우리가 앞서 생각했던 커피잔이 변경될 소지는 거의 없다.
그러므로 우리는 여기서 연속성이라는 성격으로 두 오브젝트, 남자와 커피잔의 성격을 분리할 수 있게 되었다. 남자라는 오브젝트는 연속성을 가지고 있으므로 우리는 실체가 존재한다하여 'Entity'라고 정의할 수 있다. 그리고 커피잔은 변경될 수 없는 불변의 가치를 가지고 있으므로 'Value Object'라고 부를 수 있다.
문제는 이러한 관점이 극히 주관적이라는 것이다. 만약 당신이 이 커피잔을 시간이 흐름에 따라 부식되는 상태를 표현한다거나 담겨있는 커피 양의 차이를 세밀하게 표현하고자 하는 이유를 들어 커피잔을 'Entity'라 주장할 수도 있다! 그리고 이러한 이유는 상황에 따라 매우 타당하게 받아들여질 수 있을 것이다.
결론적으로 보자면 위와 같은 분류가 가능했던 까닭이 단지 필자가 전체적인 상황을 남자라는 오브젝트를 중심으로 이해했기 때문에 커피잔이 'Value Object'가 됬을 뿐, 실상 둘다 'Entity'가 될 수도 있고 'Value Object'가 될 수 있다.
그렇다면 두 오브젝트는 도대체 어떻게 분리해야 할까? 이것은 어디까지나 설계자 스스로의 역량이다. 사물의 본질을 잘 이해하고 이것이 변하지 않는 고유의 성질을 갖고 있다면 설계자 스스로가 판단하여 'Value Object'로 분류하면 되고 연속성을 가지고 있다면 'Entity'로 분류하면 된다.
가끔 설계 단에서 하나의 오브젝트를 놓고 'Entity'와 'Value Object'를 구분짓는 논쟁이 벌어지곤 하는데… 솔직히 이건 정말 무의미한 소모전이다. 왜냐하면 대부분의 경우, 서로 주장하는 이유가 맞는 말이기 때문이다. 아마 역사상 '엄마가 좋아 아빠가 좋아' 질문 다음으로 가장 짜증나고 답답한 논쟁이라고 생각해도 될 것이다.
혹여나 개발자 사이에서 이러한 논쟁의 시비가 붙는다면 일단 서로의 이유는 잠시 접어두고 서비스의 성격만 고려하여 결론짓는 것이 가장 유리하다. 괜히 앞으로 있지도 않을 디테일한 성격을 들어 이것을 'Entity'라 지정할 필요도 없고 너무 값을 'static'하게 가져가려고만 해서도 안된다. 이런 성격의 분리는 서비스의 성격만 고려하더라도 분명한 결론에 도달할 수 있다.
마지막으로 기술적인 이야기만 짤막하게 하고 넘어가자면 'Value Object'는 연속성을 가지지 않는 오브젝트이므로 내부 프로퍼티를 final로 지정하여 변경을 불가하는 것이 좋다.
도메인 주도 설계에서 말하는 'Service'란 근본적으로 'Entity'나 'Value Object'와 다르지 않다. 물론 다르지 않다는 개념 자체가 'Service'가 데이터 단위로 활용된다거나 일정한 값을 가지고 있다는 뜻은 아니고 동일하게 도메인 레이어 (또는 계층)에서 'Entity'나 'Value Object'와 같은 오브젝트 따위로 취급되는 것을 일컫는다.
이게 개념은 말처럼 쉬운데 사실 기존의 개발풍토에 익숙한 개발자라면 실질적으로 깨닫고 코드로 옮기기까지 매우 많은 시행착오를 겪을 수 있다. 왜냐하면 보통 개발환경에서 통념 상 'Service'는 하나의 계층으로 'DAO' 계층에서 작성된 오브젝트를 근간으로 크리티컬한 로직을 작성하거나 처리하는 용도로 사용되곤 하지만 도메인 주도설계에서 'Service'는 객체의 행위 또는 활동을 표현하는 단위로 사용되기 때문이다.
필자가 겪은 시행착오를 하나 살펴보자. 다음의 코드는 처음 서비스 오브젝트를 만들 때 습관적인 개발방식에 따라 작성한 서비스 인터페이스이다.
public interface UserService {
User add(User user);
User update(User user);
void remve(User user);
...
}
좀 급하게 만드느라 약간 'DAO (Data Access Object)'스런 코드를 만들긴 했지만 어찌됬든 일반적인 개발환경의 관점에서 바라보자면 위의 코드는 충분히 서비스 계층에서 끌고 나갈 수 있는 코드이기도 하다. 이런 코드를 도메인 주도설계에서는 맹목적인 서비스라고도 하는데 서비스 자체가 하나의 도메인 오브젝트('Entity' 또는 'Value Object')에 종속적이고 행동이나 활동에 근간을 두지 않고 'Management'의 성격을 띄게 때문이다.
도메인 주도설계에서는 이러한 'Management', 즉 생존주기와 관련된 것들은 도메인 스스로가 결정하게 되어있으며 이것을 기존의 코드 방식처럼 다른 서비스 또는 'DAO'에 위임하는 것을 지향하지 않는다.
그렇다면 위의 코드를 도메인 주도설계 목적에 맞게 바꾸어 본다면 어떻게 바뀔 수 있을까? 아마 아래와 같이 작성할 수도 있겠다.
public interface AccountService {
User register(final String email, final String password);
User unsubscribe(final User user);
...
}
필자는 위의 'UserService'를 'AccountService'로 바꾼 다음에 그 성격을 사용자의 생존주기에 대한 행위를 기준으로 작성하였다. 서비스를 작성할 때 주의해야 할 점은 자신이 만든 메서드를 직접 소리내어 말해보았을 때 이것이 어색함없이 받아들일 수 있어야 하는데 도메인 주도설계가 줄곧 말하는 유비쿼터스 언어의 특성을 살리기 위해서도 그러하고 기본적으로 좋은 메서드를 만드는 좋은 방법 때문이기도 하다.
'register' 메서드와 'unsubscribe' 메서드를 소리내어 말해보자. 각각 "이메일과 패스워드로 사용자를 등록합니다.", "사용자를 탈퇴시킵니다."라고 말할 수 있겠다. 그리고 이런 방식으로 서비스를 작성하다보면은 서비스가 특정 도메인에 종속되지 않고 독립적인 성격을 띈 도메인 오브젝트로 만들어낼 수 있다.
물론 도메인 주도설계 방식으로 'Service'를 만들 때 주의해야 할 점은 'Entity' 또는 'Value Object'에서도 충분히 다룰 수 있는 기능들을 모조리 'Service'로 제작해서는 안된다는 것이다. 보통 많은 개발자들이 도메인 오브젝트에 어떠한 로직을 집어넣기 보단 빈약한 도메인 오브젝트 방식(프로퍼티와 'get...', 'set...' 으로 캡슐화만한 오브젝트)으로 작업해오다보니 도메인에 어떠한 로직이 들어가야 올바른지 헷갈려 하는 경우가 많다. 게다가 도메인 주도설계는 아키텍터, 또는 어플리케이션 특성에 따라 매우 주관적인 코드가 될 수 있으므로 이런 로직들에 대해서는 개발자들 간의 충돌이 일어날 수도 있고, 어떠한 부분에서는 매우 애매모호한 경우가 발생하여 개발에 차질을 빚기도 한다.
이러한 상황에서 명확한 기준은 가급적 'Entity', 'Value Object'. 특히 'Root Entity'에 대해서는 'Aggregate' 경계 내의 오브젝트에 대한 통제 권한을 스스로 관리하게끔하고 그 이외의 서로 다른 'Aggregate'를 가진 도메인 끼리에 대해서는 서비스로 제작하여 처리하는게 올바른 듯 하다.