이 포스팅은 내 생각을 담은 내용이므로 참고만 해주세요!
💋 Repository VS DAO
아래와 같은 레거시 코드를 만났다.
CartItem 테이블에 접근하기 위해 만들어진 CartItemDao에서 다른 member, product 테이블을 join한 쿼리를 보내고 있었다.
1. Repository에서, 하나의 테이블에만 접근하는 DAO에, 여러 번의 쿼리를 보내고, 반환된 entity 정보를 바탕으로 도메인 객체로 조립
[CartItemDao]
@Repository
public class CartItemDao {
private final JdbcTemplate jdbcTemplate;
public CartItemDao(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public List<CartItem> findByMemberId(Long memberId) {
String sql = "SELECT cart_item.id, cart_item.member_id, member.email, product.id, product.name, product.price, product.image_url, cart_item.quantity " +
"FROM cart_item " +
"INNER JOIN member ON cart_item.member_id = member.id " +
"INNER JOIN product ON cart_item.product_id = product.id " +
"WHERE cart_item.member_id = ?";
return jdbcTemplate.query(sql, new Object[]{memberId}, (rs, rowNum) -> {
String email = rs.getString("email");
Long productId = rs.getLong("product.id");
String name = rs.getString("name");
int price = rs.getInt("price");
String imageUrl = rs.getString("image_url");
Long cartItemId = rs.getLong("cart_item.id");
int quantity = rs.getInt("cart_item.quantity");
Member member = new Member(memberId, email, null);
Product product = new Product(productId, name, price, imageUrl);
return new CartItem(cartItemId, quantity, product, member);
});
}
// ...
}
왜인지 어색하게 느껴져서 (근거 없음) 아래와 같이, Repository 계층에서 세 가지 dao를 호출하도록 변경했다.
2. DAO에서 여러 테이블을 join하는 쿼리를 보내고, DAO에서 도메인 객체를 조립해서 반환
[CartItemRepository]
public List<CartItem> findByMemberId(final Long memberId) {
final List<CartItemEntity> entity = cartItemDao.findByMemberId(memberId);
return entity.stream()
.map(it -> new CartItem(
it.getId(),
it.getQuantity(),
productDao.getProductById(it.getProductId()).toProduct(),
memberDao.getMemberById(it.getMemberId()).toMember()
))
.collect(Collectors.toList());
}
join하는 대신, cartItemDao, productDao, memberDao로 세 번의 쿼리를 보내고 있었다.
각 쿼리는 모두 딱 하나의 테이블에만 접근하도록 만들어졌다.
[CartItemDao]
public List<CartItemEntity> findByMemberId(Long memberId) {
String sql = "SELECT * FROM cart_item WHERE member_id = :member_id";
final Map<String, Long> parameter = Map.of("member_id", memberId);
return namedParameterJdbcTemplate.query(sql, parameter, CART_ITEM_ENTITY_ROW_MAPPER);
}
[ProductDao]
public ProductEntity getProductById(Long productId) {
String sql = "SELECT * FROM product WHERE id = ?";
return jdbcTemplate.queryForObject(sql, new Object[]{productId}, (rs, rowNum) -> {
String name = rs.getString("name");
int price = rs.getInt("price");
String imageUrl = rs.getString("image_url");
return new ProductEntity(productId, name, price, imageUrl);
});
}
[MemberDao]
public MemberEntity getMemberById(Long id) {
String sql = "SELECT * FROM member WHERE id = ?";
List<MemberEntity> members = jdbcTemplate.query(sql, new Object[]{id}, new MemberRowMapper());
return members.isEmpty() ? null : members.get(0);
}
두 방법을 아래와 같이 요약할 수 있을 것 같다.
1. Repository에서 여러 번 쿼리 보내기
2. DAO에서 join을 포함한 쿼리 보내기
두 방법 중 어떤 것이 더 좋은 방법인지에 대한 고민이 생겼다.
✔ Repository에서 여러 번 쿼리 보내기
- 하나의 DAO가 하나의 테이블에만 접근해서 로직이 비교적 간단해진다.
- 여러 번의 쿼리를 보내야 하기 때문에 Transactional에 대한 고민이 추가되고, 데이터베이스에서 제공하는 최적화에 대한 이용이 어려워진다.
✔ DAO에서 join을 포함한 쿼리 보내기
- join과 같이 데이터베이스에서 제공하는 최적화를 사용할 수 있다.
- DAO에서 도메인 객체를 직접 조립하기 때문에, 도메인과 엔티티가 명확하게 구분되지 않는다. (도메인 객체를 entity처럼 사용하게 된다.)
- 어떤 DAO에서 다른 테이블을 join 할지에 대한 기준을 명확히 해야 할 것이다.
- 현재 코드는 CartItem에 Product와 Member의 정보를 모두 포함하고 있다고 보고 작성된 것 같다.
✔ 다른 방법: Repository에서 JdbcTemplate을 이용해서 다른 테이블을 조인해서 한 번에 CartItem을 조립한다.
Repository는 도메인 객체를 저장하는 방법이 추상화된 것이라고 생각하는데,
내부에서 SQL문을 작성한다는 것은 구체적인 데이터베이스 종류를 이미 특정해버린 것이기 때문에 좋지 않다고 생각한다!
💋 도메인 객체와 entity 객체를 분리하는 이유는 뭘까?
현재 미션에서는 데이터베이스 설계와 도메인 설계가 동시에 들어가기 때문에 entity와 도메인 구조를 크게 다르지 않게 가져갈 수 있었다.
하지만 서비스가 커지고, 새로운 비즈니스 로직이 추가되는 상황을 상상해 본다면, 데이터베이스 entity는 이미 존재하는 데이터로 고정된 형식을 이미 가지고 있을 것이다. 도메인은 완전히 새로운 비즈니스를 창조할 수 있는 영역이므로, 데이터베이스에 저장된 형태와 다르게 될 확률이 아주 높다고 생각한다. 따라서 이런 경우에는 두 가지를 분리하고 싶지 않더라도 분리할 수 밖에 없게 될 것 같다.