domain을 분리하자!
라는 말을 어디선가 많이 들어봐왔었다. 하지만 도메인을 어떻게 어떤 기준으로 뭘 보고 나눠야할까? 라는 고민도 있을것이다.
도메인을 나누는 방법을 검색하면
등등 다양한 도메인을 나누는 방법들이 존재한다. 하지만 이 말들은 나에겐 너무 추상적으로 들렸다.
우선 비지니스에 따라 나눠라! 라고 하는데 주문과 상품만 봐도 주문과 상품을 도메인으로 나눴을 때 주문속에 상품의 정보가 들어가게 되고 결제는 주문의 내용을 알고 있어야 해서 서로 연관되어 버린다. 그럼 주문, 상품, 결제는 모두 하나의 묶음인가? 아니다 도메인의 역할, 개념, 책임에 따르면 모두 나눠야 하는 것이 맞다.
이 처럼 그저 개념만으로 도메인을 나누려고 들면 도메인에 대해 깊은 이해가 있지 않는한 저런 경계에 대해 애매모호 해질 뿐이다.
그래서 나는 우선 도메인을 말로만 하지말고 몸으로 해보기로 했다. 가장 처음으로 domain이라는 폴더 안에 entity 폴더를 만들고 entity를 사용하며 response에 필요한 vo 객체만 정의하여 사용하고 있었다.
이런식으로 entity와 반환용 vo만 존재했었다. 그러던 중 테스트 코드 강의 하나를 들으며 도메인의 책임을 더욱 분리하는 방법에 대해 알게되었고 구조는 다르지만 내 프로젝트에 당장 도입할 수 있는 구조로 model이라는 패키지를 하나 더 생성하게 되었다.
model이라는 개념은 기존에 통용되던 명칭을 따라 사용하게 된것입니다. 저의 개인적인 명칭 사용일수도 있으니 참고만 바랍니다!
model은 크게 어려운 개념은 아니고 entity 객체와 domain 객체의 책임을 완전히 분리하는 것이 목적이다. 기존에 코드는 entity를 가져와 직접 entity 내부에 로직을 사용하여 가격을 할인해주거나 조회수를늘려주는 등 entity 자체를 가지고 코딩하는 방법이였다.
model을 사용하는 것은 model은 entity와 완벽하게 동일한 객체로써 entity에서 사용되던 로직들의 책임을 담당한다. 그리고 entity는 model 객체를 반환하는 것과 model 객체를 통해 entity를 반환하는 것을 담당한다.
코드로 보면
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "product")
public class ProductEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "product_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "seller_id")
private SellerEntity seller;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private CategoryEntity category;
private String mainTitle;
private String mainExplanation;
private String productMainExplanation;
private String productSubExplanation;
private int originPrice;
private int price;
private String purchaseInquiry;
private String origin;
private String producer;
private String mainImage;
private String image1;
private String image2;
private String image3;
private Long viewCnt;
@Enumerated(EnumType.STRING)
private ProductStatus status;
private int ea;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
@OneToMany(mappedBy = "product", fetch = FetchType.LAZY)
private List<OptionEntity> optionList = new ArrayList<>();
@Enumerated(EnumType.STRING)
private ProductType type;
public static ProductEntity from(Product product) {
ProductEntity productEntity = new ProductEntity();
productEntity.id = product.getId();
productEntity.seller = SellerEntity.from(product.getSeller());
productEntity.category = CategoryEntity.from(product.getCategory());
productEntity.mainTitle = product.getMainTitle();
productEntity.mainExplanation = product.getMainExplanation();
productEntity.productMainExplanation = product.getProductMainExplanation();
productEntity.productSubExplanation = product.getProductSubExplanation();
productEntity.originPrice = product.getOriginPrice();
productEntity.price = product.getPrice();
productEntity.purchaseInquiry = product.getPurchaseInquiry();
productEntity.origin = product.getOrigin();
productEntity.producer = product.getProducer();
productEntity.mainImage = product.getMainImage();
productEntity.image1 = product.getImage1();
productEntity.image2 = product.getImage2();
productEntity.image3 = product.getImage3();
productEntity.viewCnt = product.getViewCnt();
productEntity.status = product.getStatus();
productEntity.ea = product.getEa();
productEntity.createdAt = product.getCreatedAt();
productEntity.modifiedAt = product.getModifiedAt();
productEntity.type = product.getType();
return productEntity;
}
public Product toModel() {
return Product.builder()
.id(id)
.seller(seller.toModel())
.category(category.toModel())
.mainTitle(mainTitle)
.mainExplanation(mainExplanation)
.productMainExplanation(productMainExplanation)
.productSubExplanation(productSubExplanation)
.originPrice(originPrice)
.price(price)
.purchaseInquiry(purchaseInquiry)
.origin(origin)
.producer(producer)
.mainImage(mainImage)
.image1(image1)
.image2(image2)
.image3(image3)
.viewCnt(viewCnt)
.status(status)
.ea(ea)
.createdAt(createdAt)
.modifiedAt(modifiedAt)
.type(type)
.build();
}
}
entity는 다음과 같이 모델을 만들고 entity를 반환하는 것만 신경쓰면 된다.
model 객체를 보자
@Getter
public class Product {
private Long id;
private Seller seller;
private Category category;
private String mainTitle;
private String mainExplanation;
private String productMainExplanation;
private String productSubExplanation;
private int originPrice;
private int price;
private String purchaseInquiry;
private String origin;
private String producer;
private String mainImage;
private String image1;
private String image2;
private String image3;
private Long viewCnt;
private ProductStatus status;
private int ea;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
private List<Option> optionList = new ArrayList<>();
private ProductType type;
@Builder
public Product(Long id, Seller seller, Category category, String mainTitle,
String mainExplanation,
String productMainExplanation, String productSubExplanation, int originPrice, int price,
String purchaseInquiry, String origin, String producer, String mainImage, String image1,
String image2, String image3, Long viewCnt, ProductStatus status, int ea,
LocalDateTime createdAt, LocalDateTime modifiedAt, List<Option> optionList,
ProductType type) {
this.id = id;
this.seller = seller;
this.category = category;
this.mainTitle = mainTitle;
this.mainExplanation = mainExplanation;
this.productMainExplanation = productMainExplanation;
this.productSubExplanation = productSubExplanation;
this.originPrice = originPrice;
this.price = price;
this.purchaseInquiry = purchaseInquiry;
this.origin = origin;
this.producer = producer;
this.mainImage = mainImage;
this.image1 = image1;
this.image2 = image2;
this.image3 = image3;
this.viewCnt = viewCnt;
this.status = status;
this.ea = ea;
this.createdAt = createdAt;
this.modifiedAt = modifiedAt;
this.optionList = optionList;
this.type = type;
}
public void addViewCnt() {
this.viewCnt++;
}
public void minusEa(int ea, Option option) {
if(this.type == ProductType.OPTION) {
List<Option> optionList = this.optionList;
optionList.stream().filter(o -> o.getId().equals(option.getId()))
.findFirst()
.ifPresent(o -> o.minusEa(ea)
);
} else {
this.ea -= ea;
}
}
}
model은 다음과 같이 로직 자체에 사용되는 method들을 담당한다.
model과 entity를 분리함으로써 entity의 책임이 확 줄어들었다.
이제 더이상 test code에서 entity를 테스트 할 필요가 없기에 entity 객체를 대신 model 객체인 순수 자바(POJO) 코드를 테스트 할수 있게 되었다.
여기서 entity를 테스트할 필요가 없다는 건 entity 자체에 로직이 없기 때문에 entity를 model 객체 생성과 entity 반환만 테스트하면 entity로써 책임과 역할은 끝이다. 나머지 로직은 모두 model 객체에서 test를 진행하면 된다.
model을 정의하며 자연스럽게 도메인이 어디에 의존하고 있는지 찾아진다.
entity를 정의할때는 각 table관의 연관관계와 외래키등 여러가지를 신경쓰게 된다. 그러다 보면 entity가 어디에 의존하고 있는지에 대해서는 쉽게 알아채기가 어렵다. 하지만 model은 순서 자바 객체로 로직만 신경쓰면 되다 보니 해당 객체가 어떤 객체들에 의존하고 있는지 쉽게 파악이 된다.
예를 들어 OrderItem의 경우 설계시에는 파악하지 못했던 Product에 대한 의존성이 있었다. Product에 대한 의존성이 문제야? 라고 생각할 수도 있지만 먼저 패키지 구조를 살펴보면
orderItem과 product는 전혀 다른 도메인에 존재하고 있다. 현재는 크게 문제가 되지 않겠지만 이후에 도메인별로 분리가 되어 버린다면 서로 데이터를 주고 받는게 큰 문제가 될 수도 있다.
또한 해당 객체를 설계하면서 느낀것은 product 자체를 의존해버리면 누군가 주문을 했고 주문 이후에 판매자가 해당 상품의 정보를 수정해버린다면? 주문자가 주문한 product와 판매자가 판매하는 product는 전혀 다른 상품이 되어버린다. 그래서 나는 해당 model로 분리하며 orderProduct라는 table을 새로 만들기로 했다. 아예 도메인별로 책임을 분리시키기 위해서
공통으로 사용하는 부분부터 도메인을 분리하는 것이 나중에 다른 도메인 분리시에도 더 효율적이다. 예를들어 member 도메인과 product 도메인 중 공통으로 사용되는 도메인은 누가봐도 member다. 그럼 member 부터 도메인을 분리하고 가는 것이 product를 나눌때 더 편하다.