1. 배경
Devlime의 Prism에서 Scrum의 AgileProject(스프린트 실행 단위)와 Crewit의 Project(계약 및 공수 관리)에 대한 정보를 묶어서 표시하기 위해서 두 도메인 사이에 관계를 맺어야 하는 요구사항이 발생하게 되었습니다. 또한, 두 도메인의 관계를 맺는 화면에서 Crewit의 공수 정보를 표시해야 했습니다.
이에 따라, 공유 API라는 새로운 레이어가 추가되었고, 해당 레이어를 활용하여 두 MS의 도메인 간의 관계를 정의하고, 하나의 MS의 도메인을 공유 도메인으로 설계하여 다른 MS에서도 해당 도메인을 활용할 수 있도록 설계하게 되었습니다.
이 문서는 위 사례를 계기로 정리한 공유 도메인 및 MS 간 도메인 참조 설계 원칙과 방식을 공유하여 추후에 새로운 공유 도메인 설계 시 참고하기 위함입니다.
2. 설계 판단 기준
MS 간 도메인을 매핑해야 하는 상황은 공유 API의 존재 여부와 성능 이슈 발생 여부에 따라 아래 3가지로 나뉘고, 각각 적용해야 할 설계가 다릅니다. 실제 설계에 들어가기 전에 먼저 어떤 경우에 해당하는지부터 판단해야 합니다.
|
시나리오 |
조건 |
설계 방식 |
|---|---|---|
|
1 |
공유 API가 없고, 두 MS 도메인 간 매핑이 필요할 때 |
참조하는 MS가 원본 MS의 도메인을 엔티티 형식으로 직접 보유 |
|
2 |
공유 API가 존재하고, 성능 이슈가 크게 발생하지 않을 때 |
참조하는 MS는 원본의 ID만 보유, 필요 시 공유 API를 통해 ID로 조회 |
|
3 |
공유 API가 존재하고, 성능 이슈(N+1, 반복 호출 등)가 발생할 때 |
참조하는 MS가 표시용 VO를 보유, 배치/지연/비즈니스 로직으로 갱신 |
3. 설계 원칙
|
공유 도메인 설계 |
MS 간 참조 설계 |
|
|---|---|---|
|
예시 |
공유 API의 공유 도메인 엔티티 |
scrum AgileProject가 갖는 crewit Project 참조 |
|
위치 |
별도의 공유 API 서비스 내부 |
참조하는 쪽 서비스(scrum)의 도메인 내부 |
|
성격 |
원본 MS 도메인의 프로젝션 |
원본과 별개의 독립 객체 또는 원본 MS 도메인의 Id |
3-1. 공유 도메인 설계 원칙
공유 도메인은 원본 MS의 도메인 중 다른 MS와 공유하기 위한 목적으로 존재하는 도메인입니다. 아래 원칙을 따릅니다.
1. 원본과 동일한 생명주기를 가진다
원본 MS에 저장/수정이 발생하면 이벤트를 통해 공유 도메인도 함께 갱신됩니다. 즉, 원본과 생명주기가 같도록 합니다. 원본 MS의 기존 DataEvent → ProjectionHandler 훅에 트리거를 걸어 반영합니다.
2. 공유에 필요한 필드만 포함한다
원본 필드를 전부 복제하지 않습니다. 원본 MS 내부 관리용 필드는 제외하고, 다른 MS가 실제로 필요로 하는 필드만 포함합니다.
3. 명칭은 원본 용어를 그대로 쓰지 않는다
원본 MS의 엔티티명을 그대로 가져오면 다른 MS 입장에서 의미가 모호해집니다. 실제 핵심 속성을 반영해 재정의합니다.
3-2. MS 간 참조 설계 원칙
한 MS의 도메인이 다른 MS(또는 공유 도메인)의 정보를 참조함으로써 관계를 표현해야 하는 경우입니다. 구체적으로 어떻게 참조할지는 시나리오에 따라 달라지므로 아래에서 시나리오별로 나눠 설명하겠습니다.
세 시나리오 모두에 공통으로 적용되는 원칙은 다음 두 가지입니다.
1. 전파된 데이터는 원본과 별개의 객체로 취급한다
참조하는 쪽이 갖는 정보(엔티티, VO)는 원본 필드의 복제가 아니라, 참조 도메인이 실제로 필요로 하는 속성만 담은 자체 정의 객체입니다.
2. 명칭은 참조하는 도메인의 관점에서 재정의한다
원본 용어를 그대로 쓰지 않고, 참조하는 도메인 입장에서 이 정보가 실제로 어떤 의미인지를 반영해 이름 짓습니다.
3-2-1. 시나리오 1 — 공유 API 없음: 엔티티 직접 보유
공유 API가 아직 없는 상태에서 두 MS 도메인을 매핑해야 할 때 참조하는 MS가 원본 도메인 정보를 자신의 엔티티로 직접 보유합니다. 사실상 공유 도메인의 역할을 참조하는 MS 내부에서 가지고 있게 됩니다.
1. 공유 도메인과 동일하게 sourceSystem/sourceKey로 추적성을 확보합니다.
2. 가능하면 원본의 이벤트를 받아 갱신하고, 그럴 방법이 없다면 수동/배치 갱신 경로를 마련합니다.
3. 참조하는 MS가 둘 이상으로 늘어나면 각자 비슷한 사본을 따로 만들게 되어 관리가 어려워집니다. 그 시점에는 공유 API를 만들어 확장성 있게 구성합니다.
3-2-2. 시나리오 2 — 공유 API 존재, 성능 이슈 없음: ID 참조 (기본)
공유 API가 있고, 반복 조회로 인한 성능 문제가 확인되지 않았을 때 참조하는 MS는 ID만 보유하고, 조회 시점마다 공유 API를 실시간 호출합니다.
1. VO 필드를 두지 않고, 참조하려는 도메인의 참조키(ID) 하나만 필드로 보유합니다.
2. 조회 시점에 공유 API를 실시간 호출하여 참조키를 통해 매핑되어 있는 도메인을 가져옵니다.
3-2-3. 시나리오 3 — 공유 API 존재, 성능 이슈 발생: VO (예외)
시나리오 2로 운영하다가 반복 조회로 인한 성능 이슈가 실제로 확인됐을 때 참조하는 MS가 표시용 VO를 보유하고, 배치 재동기화 또는 지연(lazy) 갱신으로 채웁니다.
1. 동기화 시도 자체를 지양한다
실시간 강제 동기화(원본 변경 즉시 최신화)는 채택하지 않습니다.
2. 정합성 문제는 비즈니스 로직으로 해결한다
VO는 표시 전용으로만 쓰고, 실제 비즈니스 판단이 필요한 로직에서는 VO 값을 신뢰하지 않고 참조키로 원본(또는 공유 도메인)을 재조회합니다.
3. syncedAt으로 최신성을 가시화한다
마지막 동기화 시각을 함께 노출하여 데이터의 신선도를 확인할 수 있게 합니다.
갱신 방식
|
방식 |
내용 |
특징 |
|---|---|---|
|
배치 재동기화 |
스케줄러가 주기적으로(예: 1일 1회) 재조회해 VO 갱신 |
인프라 추가 최소, 정합성 지연 폭이 배치 주기만큼 고정적으로 발생하는 것을 정상으로 받아들임 |
|
지연(lazy) 갱신 |
scrum이 AgileProject를 다루는 시점(생성/수정 등)에 syncedAt이 오래됐으면 그때 재조회 |
배치 인프라 불필요. 다만 한동안 접근이 없으면 계속 최신화 이전 상태로 남을 수 있음 |
반드시 위 두 방식만 사용해야 하는 것은 아니며, 비즈니스적으로 불일치 문제를 해결하는 다른 방법이 있다면 그 방법을 사용할 수 있습니다.
4. 설계 예시 (시나리오 2)
① 원본 (crewit의 Project)
public class Project extends StageEntity {
...
private String code; // 코드
private LangStrings langNames; // 이름
private LangStrings langDescription; // 설명
private LangStrings langCustomers; // 고객사
private LangStrings langLocations; // 위치
private Member manager; // PM
private ProjectStatus projectStatus; // 프로젝트상태
private ProjectPeriod projectPeriod; // 프로젝트기간
private String skillSet; // 기술세트
private LangStrings langVendors; // 협력업체
private LangStrings langRiskNotes; // 리스크
private double budget; // 예산, ex, 21억5천만원
private int totalEffort; // 총공수, 230 M/M
private boolean active;
transient List<ProjectManMonthPlan> projectManMonthPlans; // 구성원별
transient List<ProjectMonthPlan> projectMonthPlans; // 월별
transient List<ManOutput> manOutputs;
transient List<ManRatio> manRatios;
transient List<ManOutputMonthSum> manOutputMonthSums;
transient List<ManRatioMonthSum> manRatioMonthSums;
...
}
② 공유 도메인 (공유 API의 Staffing Project — Project의 Projection)
public class StaffingProject extends StageEntity {
...
private String id; // PK
private String sourceSystem; // 원본 MS 또는 도메인 추적용
private String sourceKey; // crewit Project의 Id
private String code; // 코드
private LangStrings name; // 이름
private LangStrings description; // 설명
private LangStrings customerNames; // 고객사
private ProjectStatus status; // 프로젝트상태
private ProjectPeriod projectPeriod; // 프로젝트기간
private Member manager; // 담당자
private double budget; // 예산, ex, 21억5천만원
private int totalEffort; // 총공수, 230 M/M
private boolean active;
private LocalDateTime syncedAt; // 마지막 동기화 시각
...
}
-
crewit Project 저장/수정 시점에 DataEvent/ProjectionHandler 훅 + 이벤트를 통해 갱신 (원본과 동일 생명주기)
-
skillSet/budget/langVendors 등 내부 관리 필드는 제외
-
명칭은 "계약 정보"보다 "공수 정보"가 핵심 속성이라는 논의에 따라 StaffingProject로 명명
-
syncedAt으로 마지막 동기화 시각을 관리 (공유 도메인이라도 완전한 실시간 반영을 보장하진 않으므로 추적 목적으로 유지)
③ 참조하는 서비스 (scrum의 AgileProject)
public class AgileProject extends StageEntity {
...
private String staffingProjectId; // StaffingProject 참조키. ID만 보유.
...
}
-
AgileProject는 crewit/공유 도메인의 정보를 필드로 보유하지 않습니다 — ID 하나만 참조.
-
화면 표시 시점마다 공유 API를 호출해 정보를 가져옵니다.
5. 참고 (시나리오 3) — 성능 이슈 발생 시 대비
향후 조회 빈도가 높아져 성능 이슈가 확인되면, 아래처럼 표시용 VO를 임베딩하는 방식으로 전환을 검토합니다. 현재는 채택하지 않으며 참고용으로만 작성합니다.
public class StaffingProject implements ValueObject {
...
private String staffingProjectId; // staffing project id
private LangStrings name; // 이름
private LangStrings customerNames; // 고객사
private ProjectStatus status; // 프로젝트상태
private ProjectPeriod projectPeriod; // 프로젝트기간
private Member manager; // 담당자
private double budget; // 예산, ex, 21억5천만원
private int totalEffort; // 총공수, 230 M/M
private LocalDateTime syncedAt; // 마지막 동기화 시각
...
}
-
AgileProject에 포함하여 관계 지정
-
crewit의 복제본이 아닌 scrum의 별개 독립 객체로 관리
-
실시간으로 원본을 반영하지 않고, 배치 재동기화 또는 지연 갱신(또는 다른 비즈니스 로직)으로 채워짐
-
전환 시점은 실제 조회 패턴에서 성능 이슈가 확인된 이후로 한정
Bignow