2009/12/03 15:46
도메인 모델을 구성하는 방법 도메인주도설계2009/12/03 15:46
DDD, 그리고 마틴파울러의 EAA 카탈로그, 그리고 많은 분들이 정리해두신 도메인에 관한 글들을 보면서 문뜩 이런 생각이 들었습니다.
단 주의하실 점이 있습니다. getter와 setter에 비지니스 로직이 들어가서는 안된다는 점이지요. 비지니스 로직이 포함되어야 한다면 add나 다른 네이밍 규칙을 통해서 비지니스 메서드임을 분명히 알려야합니다. OO의 개념을 양보한 이상 비지니스 로직이 포함된 메서드들과 데이터 제공을 위한 단순 getter/setter는 반드시 구분해야하기 때문입니다.
- 과연 도메인 모델(여기서는 DDD에서의 Entity 도메인으로 한정해서 이야기 하겠습니다. 이하 도메인 모델)을 어디까지 노출을 시켜야하는가?
- OO의 관점에서 바라볼 때 도메인 모델이 getter와 setter를 가지는 것이 바람직한 것인가?
구글링을 통해서 접한 지식으로는 국내뿐만이 아니라 외국에서조차 getter/setter를 가지는 것에 대해서 의견이 분분하였습니다. OO의 원칙상으로는 getter와 setter를 통해서 외부에 도메인 모델이 구성하고 있는 데이터를 노출하는 것은 좋지 않다고 봅니다. 하지만 우리가 일을 하고 있는 layered architecture에서 도메인 레이어와 서비스 레이어(마틴 파울러는 애플리케이션 레이어라고 부릅니다.)는 도메인 모델을 공유할 수 있을지 몰라도 persistence layer 또는 presentation layer에서의 데이터 교환시 문제가 발생합니다.
예를들어 Customer라는 도메인 객체를 만들어봅시다. (예를 위해서 비지니스 로직이 담긴 메서드는 생략합니다.)
public class Customer {
private Long id;
private String name;
private int age;
....
// 그 외 비지니스 로직에 관련된 메서드 있다고 가정함
}
private Long id;
private String name;
private int age;
....
// 그 외 비지니스 로직에 관련된 메서드 있다고 가정함
}
만약 위의 예제에서 단순히 데이터만 가지고 있다면 Anemic Domain Model이 됩니다. 이는 지양해야할 안티 패턴입니다. 그래서 오해의 여지를 없애기 위해 주석으로 비지니스 로직을 포함하고 있다고 명시했습니다.
만약 persistence layer에서 Repositories를 통해서 도메인 모델을 만들어낼 때 실제 구현에서는 ibatis든 hibernate든 반드시 id, name, age에 값을 넣어주어야 합니다. 하지만 이는 크게 문제가 되지 않습니다. 둘 다 getter/setter가 없어도 리플랙션을 통해서 직접 값을 넣고, 또는 값은 얻어가는 것이 가능하니까요. 만약 순수 JDBC를 사용한다면 그 부분을 해결할 수 있는 Data Mapper가 필요할 것입니다.
문제는 바로 presentation layer입니다. 도메인 모델을 presentation layer에 노출시키지 않는다고 가정하면 아주 많은 DTO 또는 BDT(Business Data Type)이 필요할 것입니다. 그렇다고 해서 도메인 모델을 노출을 시키면 getter/setter를 제공해야 할 것입니다. persistence layer에서처럼 리플랙션을 이용해서 할 수도 있겠지만 이는 쓸대없는 작업을 하는 것이나 다름없죠. 과도하게 리플랙션을 사용하기 보다는 차라리 getter/setter를 제공하는 편이 개발의 편의성을 돕는 일이니까요.
그러나 도메인 모델에 getter/setter를 만드는 것은 위에서 말한 것처럼 OO의 관점에서 볼 때 바람직하지 않습니다. 그렇다면 이러한 문제를 어떻게 해결하면 좋을까요?
1. OO의 개념을 훼손하지 않고 인터페이스를 제공한다.
이 방법은 Allen I. Holub이 제안한 방법입니다. 이 분은 getter/setter를 아주 싫어하죠. evil이라는 표현을 쓰시니까요 ^^; 정확한 내용은 다음을 참조하시면 됩니다.
요약하자면 다음과 같습니다.
getter, setter를 사용하지 마세요. 차라리 자바 1.5이상이라면 MetaData인 annotation을 사용하세요. 가장 좋은 방법은 import와 export에 대한 인터페이스를 도메인 모델이 제공하는 것입니다.annotation을 사용하면 다음과 같이 표현이 됩니다.
public class Customer {
@property private Long id;
@property private String name;
@property private int age;
...
// 기타 비지니스 메서드...
}
@property private Long id;
@property private String name;
@property private int age;
...
// 기타 비지니스 메서드...
}
자바 1.5에는 property라는 annotation이 없습니다. (있는 줄 알고 한참 찾았습니다.) 이는 의미론적인 요소로 metadata를 활용하라는 것이었습니다. 즉, 프레임워크를 위해서라면 동적으로 getter/setter를 생성하는 어노테이션을 이용하는 것입니다. 그렇게 되면 오염되어 의도치않은 동작을 하는 것을 막을 수 있습니다. 이를 구현한 것은 spring 2.5에 있는 @Autowired가 있으며 특히 이번 spring 3.0에 추가된 @Inject의 경우 JSR-330 스펙으로 채택되었습니다. 자세한 내용은 Toby님의 글을 참조하세요.
이 방법까지는 저도 이해도 잘 되고 어느정도 수긍이 갑니다. 하지만 다음에 나오는 인터페이스를 통한 방법은 이해는 가나 과연 이렇게까지 구현해야 하는가? 라는 생각이 들 정도입니다.
public class Customer {
private CustomerId id;
private Name name;
private Age age;
public Customer(Importer builder) {
builder.open();
this.id = new CustomerId(builder.provideId());
this.name = new Name(builder.provideName());
this.age = new Age(builder.provideAge());
builder.close();
}
public void export(Exporter builder) {
builder.addId(id.toString());
builder.addName(name.toString());
builder.addAge(age.toString())
}
....
// 그 외 비지니스 로직에 관련된 메서드 있다고 가정함
...
public interface Exporter {
void addId(String id);
void addName(String name);
void addAge(String age);
}
public interface Importer {
String proiveId();
String proiveName();
String proiveAge();
void open();
void close();
}
}
private CustomerId id;
private Name name;
private Age age;
public Customer(Importer builder) {
builder.open();
this.id = new CustomerId(builder.provideId());
this.name = new Name(builder.provideName());
this.age = new Age(builder.provideAge());
builder.close();
}
public void export(Exporter builder) {
builder.addId(id.toString());
builder.addName(name.toString());
builder.addAge(age.toString())
}
....
// 그 외 비지니스 로직에 관련된 메서드 있다고 가정함
...
public interface Exporter {
void addId(String id);
void addName(String name);
void addAge(String age);
}
public interface Importer {
String proiveId();
String proiveName();
String proiveAge();
void open();
void close();
}
}
분명 OO의 관점에서 보면 아주 좋은 방법같습니다. 마틴 파울러도 이 방법이 좋은 방법중 하나라고 표현을 했었습니다.(참조링크) 하지만 이 방법이 좋다고 무조건 도입할 수는 없습니다. 다음과 같은 단점이 존재하기 떄문이죠.
- 이러한 구현을 실제 도입하려면 프로젝트의 개발자들이 OO개념에 대해서 아주 잘 알고 있어야 합니다.
- 테스트하기가 쉽지 않습니다. 데이터 검증을 위해서는 도메인 객체 뿐만이 아니라 Importer와 Exporter에 대한 구현체가 존재해야 합니다. 또는 Mock객체를 활용해야 합니다.
결론적으로 제 생각으로는 이론은 좋지만 실제 프로젝트에 도입하기에는 스터디 비용 등이 너무 클 것 같습니다. 세상은 holub과 같은 똑똑한 개발자만 있는 것이 아니니까요. 그러나 이러한 구현을 도입해야할 경우도 있을 것 같습니다.
2. OO의 개념을 양보를 약간하고 getter와 setter를 제공한다.
가장 일반적인 방법이 아닐까 합니다. spring에서도 도메인 객체를 domain layer에서부터 presentation layer에 이르기까지 사용하고 있습니다. 또한 스터디 비용도 작아서 가장 현실적인 대안이라고 생각됩니다. (1번에서 annotation을 쓰는 것도 결국 이 방법과 동일한 방법입니다.)
public class Customer {
private Long id;
private String name;
private int age;
....
// 그 외 비지니스 로직에 관련된 메서드 있다고 가정함
...
// getter, setter
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
...
}
private Long id;
private String name;
private int age;
....
// 그 외 비지니스 로직에 관련된 메서드 있다고 가정함
...
// getter, setter
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
...
}
3. BDT를 구성하고 도메인의 import와 export를 BDT를 통해서 제공한다.
1번 사항과 약간 다른 개념입니다. persistence 또는 presentation layer에서 입력 또는 출력되는 데이터 형태를 BDT라는 표준 DTO 클래스를 통해서 하는 방법입니다. 1번에서는 인터페이스를 통해서 제공했지요. BDT는 어떻게보면 Anemic Domain Model이기도 합니다. 하지만 이 방법은 도메인을 외부로 노출하지 않는다는 장점이 있습니다.
이 외에도 많은 방법들이 있습니다. 어떠한 방법이 가장 좋다라고 말씀드릴 수는 없습니다. 항상 이런 문제에서의 답은 "최상의 정답은 없다. 다만 더 좋은 방법이 있을 뿐이다."라는 것이지요. 이러한 도메인 구성 방법을 현재의 프로젝트 팀과 프로젝트 상황, 그리고 비지니스 요구사항 등을 고려해서 선택하면 될 것이라 생각됩니다.
첨부파일 : Allen I. Holub의 자료입니다.
'도메인주도설계' 카테고리의 다른 글
| 도메인 모델을 구성하는 방법 (0) | 2009/12/03 |
|---|---|
| DDD에서의 도메인 객체들 (0) | 2009/11/05 |
Everything.You.Know.is.Wrong.pdf

