[도메인 주도 설계 철저 입문] 12. 도메인의 규칙을 지키는 "어그리게이트"
어그리게이트란?
여러 객체가 모여 한가지 의미를 갖는 하나의 객체가 될 때, 이를 어그리게이트라고 할 수 있다.
어그리게이트는 경계와 루트를 갖는다. 루트는 어그리게이트 내의 특정한 객체인데, 외부에서 어그리게이트를 다루는 모든 조작은 루트를 거쳐야만 한다. 어그리게이트에 포함되는 객체를 외부에 노출하지 않음으로써 객체를 안전하게 다룰 수 있다.
객체를 다루는 기본 원칙
질서 없이 어그리게이트 내의 객체들을 다루게 되면, 객체의 논리적 일관성을 유지하기가 어렵다.
UserName userName = new UserName("gildong");
user.name = userName; // 바람직하지 않은 변경 시도
user.changeName(userName); // ok
위와 같이 사용자의 이름을 변경 코드가 있다. 어그리게이트의 루트가 아닌 멤버 객체를 직접적으로 바꾸는 것은 User 라는 객체의 일관성을 해칠 수 있다. 만약 이름을 변경할 때마다 이름에 대한 유효성 검사를 진행해야한다면, 유사한 코드가 이곳 저곳에 퍼져있게 될 것이다. 따라서 user 객체에게 `이름변경`이라는 행위를 위임함으로써 객체 스스로가 자기 자신에 대해서 일관성 있게 행동하도록 해야한다. 프로그래머가 객체의 속성을 직접적으로 변경하는 것은 바람직하지 않다.
우리가 보통 개발할 때에는 객체의 내부 사정을 훤히 다 알고 있다. 그래서 위와 같은 안좋은 사례들이 나오는 것 같은데, OOP의 기본 원리 중의 하나가 `추상화`인 것을 다시 생각해보자. 객체 밖에서 해당 객체를 바라보았을 때는 내부의 데이터나 구체적인 코드구현을 몰라도, 객체의 의미만 알면 된다는 뜻가 아닐까. 즉 개발자는 어떤 객체의 내부를 너무 잘 알고 있더라고, 객체 외부의 코드를 작성할 때는 마치 객체 내부 구현을 하나도 모르는 것처럼 코딩해야한다. 객체의 상세를 너무 잘 알고 있는 나머지, 직접 데이터에 접근하면서 일관성을 깨뜨리는 우를 범할 수 있기 때문이다.
내부 데이터를 함부로 공개하지 않기
이런 상황을 방지하기 위한 방법으로, 내부 데이터는 외부에 함부로 공개하지 않는 방법이 있다.(즉 getter를 사용하지 않는 방법이다.) 사실 당연하게도 위의 예제에서 name 객체가 외부에 노출되지 않는다면 애초에 문제가 될 상황이 만들어지지 않는다. 뿐만 아니라 getter 도 노출시키지 말아야한다는 것은 무슨 의미인가?
if(circle.getCount() < 30) {
// ...생략...
}
예로 Circle 객체에 회원의 수를 나타내는 count 변수가 있다고 해보자. circle.getCount() 와 같이 getter 를 통해 총 회원수에 접근하도록 허용하는 것에 대해서 생각을 해보아야한다. 만약 circle 의 총 회원수를 검사하는 로직이 부분적으로 필요할 때, 아무생각 없이 위처럼 작성할 수 있을 것이다. 저 코드가 논리적으로 무엇을 의미하는지 알 수 있는 방법이 있을까?
아래와 같이 리팩토링 했을때를 비교해보자
if(!circle.isFull()) {
// ...생략...
}
확실히 의미를 알기 쉬워졌다. 또 `회원이 가득차있는 상태인지 확인하는 행동`을 circle 객체가 스스로 판단하게 했다. 위와 같이 getter 를 사용한 코드들이 이곳저곳에 흩어져있을 때, circle의 최대 인원이 50명으로 변경되었다면.. 생각만 해도 끔찍한 일이다.
어그리게이트의 경계를 어떻게 정할 것인가
어그리게이트의 경계를 정하는 것은 매우 어려운 주제이지만, 가장 흔히 쓰이는 원칙 중 하나는 '변경의 단위' 이다. 이것을 어겼을 때 그 이유를 명확히 알 수 있다. 생명주기가 다른 여러 객체가 어그리게이트에 포함되어 있을 때, 주기의 sync를 맞추기 위한 레파지토리의 코드가 상당히 더러워지기 때문이다. 최소한의 변경 단위를 설정하는 일이라고 봐도 무방할 듯하다. 이렇게 되면 하나의 레파지토리 객체는 하나의 어그리게이트만 담당하게 된다.
어그리게이트가 너무 커지게 되면 한번의 트랜젝션에서 다루는 데이터가 많아진다. 자원 경합이 심한 객체에서 이런 상황이 벌어지면, 한 번 변경을 성공하기 위해 여러번 트랜젝션을 시도해야할 수도 있다. 따라서 어그리게이트의 크기를 너무 크게 하지 않도록 하는 것도 중요하다.
식별자 이용하기.. 난 반댈세
public class Circle {
/* ...생략... */
//private List<User> members;
private List<UserId> members;
/* ...생략... */
}
실제로 서비스를 설계하다보면, 딱 떨어지게 어그리게이트의 경계를 구분하기가 어려운 부분이 많다. '어그리게이션의 경계를 넘어서는 안된다'는 불문율을 지키기 위해 애쓰곤 한다. 이런 경우에는 인스턴스를 포함시키는 대신에 식별자만 포함시키도록 하는 방법이 있다. 이렇게 하면 id를 통해 User를 복원할 수 있으면서, 어그리게이트를 심하게 어기지 않을 수 있다.
라고 저자는 썼지만 근데.. 난 이 의견에는 동의할 수 없다. OOP 같지 않기 때문이다. Circle과 User를 완전한 객체로서 다루려고 해야하지 않나? UserId로 다룬다는 것은.. 결국 database를 생각하면서 코딩을 할 수 밖에 없다는 것인데, 그렇게 되면 프로젝트에서 객체스럽지 못한 냄새가 풍길 것 같은 느낌이 든다.
이 경우에는 (User) 와 (Circle 내부 Member로서의 User)를 다르게 구분하는게 대안이 될 수 있을 것 같다. circle 이라는 어그리게이트 내부에서 의미하는 User 와, User 어그리게이션에서의 User 의미는 엄연히 다르다. 동아리(?) 멤버로서의 특성과, 서비스 전체를 대표하는 사용자로서의 특성이 같을 수 없기 때문이다.