
(누가 뭘 책임질거냐...)
카카오페이 여신코어 DDD(Domain Driven Design, 도메인 주도 설계)로 구축하기
카카오페이에서 DDD를 어케 썼나... 하고 보던 도중

????
Entity가 다 같은 Entity 아니야?
엔티티 개념을 잘못 알고있었나...
정확하게 한번 알아보고 싶어서 찾아보았다.
사람이 생각하는 개념이나 정보 단위와 같은 현실 세계의 대상체
실세계에 존재하는 유형 혹은 무형 정보의 대상이며, 서로 구별이 되는 하나하나의 대상을 의미
출처: 엔티티-나무위키
예를들어 학교라는 곳에서는,
과목이라는 엔티티가 존재할 수 있고 엔티티는 인스턴스의 집합으로 나타나게 된다.
과목은 국어, 수학, 사회, 과학, 영어 등 이러한 요소들의 집합이다.
이러한 요소들을 인스턴스라고 한다.
우리가 지금까지 주로 말했던 엔티티는, DB 엔티티다.
(DB 입장에서는 그냥 데이터 Record)
비즈니스 도메인 내의 개념을 나타내는 객체
User, Customer, Order, Product 등 도메인 엔티티가 될 수 있다.
/**
* 주문 도메인
*/
@Getter
public class Order {
private String id;
private List<OrderLine> orderLines;
private OrderState state;
private Money totalAmounts;
private ShippingInfo shippingInfo;
}
이들의 주요 특징은 다음과 같다
위에 코드에서 내가 생략을 했지만,
/**
* 배송 정보 설정
*/
private void setOrderLines(List<OrderLine> orderLines) {
if (isEmptyLOrderLines(orderLines)) {
throw new IllegalStateException(ErrorMessages.NO_ORDER_LINE.getMsg());
}
this.orderLines = orderLines;
calculateTotalAmounts();
}
private boolean isEmptyLOrderLines(List<OrderLine> orderLines) {
return orderLines == null || orderLines.isEmpty();
}
/**
* 수신자 설정
*/
private void setShippingInfo(ShippingInfo shippingInfo) {
if (isBlankShippingInfo(shippingInfo)) {
throw new IllegalStateException(ErrorMessages.NO_SHIPPING_INFO.getMsg());
}
this.shippingInfo = shippingInfo;
}
private boolean isBlankShippingInfo(ShippingInfo shippingInfo) {
return shippingInfo == null || shippingInfo.isBlank();
}
이런식으로 배송 정보 설정하는 로직과, 받는 사람 설정하는 코드가 저 Order Class에 들어가 있다.
private String id;
이처럼 고유한 Id값이 들어간다.
// 결제 후
public void payment() {
if (state != OrderState.PAYMENT_WAITING) {
throw new IllegalStateException(ErrorMessages.ALREADY_PAID.getMsg());
}
this.state = OrderState.PREPARING;
}
// 운송 중
public void shipped() {
if (state != OrderState.PREPARING) {
throw new IllegalStateException(ErrorMessages.NOT_READY.getMsg());
}
this.state = OrderState.SHIPPED;
}
// 배송 중
public void startDelivery() {
if (state != OrderState.SHIPPED) {
throw new IllegalStateException(ErrorMessages.NOT_SHIPPED.getMsg());
}
this.state = OrderState.DELIVERING;
}
너무 길어질거 같아서 나머지 코드는 생략했다.
이처럼 "Order가 언제 생성되고, 언제 준비중이 되고, 언제 취소되거나 끝나는가?" 처럼
Order 도메인의 생명이 주기되어 있다.
DB 테이블과 1:1로 매핑되는 클래스
데이터베이스에 저장되고 ORM을 통해 관리되는 객체다.
import jakarta.persistence.*;
import java.util.*;
@Entity
@Table(name = "orders")
public class OrderJpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@OneToMany(fetch = FetchType.LAZY)
private List<OrderLine> orderLines;
@Enumerated(EnumType.STRING)
private OrderState state;
@Embedded
private Money totalAmounts;
@Embedded
private ShippingInfo shippingInfo;
}
@Embedded
JPA에서 값 타입(VO, Value Object)을 엔티티에 포함시킬 때 사용
우리가 지금까지 DB에 테이블을 생성하거나, DB에 있던 값을 매핑할 경우에 사용한다.
단, 상속 받는 구현체거나, 테이블 내에 존재하지 않는 칼럼을 가져서는 안된다.
검색하다가 좋은 질문글이 있길래 찾아봤다.
출처: [인프런] Entity와 VO에 대해..
질문 내용이
이러한 Order 도메인이 있다고 가정하자.
특정 속성을 설명하는 객체로, 객체를 값(Value)처럼 쓸 수 있다.
저 예시에서는
Address, Money, OrderLine, Product, Receiver, ShippingInfo 가 VO에 해당된다.
각 객체를 나타내기 위해서는 기존 자료형을 사용하는게 아닌, 자료형을 만들어 사용하는 것이 좋다.
(VO도 그 중 하나)
// 안좋은 예시
private String id;
// 좋은 예시
public class OrderId {
private String id;
}
private OrderId id;
근데... 왜 불변성이어야 할까?
이건 다른 포스팅에서 다루겠다.
[DDD] VO(Value Object)
/**
* 주문 도메인
*/
@Getter
public class Order {
private String id;
private List<OrderLine> orderLines;
private OrderState state;
private Money totalAmounts;
private ShippingInfo shippingInfo;
}
Entity는 고유한 식별자(Id)에 의해 정해지는 객체다.
값이 똑같아도, 식별자(Id) 값이 다르면 다른 객체다.
엔터티는 일반적으로 가변적이다.
시간이 지남에 따라 상태가 변할 수 있지만, Id는 불변이다.
엔티티에도 식별자는 불변성을 가지고 있다.
엔티티와 VO는 단순하게 불변성을 가지고 구분하는 것이 아닌, 식별자의 유무로 판단된다.
자 결론부터 말하면 다르다!!
하지만 이 둘을 통합하느냐?, 분리를 하느냐?는 프로젝트의 규모와 성격 등 여러가지를 종합해서 결정하면 된다고 한다.
도메인 Entity는 주로 비즈니스 로직을 포함하고 있다.
비즈니스 규칙과 도메인 개념을 표현하는 데 중점을 둔다.
반면, 영속성(JPA) Entity는 DB의 상호작용을 위해 설계되며, 데이터 저장과 조회에 중점을 둔다.
테이블에 매핑되는 영속 데이터 객체로, 테이블의 레코드를 나타낸다.
그러나,
도메인 Entity의 식별자(Id)는 반드시 PK가 되는 것은 아니라고 한다.
[인프런] domain과 repository 구현 상세를 분리하고자 할 때 entity 디자인
(이분이 없었다면 아마 백엔드를 시작을 못했을지도 모른다....)
김영한 강사님께서 답변해주신 말을 한번 정리해보겠다.
DDD 관점에서 보면, 도메인 모델은 JPA에 의존하지 않는 게 이상적이다.
XML 매핑으로 JPA 의존성을 완전히 제거할 수는 있지만, 개발 공수가 너무 크고 비효율적이다.
즉, "분리하게 되면 개발하는 비용이 더 커진다는 뜻"이다.
엔티티와 도메인 객체를 분리하지 않고, JPA 엔티티를 그대로 도메인 모델로 사용하는 것이 실무적(실용적)으로 합리적이다.
라고 말씀하시는 것 같다.
클린 아키텍처의 원칙을 살펴보면, Entities를 향해 안쪽 방향으로 향한다.
즉, 안쪽 계층은 바깥 계층을 알지 못한다.
만약에 분리를 안하고 도메인 엔티티에 @Entity, @Table 같은 JPA 애노테이션을 붙이면,
Entities(도메인)가 Frameworks(DB)에 의존하게 되고 이는 클린 아키텍처를 위반하게 된다.
안쪽에 있던 Entites가 DB를 의존하게 되는 현상이 되어버리기 때문이다.
일단 내 생각을 한번 적어보겠다.
카카오페이가 분리를 선택한 이유는 "서비스의 규모와 복잡성 때문"이 아닐까 싶다.
카카오페이 같은 대규모 서비스는
이런 환경에서는 도메인 Entity와 JPA Entity를 통합해서 사용하게 되면,
테스트 코드 작성이 어렵고, 비즈니스 로직과 영속성 로직이 뒤섞여서 협업이 복잡해진다고 생각한다.
즉, 단기적인 개발 속도보다 장기적인 유지보수성과 확장성을 고려해
클린 아키텍처의 원칙을 따라 분리하지 않았나 싶다.
이 글을 쓰면서 여러가지를 반성했다.
첫 번째는,
부트캠프 때, 이런걸 알아보고 한번 적용해볼걸...
사실 이 글을 간다하게 비교만 하고 포스팅을 할려고 했다.
근데 계속 알아보면 알아볼수록 새로운 것들이 자꾸 나오게 된다.
그러다보니까 "어 이게 왜 이렇지?", "이건 또 뭐지?" 라는 질문할게 엄청 많아졌다.
좀 이런거 찾아보고 멘토님께 질문좀 할걸...;
두 번째는, 진짜 무식하게 개발했던 것 같다.
도메인, 책임, 원칙 등 이런거 다 상관없이 그냥 서비스는 서비스, Entity는 DB 테이블, Controller는 그냥 뭐 응답해주는구나~ 라고 생각하고 기능 구현만 생각한 것 같다.
DDD를 공부하면서 많은 도움과 반성이 되고 있다.
싱글 프로젝트를 해야겠다.
리펙토링이든 뭐든...