하나의 프로젝트가 장기화 되면 될수록 유지보수는 커녕 점점 더러워지는 코드베이스에 신물이 나기 시작했다.
이를 해결하기 위해서는 항상 외부 요인을 찾곤했었는데 — EDA 라던지, 모듈구조 등등 —
진정한 해결책은 그게 아닌 내부의 복합적인 문제였다.
어떤 문제들이 있는지 살펴보고, 어떻게 해결할 수 있는지 정리해보고자 한다.
1. 흩어진 도메인 로직
2. 낮은 인프라 유연성
3. 정책 수정에 따른 코드 수정
4. 로직 구현 위치 판단 어려움
5. 지켜지지 않는 컨벤션
6. 확장하기 어려운 도메인 코드
역할
규칙
1. 페이지네이션과 아닌 것을 분리할 것
2. 액터 별로 분리할 것
역할
규칙
1. 유스케이스 당 하나의 클래스 선언할 것
→ SRP 및 Vertical Slice https://antondevtips.com/blog/vertical-slice-architecture-the-best-ways-to-structure-your-project 유지
2. 도메인 DAO 가 아닌 도메인 객체만을 사용할 것
역할
규칙
1. 시스템 별로 DAO 를 선언/활용
2. 최대한 의미있는 함수를 사용할 것 (특정 조건의 도메인을 가져오는 로직을 짜야하다보니 최대한 의미있는 함수를 활용하는 게 좋음)
역할
도메인 데이터에 대한 도메인 로직
데이터 조회가 아닌 데이터 변경사항은 모두 해당 컴포넌트를 거쳐야 함
규칙
1. 도메인 경계 벗어나지 말 것
2. Functional + 객체지향을 지향
Service Function 을 인자로 받는 도메인 로직 (예시)
public PriceV2 getPriceV2(
ChocoDiscountPolicyServiceV2 chocoDiscountPolicyServiceV2,
ChocoTruncatePolicyServiceV2 chocoTruncatePolicyServiceV2
){
return this.chocoPremiumV2.getDiscountedPrice(
this.urgentJobPostingPriceV2,
chocoDiscountPolicyServiceV2,
chocoTruncatePolicyServiceV2
);
}
Infra Function 을 인자로 받는 도메인 로직 (예시)
public static List<ChocoHistoryV2> consumeChoco(
Supplier<List<ChocoHistoryV2>> accumulatedChocoFetcher,
Supplier<Long> balanceFetcher,
Long choco
) {
List<ChocoHistoryV2> accumulatedChoco = accumulatedChocoFetcher.get(); // 사용 전인 적립된 (+) 유효기간 마감 임박 순으로 find
Long balance = balanceFetcher.get(); // 회원의 초코 총액 계산
// 초코 사용 여부 검증
if (!isConsumable(accumulatedChoco,choco,balance)){
throw new CustomException(ErrorCode.LACK_OF_CHOCO);
}
// 충전 초코 사용
AtomicLong initialPrice = new AtomicLong(choco);
accumulatedChoco
.stream()
.takeWhile(ch -> initialPrice.get() > 0) // 초코가격이 완전히 사용될 때까지
.forEach(ch -> ch.updateConsumedChoco(initialPrice)); // 충전초코들을 차감
return accumulatedChoco;
}
위 구조에 대해 규칙을 강제화하는 방안들이다.
선택적인 사항이므로 개선사항으로 간주하였다.
@QueryDelegate
를 활용하여 QClass 에 위임// User Jpa Model
@Entity
@QueryEntity
public class User {
String name;
User manager;
@QueryDelegate(User.class)
public static BooleanPath isManagedBy(QUser user, User other){
return user.manager.eq(other);
}
}
// QUser is a Querydsl query type for User
@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QUser extends EntityPathBase<User> {
private static final long serialVersionUID = -1825801311L;
private static final PathInits INITS = PathInits.DIRECT2;
public static final QUser user = new QUser("user1");
public BooleanPath isManagedBy(QUser other) {
return User.isManagedBy(this, other);
}
,,,
}
// ❌
JPAQuery<T> query = select(select)
.from(memberCompanyV2)
.where(memberCompanyV2.mcIsShutDown.eq(0))
,,,
;
// ✅
JPAQuery<T> query = select(select)
.from(memberCompanyV2)
.where(memberCompanyV2.isShutDown())
,,,
;
https://github.com/vanillacake369/otlp-demo/tree/divide-per-domain
https://github.com/vanillacake369/otlp-demo/tree/divide-per-usecase
https://medium.com/@inzuael/anemic-domain-model-vs-rich-domain-model-78752b46098f
https://www.cnblogs.com/aspiration2016/articles/13306649.html
https://lannex.github.io/blog/2022/ddd-hexagonal-onion-clean-cqrs/
https://github.com/hgraca/explicit-architecture-php/tree/master
https://vaadin.com/blog/ddd-part-3-domain-driven-design-and-the-hexagonal-architecture