
상품 도메인 개발 – 엔티티에 비즈니스 로직을 넣는 이유
도메인 모델 중심 설계 관점에서 상품을 어떻게 다뤄야 하는지에 초점을 둔다.
상품(Item)은 단순히 name, price, stockQuantity를 가지는 데이터 객체가 아니다.
재고 관리라는 비즈니스 로직을 직접 가지고 있는 도메인 객체다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
@Getter
@Setter
public abstract class Item {
@Id @GeneratedValue
@Column(name = "item_id")
private Long id;
private String name;
private int price;
private int stockQuantity;
@ManyToMany(mappedBy = "items")
private List<Category> categories = new ArrayList<>();
// == 비즈니스 로직 == //
public void addStock(int quantity) {
this.stockQuantity += quantity;
}
public void removeStock(int quantity) {
int restStock = this.stockQuantity - quantity;
if (restStock < 0) {
throw new NotEnoughStockException("need more stock");
}
this.stockQuantity = restStock;
}
}
왜 재고 로직을 엔티티에 넣었을까?
많이 하는 실수가 있다.
재고 차감 로직을 서비스에서 처리하는 것
그렇게 되면:
하지만 재고 감소는 상품의 핵심 책임이다.
즉, 도메인 모델에 비즈니스 규칙을 캡슐화한 것이다.
public class NotEnoughStockException extends RuntimeException {
public NotEnoughStockException(String message) {
super(message);
}
}
재고가 부족하면 removeStock() 내부에서 바로 예외를 던진다.
이 설계의 의미는 명확하다.
재고 부족은 “시스템 오류”가 아니라 “비즈니스 규칙 위반”이다.
따라서:
@Repository
@RequiredArgsConstructor
public class ItemRepository {
private final EntityManager em;
public void save(Item item) {
if (item.getId() == null) {
em.persist(item);
} else {
em.merge(item);
}
}
public Item findOne(Long id) {
return em.find(Item.class, id);
}
public List<Item> findAll() {
return em.createQuery("select i from Item i", Item.class)
.getResultList();
}
}
여기서 가장 중요한 부분은 merge()다.
하지만 실무에서는
변경 감지(dirty checking)를 사용하는 것이 훨씬 안전하다.
merge()는 예측하기 어렵고, 잘못 쓰면 의도하지 않은 업데이트가 발생할 수 있다.
이 강의에서는 구조 이해용으로 쓰지만,
실무에서는 가급적 merge()를 직접 쓰지 않는다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
@Transactional
public void saveItem(Item item) {
itemRepository.save(item);
}
public List<Item> findItems() {
return itemRepository.findAll();
}
public Item findOne(Long itemId) {
return itemRepository.findOne(itemId);
}
}
서비스는 거의 리포지토리에 위임만 한다.
이게 이상해 보일 수도 있다.
“서비스에 로직이 없는데 왜 만들지?”
이 구조의 의미는 명확하다.
즉, 계층 책임 분리가 명확하다.
상품 도메인 구현은
이 네 가지를 이해하는 단계다.
출처
스프링 부트와 JPA 활용 (김영한, 인프런, 2019)