MSA 환경은 기본적으로 도메인 별로 분리되어있는 마이크로서비스 환경이다.
모놀리스 아키텍처에서 JPA를 사용할 때는 DDD를 이용해 도메인 별로 분리 시켜서 구현할 수도 있었지만 그렇게 하지 않더라도 시스템 상 제약은 없었다.
근데 MSA 환경에서는 아예 물리적으로 서비스가 떨어져 있기 때문에 DDD를 강제해야하는 부분이 존재한다.
그래서 아래 Popit에서 본 글을 정리해보고자 한다.
서로 다른 Aggregate에서 연관관계 사용하지 않기
예를 들어 User와 Post라는 도메인이 서로 분리되어있는 마이크로 서비스라고 가정하자.
User는 여러개의 Post를 작성할 수 있기 때문에 1:N 관계에 놓여있다.
Post 테이블에는 당연히 '작성자'라는 User 정보가 담겨져 있긴 하지만 그 외에 User에 대한 정보를 상세히 가져오는 경우가 필요할 수도 있다. 이런 경우 1:N 연관관계(ManyToOne)가 설정되어있다면 객체 그래프 탐색을 이용해 User 정보를 들고오면 된다. 하지만 서로 분리된 도메인끼리는 연관관계 설정이 불가능하다.
위의 글에서 이런 경우에 어떻게 설계해야 하는지 잘 알려주고 있다.
요약하자면 도메인 주도 설계에서 각 도메인은 Aggregate(조립물)로 구성되는데, 해당 조립물에 있는 ID를 통해 다른 조립물을 참조하라는 뜻이다.
Aggregate에 외부 조립물에 대한 ID만 넣고 외부 조립물 그 자체를 넣는 연관관계는 설정하지 말아야 한다.
다른 조립물을 반복적으로 탐색 하는 경우더라도 연관관계 구조에서 자주 쓰였던 hibernate.default_batch_fetch_size를 사용하면 한꺼번에 탐색하게 되니 트래픽 문제는 발생하지 않을 것이다.
쉽게 말해서 서로 다른 Aggregate라면 연관관계를 쓰지말라는 뜻이다.
물론 연관관계를 쓰지 않을 경우 객체 그래프 탐색을 못한다는 단점이 있다.
객체 그래프 탐색이란 통상적으로 Lazy Loading 이용해서 다른 테이블을 탐색하던 것을 말한다. 객체 그래프 탐색을 사용하면 관련된 엔티티를 한 번의 조회로 가져올 수 있으며 이는 편리한 객체 지향적인 접근 방식을 제공한다. 하지만 이 경우에는 엔티티 간의 결합도가 높아질 수 있고, 도메인 모델의 독립성과 일관성이 감소할 수 있다.
그러므로 도메인 주도 설계와 연관 관계 설정은 Trade-Off 관계에 놓여져 있다고 볼 수 있다.
Aggregate에 값 객체(Value Object)를 컬럼으로 추가하기
우선 값 객체란 Immutable한 객체를 말한다.
즉, Setter와 같은 기능은 없고 생성자로서만 값이 결정되는 값으로서만 기능을 하는 객체를 말한다.
위의 예시로 든 것은 Post 서비스에서 User 서비스가 필요한 경우를 다시 생각해보자.
결국에 ID를 통해 다른 Aggregate을 조회하더라도 문제가 발생하는 점이 존재한다.
User 서비스를 호출해야하는 성능상의 문제도 있을 뿐더러 Post가 User를 부르는 강결합 문제도 발생한다.
그렇기 때문에 Post에 User 정보를 값 객체로 저장한다면 추후에 User 서비스를 조회할 필요 없이 Post 만으로 조회가 가능하다.
모놀리스 아키텍처와 같은 강결합 시스템을 추구하는 것 보다 코드 중복, 데이터 중복, 데이터 복제를 허용하는 것이 더 유지보수에 효율적이라는 이야기다.
Post에는 User 객체를 불변한 상태(작성자 ID는 항상 동일해서)인 값 객체로 넣을 수 있지만 반대로 User에 Post 정보를 값 객체로 넣는 것은 불가능하다. 왜냐면 Post는 값 객체가 아니기 때문이다. Post는 언제든지 수정될 수 있기 때문에 불변 특성을 가지는 값 객체를 넣는 것은 불가능하다.
아래는 위 링크에서 가져온 값 객체를 넣는 법이다.
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue
private Long id;
@Embedded
private ShippingAddress shippingAddress;
//...
}
@Embeddable
public class ShippingAddress {
private String zipCode;
private String recipient;
private String address;
private ShippingAddress() {
}
public ShippingAddress(String zipCode, String address, String recipient) {
this.zipCode = zipCode;
this.address = address;
this.recipient = recipient;
}
public String getZipCode() {
return zipCode;
}
public String getAddress() {
return address;
}
public String getRecipient() {
return recipient;
}
}
주문시 필요한 쿠폰, 배송지 등에 대한 스냅샷은 스냅샷이라는 말 자체가 의미하는 것처럼 바뀌지 않는 값이다.
이것은 값 객체로 설계할 수 있음을 의미한다.
쿠폰, 배송지를 따로 도메인으로 만드는 것보다 변하지 않는 값이라면 값 객체를 사용해 JSON을 삽입한다든가 하는 형태로 개발할 수 있다.
@Entity
public class PurchaseOrder {
@Id
@GeneratedValue
private Long id;
private Long orderId;
private Long channelId;
@Enumerated(EnumType.STRING)
private PurchaseOrderStatus status;
@OneToMany(mappedBy = "purchaseOrder", cascade = CascadeType.ALL)
private List<OrderLineItem> orderLineItems = new ArrayList<>();
// 주문 결제 정보
@Lob
private String payment;
// 주문 당시 회원 정보
@Lob
private String customer;
// 쿠폰
@Lob
private String coupons;
// 주문 배송지
@Lob
private String shippingAddress;
// ...
}
@Entity
public class OrderLineItem {
@Id
@GeneratedValue
private Long id;
private String productId;
private OrderLineItemStatus status;
// 주문 당시 상품 정보
@Lob
private String product;
// LineItem 쿠폰
@Lob
private String coupons;
@ManyToOne
@JoinColumn(name = "po_id", referencedColumnName = "id")
private PurchaseOrder purchaseOrder;
// ...
}
값 객체가 아닐 경우
User와 Post의 관계라고 해도 Post가 항상 User에 대한 값 객체를 넣을 수 있는 상황이 생기는 것은 아니다.
불변한 값은 고객 ID에 한정된 것이지 만약에 게시글이 닉네임 혹은 이름에 관한 부분이 필요하다면 해당 컬럼은 값 객체가 아니기 때문에 다시 Post에서 User를 조회해야한다.
즉, User와 Post를 어쩔 수 없이 같이 봐야하는 상황이 생긴다면 느슨한 결합을 유지하기 위해 AggregateService라는 중간 마이크로서비스를 만들어서 해당 서비스에서 User, Post 마이크로서비스 데이터의 정보가 업데이트 될 때마다 메시지큐로 받아서 저장해놓고 사용할 수도 있다고 한다.
'📖 ORM' 카테고리의 다른 글
JPA Entity에서 필드에 this를 쓴 것과 안 쓴 것의 차이 (1) | 2023.07.27 |
---|---|
Kotlin에서 JPA 사용법 (0) | 2023.06.21 |
Fetch Join 사용 시 조건문(Condition) 올바르게 사용하기 - 실습으로 배우는 JPA 4편 (0) | 2022.11.03 |
@Transactional 사용 시 자기 호출(Self-Invocation) 이슈 - 실습으로 배우는 JPA 3편 (2) | 2022.07.06 |
findAll()에 관한 N+1 테스트 - 실습으로 배우는 JPA 2편 (0) | 2022.06.30 |