장바구니 미션의 주요 기능은 주문을 하는 것이다.
주문할 때, 여러 상품을 주문할 수 있고, 수량도 지정할 수 있다.
따라서 주문이라는 Order, 주문 상품이라는 OrderItem 객체로 나뉠 수 있다.
그리고 사용자는 주문에 대한 기록을 확인해야 하므로 데이터베이스에 주문을 저장해야 한다.
이때, 단순히 Order와 OrderItem을 Dao를 통해 저장하고 조회하게 된다면 여러 문제점이 발생한다.
Order 객체와 OrderItem 객체는 다음과 같이 구현되어 있다.
class Order {
private final Long id;
private final Member orderer;
private final List<OrderItems> orderItems;
...
}
class OrderItem {
private final Long id;
private final Product product;
private final int qunatity;
private final long price;
...
}
그리고 Order와 OrderItem의 데이터베이스 구조는 다음과 같다.
CREATE TABLE orders (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
member_id BIGINT NOT NULL,
FOREIGN KEY (member_id) REFERENCES member(id)
);
CREATE TABLE order_item (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
price BIGINT NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id),
FOREIGN KEY (product_id) REFERENCES product(id)
);
그리고 주문을 생성하고 저장하는 것은 서비스 레이어에서 처리한다.
class OrderService {
...
public Long createOrder(OrderRequest orderRequest, Long memberId) {
Member member = memberDao.findById(memberId);
List<OrderItem> orderItems = getOrderItems(orderRequest);
Order order = new Order(null, member, orderItems);
Long orderId = orderDao.save(order);
orderItemDao.saveAll(orderId, orderItems)
return orderId;
}
}
주문을 저장할 때, 단순히 주문만 저장하는 것이 아니라, 주문 상품도 저장해야 한다.
객체 모델에서는 Order가 OrderItem들을 필드로 가진다.
하지만 데이터베이스 모델은 반대로 OrderItem이 Order를 참조한다. 또한 관계형 데이터베이스는 테이블로 데이터를 구조화하기 때문에 객체처럼 주소값으로 참조하는 것이 아닌, Id 같은 식별자로 참조한다.
따라서 주문 상품을 저장할 때 주문의 식별자가 필요하다.
이러한 구조적인 차이 때문에 객체 모델과 관계형 데이터베이스 모델 사이에서 불일치가 발생한다.
이것을 객체-관계 불일치라고 한다.
이러한 객체-관계 불일치는 상품을 조회할 때도 문제를 발생시킨다.
class OrderService {
...
public OrderResponse findOrder(Long orderId, Long memberId) {
List<OrderItem> orderItems = orderItemDao.findAllByOrderId(orderId);
Order order = orderDao.findById(orderId); // ???
...
}
}
Order 객체를 생성할 때는 List\<OrderItem>과 Member가 필요하다.
Order 객체는 OrderDao를 통해 데이터베이스에서 불러와야 한다.
Member는 1:1 관계이기 때문에 JOIN을 사용해서 한 번에 불러올 수 있다.
하지만 OrderItem은 1:N 관계이기 때문에 단순히 JOIN을 해서 불러올 수 없다.
어떻게 이 문제를 해결해야 할까?
이 문제를 가장 쉽게 해결하려면 Order에 setter를 만들면 된다.
class OrderService {
...
public OrderResponse findOrder(Long orderId, Long memberId) {
Order order = orderDao.findById(orderId);
List<OrderItems> orderItems = orderItemDao.findAllByOrderId(orderId);
order.setOrderItems(orderItems);
...
}
}
하지만 객체에 setter를 구현하는 것은 캡슐화를 위반하고, Order 객체를 불변으로 설계할 수 없다.
두 번째 방법은 OrderDao에서 OrderItem에 대한 SQL문을 추가로 작성한다.
class OrderDao {
private final RowMapper<Order> OrderRowMapper = ...
private final RowMapper<OrderItem> OrderItemRowMapper = ...
...
public Order findById(Long orderId) {
String orderSql = ...
Order order = ...
String orderItemSql = ...
List<OrderItem> orderItems = ...
return new Order(orderId, order.getMember(), orderItems);
}
}
OrderDao는 Order에 대한 RowMapper만 존재해야 하지만, OrderItem에 대한 RowMapper를 가지고 있고, OrderItem에 대한 SQL 쿼리를 추가로 가지고 있다.
이렇게 된다면 OrderDao는 SRP 원칙을 지킨다고 할 수 있을까?
문제를 해결하고 원하는 기능을 수행하기 위해 죄악을 저질러야만 한다.
죄악을 저지르지 않고 문제를 해결하는 방법은 Entity라는 데이터베이스 테이블을 그대로 바라보는 객체를 만들어, 이후 도메인 모델로 변환하는 방법을 생각했다.
Entity는 다음과 같은 구조를 가진다.
class OrderEntity {
private final Long id;
private final Long memberId;
...
}
그리고 다음과 같이 사용할 수 있다.
class OrderService {
...
public OrderResponse findOrder(Long orderId, Long memberId) {
OrderEntity orderEntity = orderDao.findById(orderId);
List<OrderItem> orderItems = orderItemDao.findAllByOrderId(orderId);
Member member = memberDao.findById(orderEntity.getMemberId());
Order order = new Order(orderId, member, orderItems);
...
}
}
그런데 이렇게 OrderEntity를 Service에서 사용하는 게 올바를까?
OrderEntity는 데이터베이스 테이블을 그대로 바라보는 객체이다.
만약 관계형 데이터베이스가 아닌 NoSQL과 같은 비관계형 데이터베이스로 바뀌게 된다면 OrderEntity는 필요가 없어진다.
즉, Service가 특정 데이터베이스의 구현을 알고 있는 OrderEntity를 의존하게 되면서, 자연스럽게 데이터베이스 구현체에 의존하게 된다.
Order 객체를 OrderEntity로 변환하고, OrderEntity를 Order로 변환하는 역할을 다른 객체로 위임하면 해결할 수 있다.
해당 역할을 하는 객체를 Repository로 만들고 사용하였다.
class OrderRepository {
...
public Order findById(Long orderId) {
OrderEntity orderEntity = orderDao.findById(orderId);
List<OrderItem> orderItems = orderItemDao.findAllByOrderId(orderId);
Member member = memberDao.findById(orderEntity.getMemberId());
return new Order(orderId, member, orderItems);
}
}
class OrderService {
private final OrderRepository orderRepository;
...
public OrderResponse findOrder(Long orderId, Long memberId) {
Order order = orderRepository.findById(orderId);
order.checkOrderer(memberId);
...
}
}
또한, Service에서 Order, OrderItem을 저장하는 과정도 Repository에 위임할 수 있다.
class OrderService {
...
public Long createOrder(OrderRequest orderRequest, Long memberId) {
Member member = memberDao.findById(memberId);
List<OrderItem> orderItems = getOrderItems(orderRequest);
Order order = new Order(null, member, orderItems);
Order saveOrder = orderRepository.save(order);
// orderItemDao.saveAll(orderId, orderItems)
return saveOrder.getId();
}
}
추가로 Repository를 인터페이스로 설계한다면, Service 계층에서 특정 데이터베이스 구현체에 관한 의존성을 확실하게 끊어낼 수 있다.