김영한님의 실전! 스프링 부트와 JPA 활용1 편을 듣다가 처음 보는 내용을 발견했다. 그동안 내가 쓰던 방식과 너무 달라서 기록으로 남겨본다. 너무 신기해
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "Category_ID")
private Long id;
@Column(nullable = false)
private String tag; // ex) 고양이 / 고양이모래 등등 ~
@ManyToOne
@JoinColumn(name="User_ID", nullable = false)
private Seller seller;
위와 같이 Seller와 Category는 양방향 연관관계를 맺고 있고, cascadeType은 All이다.
양방향 연관관계, Cascade.ALL 은 객체의 생성 주기와 삭제 주기가 완전히 부모 타입에 종속적이어야 한다.
OneToMany는 저 경우에 사용 할 필요가 없었다.
Category는 Seller에 완전히 종속적인데도 불구하고 본인의 repository를 가진다.
public interface CategoryRepository extends JpaRepository<Category, Long> {
@Query("delete from Category c where c.seller.id = :sellerId")
@Modifying(clearAutomatically = true)
void deleteAllBySellerId(@Param("sellerId") Long sellerId);
}
위와 같이. 그리고 이것을 어디서 쓸까? 바로 Seller를 지울 때 쓴다.
@Transactional
public MessageResponseDto deleteSeller(User user) {
Seller seller = sellerRepository.findByUsername(user.getUsername()).orElseThrow(
() -> new IllegalArgumentException("그런 사람 없습니다")
);
// 쿼리가 긴 이유 -> 조인해서 날라가니까. left outer join 입니다.
orderRepository.deleteAllByStoreName(seller.getStoreName());
categoryRepository.deleteAllBySellerId(seller.getId());
productRepository.deleteAllByUsername(seller.getUsername());
// 위에놈들은 내가 직접 쿼리짜서 하나만 날라가지만
sellerRepository.delete(seller);
// 이놈은 JPA 에서 상속 구현할 때의 한계. 자식 객체 조회/삭제등에 쿼리 두번씩 날라감.(큰 단점은 아니라고 하긴 함~)
return new MessageResponseDto("삭제 완료");
}
내가 봐도 너무 더럽다. 셀러 하나 삭제하는데 이렇게까지 해야하나?
@Entity
@Table(name = "orders")
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
@Id
@GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "member_id")
private Member member;
@JsonIgnore // 이거 왜 썼을까?
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@JsonIgnore
@OneToOne(fetch = LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
}
위와 같이 member, delivery, orderItems 등에 연관관계를 맺고 있다. 내가 썼던 방식대로 모든 친구들에게 repository를 넣어줄까? 아니다! (MemberRepository는 꼭 필요하므로 쓰시긴 한다.)
갓영한님은 다음과 같이 "생성 매서드"를 사용하셨다.
//==생성 메서드==//
public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems) {
Order order = new Order();
order.setMember(member);
order.setDelivery(delivery);
for (OrderItem orderItem : orderItems) {
order.addOrderItem(orderItem);
}
order.setStatus(OrderStatus.ORDER);
order.setOrderDate(LocalDateTime.now());
return order;
}
위에서 보는 바와 같이, Order라는 객체를 설정 해 줄 때 member, deliver, orderitems 등등을 모두 한번에 설정 할 수 있게 해뒀다. 이것은 어떻게 쓰이느냐? 바로 다음과 같이 쓰인다.
@Transactional
public Long order(Long memberId, Long itemId, int count) {
//엔티티 조회
Member member = memberRepository.findOne(memberId);
Item item = itemRepository.findOne(itemId);
//배송정보 생성
Delivery delivery = new Delivery();
delivery.setAddress(member.getAddress());
delivery.setStatus(DeliveryStatus.READY);
//주문상품 생성
OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);
//주문 생성
Order order = Order.createOrder(member, delivery, orderItem);
//주문 저장
orderRepository.save(order);
return order.getId();
}
위와 같이 order 객체를 생성 할 때 save를 통한 persist 호출은 "단 한번만 일어난다". 진짜 기똥차고 기가 맥히다...너무 좋아
연관관계 설정 시에 "연관관계 편의 매서드" 나, 생성 매서드 등등의 것들은 엔티티 내에서 설정되어서 사용되면 매우 깔끔해진다.
위와 같이 order가 가지는 책임을 정확히 분리하면 코드가 너무너무 편해지고 깔끔해진다. 생성 -> 수정 -> 삭제 모두 편해진다.
반성 할 점은, 내가 JPA를 계속 써오면서 너무 테이블 관점으로 생각했다는 것이다. JPA는 ORM이고, 자바 엔티티를 테이블로 최대한 바꿔주는 역할을 한다. 자바 객체를 어떻게 잘 만지느냐도 굉장히 굉장히 중요할 듯 하다.