실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발
스프링 핵심 원리 - 기본편 - 인프런
스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다. 초급 프레임워크 및 라이브러리 웹 개발 서버 개발 Back-End Spring 객체지향 온
www.inflearn.com
강의를 들으며 생각 정리 + "자바 ORM 표준 JPA 프로그래밍" 책 참고
주문, 주문상품 엔티티 개발
상품 주문, 주문 내역 조회, 주문 취소 기능을 구현하기 위해 엔티티에 메서드를 추가한다.
상품 주문시 상품의 재고가 감소하고, 취소 시 재고가 증가해야 한다.
생성 메서드
이처럼 주문과 주문상품의 경우 생성 시 재고를 관리해야 하고 다른 여러 엔티티들과 연관관계를 맺어줘야 한다. 이렇게 엔티티를 생성하기 복잡하다면 새롭게 생성 메서드를 만드는 것이 좋다.
<Order.java>
//==생성 메서드==//
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;
}
<OrderItem.java>
//==생성 메서드==//
public static OrderItem createOrderItem(Item item, int orderPrice, int count) {
OrderItem orderItem = new OrderItem();
orderItem.setItem(item);
orderItem.setOrderPrice(orderPrice);
orderItem.setCount(count);
item.removeStock(count);
return orderItem;
}
주문상품을 생성할 때 removeStock 메서드로 재고가 줄어들게 설정했다.
+) 정적 팩토리 메서드 : 이처럼 객체를 생성하는 메소드를 만들고 static으로 선언해 캡슐화하는 기법이다.
이 때, 다른 방법(new 등)으로 객체를 생성하지 못하게 protected 생성자를 따로 명시해주는 것이 좋다.
취소 로직
주문을 취소할 때, 상품의 재고가 증가해야 한다.
<Order.java>
//==비즈니스 로직==//
/**
* 주문 취소
*/
public void cancel() {
if (delivery.getStatus() == DeliveryStatus.COMP) {
throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
}
this.setStatus(OrderStatus.CANCEL);
for (OrderItem orderItem : orderItems) {
orderItem.cancel();
}
}
<OrderItem.java>
//==비즈니스 로직==//
public void cancel() {
getItem().addStock(count);
}
addStock()으로 주문 수량(count)만큼 재고를 증가시킨다.
조회 로직
<Order.java>
//==조회 로직==//
/**
* 전체 주문 가격 조회
*/
public int getTotalPrice() {
int totalPrice = 0;
for (OrderItem orderItem : orderItems) {
totalPrice += orderItem.getTotalPrice();
}
return totalPrice;
}
<OrderItem.java>
//==조회 로직==//
/**
* 주문상품 전체 가격 조회
*/
public int getTotalPrice() {
return getOrderPrice() * getCount();
}
주문상품 각각의 전체 가격을 더해서 총 주문 가격을 조회하는 로직이다.
영속성 전이
앞서 설계한 주문과 주문상품 도메인에서 주문이 cascade(영속성 전이)로 주문상품과 연관관계를 맺는다.
@OneToMany(mappedBy = "order", cascade = ALL)
private List<OrderItem> orderItems = new ArrayList<>();
주문을 persist하면 주문상품도 따라서 persist될 수 있다. 이처럼 주문이 주문상품의 생명주기를 관리함으로써 도메인을 좀 더 덜 복잡하게 설계한다.
+) 주문과 같이 한 엔티티 그룹을 대표하는 엔티티를 도메인 주도 설계에서는 aggreagate root라고 한다.
주문 리포지토리 개발
@Repository
@RequiredArgsConstructor
public class OrderRepository {
private final EntityManager em;
public void save(Order order) {
em.persist(order);
}
public Order findOne(Long id) {
return em.find(Order.class, id);
}
}
일반적인 리포지토리의 형태이다.
주문 서비스 개발
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final MemberRepository memberRepository;
private final ItemRepository itemRepository;
/**
* 주문
*/
@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());
//주문상품 생성
OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);
//주문 생성
Order order = Order.createOrder(member, delivery, orderItem);
//주문 저장
orderRepository.save(order);
return order.getId();
}
//취소
@Transactional
public void cancelOrder(Long orderId) {
//주문 엔티티 조회
Order order = orderRepository.findOne(orderId);
//주문 취소
order.cancel();
}
}
상품 주문과 상품 취소 두 로직으로 나뉜다.
상품 주문
주문은 회원, 주문상품, 배송 엔티티와 연관되어 있다. 여기서 주문상품과 배송은 영속성 전이(cascade)설정을 했기 때문에 회원 리포지토리를 의존하여 회원 ID에 맞는 회원을 조회할 수 있도록 한다.
또한 주문상품은 상품과 연관되어 있기 때문에 상품 리포지토리를 의존하여 상품 ID에 맞는 상품을 조회할 수 있도록 한다.
+) 주문은 여러 주문상품을 가질 수 있지만 여기서는 주문상품을 하나만 넣을 수 있게 했다.
앞서 만든 생성 메서드를 사용해 주문을 생성하고 영속화한다.
상품 취소
미리 만들어둔 비즈니스 로직인 cancel을 호출하는 것으로 서비스 로직은 끝난다.
cancel 로직 처럼 데이터가 변경되면(재고가 올라간다) JPA는 알아서 update 쿼리를 날려주기 때문에 서비스 로직은 엔티티 비즈니스 로직을 호출하는 정도의 기능만 하고 변경되는 데이터와 가장 가까운 엔티티에서 비즈니스 로직을 관리할 수 있다. 이를 도메인 주도 설계라고 한다.
만약 쿼리를 일일이 작성해줘야 한다면 서비스에 비즈니스 로직을 전부 추가해야 하는 어려움이 있다.
주문 기능 테스트
테스트는 상품 주문, 상품 주문시 재고수량 초과, 주문 취소 3가지를 확인한다.
테스트를 포함한 전체 코드는 깃허브에 등록했다.
주문 검색 기능 개발
주문 검색은 동적 쿼리를 사용한다. 아무 조건도 없다면 전체 주문을 조회하고, 회원 이름과 주문 상태(ORDER, CANCEL)에 따라 동적으로 관련 주문 조회를 하는 기능이다.
여기서는 OrderSearch 클래스를 만들어 주문 검색의 조건으로 설정했다.
<OrderSearch.java>
@Getter @Setter
public class OrderSearch {
private String memberName; //회원 이름
private OrderStatus orderStatus; //주문 상태[ORDER,CANCEL]
}
동적 쿼리는 여러 방법이 있지만 여기서는 직접 쿼리 문자열을 상황에 따라 생성하는 방식과 JPA Criteria를 사용하는 방식을 주문 리포지토리에 추가했다.
쿼리 문자열을 상황에 따라 생성
public List<Order> findAllByString(OrderSearch orderSearch) {
String jpql = "select o From Order o join o.member m";
boolean isFirstCondition = true;
//주문 상태 검색
if (orderSearch.getOrderStatus() != null) {
if (isFirstCondition) {
jpql += " where";
isFirstCondition = false;
} else {
jpql += " and";
}
jpql += " o.status = :status";
}
//회원 이름 검색
if (StringUtils.hasText(orderSearch.getMemberName())) {
if (isFirstCondition) {
jpql += " where";
isFirstCondition = false;
} else {
jpql += " and";
}
jpql += " m.name like :name";
}
TypedQuery<Order> query = em.createQuery(jpql, Order.class)
.setMaxResults(1000); //최대 1000건
if (orderSearch.getOrderStatus() != null) {
query = query.setParameter("status", orderSearch.getOrderStatus());
}
if (StringUtils.hasText(orderSearch.getMemberName())) {
query = query.setParameter("name", orderSearch.getMemberName());
}
return query.getResultList();
}
단순히 상황에 따라 알맞은 문자열들을 붙이는 방식이다. 자세히 볼 필요가 없는게 너무 복잡하기 때문에 거의 사용하지 않는 방식이다.
JPA Criteria
/**
* JPA Criteria
*/
public List<Order> findAllByCriteria(OrderSearch orderSearch) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Order> cq = cb.createQuery(Order.class);
Root<Order> o = cq.from(Order.class);
Join<Object, Object> m = o.join("member", JoinType.INNER);
List<Predicate> criteria = new ArrayList<>();
//주문 상태 검색
if (orderSearch.getOrderStatus() != null) {
Predicate status = cb.equal(o.get("status"), orderSearch.getOrderStatus());
criteria.add(status);
}
//회원 이름 검색
if (StringUtils.hasText(orderSearch.getMemberName())) {
Predicate name =
cb.like(m.<String>get("name"), "%" +
orderSearch.getMemberName() + "%");
criteria.add(name);
}
cq.where(cb.and(criteria.toArray(new Predicate[criteria.size()])));
TypedQuery<Order> query = em.createQuery(cq).setMaxResults(1000);
return query.getResultList();
}
이전 방식보다는 코드의 크기가 많이 줄었다. 그러나 JPA Criteria는 JPA 표준 스펙이지만 실무에서 사용하기에 너무 복잡하고 한눈에 어떤 쿼리가 나가는지 알아보기도 어렵다. 결국 다른 대안이 필요하다.
일반적으로 동적 쿼리에 대해서는 QueryDSL을 많이 사용한다. QueryDSL에 관해서는 나중에 알아보자.
'java > jpa' 카테고리의 다른 글
[JPA] API 개발 기본 (0) | 2021.05.17 |
---|---|
[JPA] 웹 계층 개발 (0) | 2021.04.20 |
[JPA] 상품 도메인 개발 (0) | 2021.02.13 |
[JPA] 회원 도메인 개발 (0) | 2021.02.12 |
[JPA] 애플리케이션 구현 준비 (0) | 2021.02.11 |