일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- exclusive lock
- dfs
- querydsl
- PS
- 일대다
- 즉시로딩
- 다대일
- JPQL
- 낙관적락
- 백트래킹
- FetchType
- 유니크제약조건
- CHECK OPTION
- 연결리스트
- SQL프로그래밍
- 힙
- 스토어드 프로시저
- shared lock
- eager
- BOJ
- 동적sql
- 다대다
- 스프링 폼
- execute
- 비관적락
- 지연로딩
- 이진탐색
- fetch
- 연관관계
- 데코레이터
- Today
- Total
흰 스타렉스에서 내가 내리지
[스프링부트와JPA활용1]섹션6. 주문 도메인 개발 본문
강의1. 주문, 주문상품 엔티티 개발
@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order {
...
...
...
//==생성 메서드==//
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;
}
//==비즈니스 로직==//
/**
* 주문취소
*/
public void cancel(){
if (delivery.getStatus() == DeliveryStatus.COMP){
throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
}
this.setStatus(OrderStatus.CANCEL);
for(OrderItem orderItem : orderItems){
orderItem.cancel();
}
}
//==조회 로직==//
/**
* 전체 주문 가격 조회
*/
public int getTotalPrice(){
// int totalPrice = 0;
// for(OrderItem orderItem : orderItems){
// totalPrice += orderItem.getTotalPrice();
// }
return orderItems.stream()
.mapToInt(OrderItem::getTotalPrice)
.sum();
}
}
주문 취소( cancel() ): 주문 취소시 사용한다. 주문 상태를 취소로 변경하고 주문상품에 주문 취소를 알린다.
@Entity
@Getter @Setter
public class OrderItem {
...
...
...
//==생성 메서드==//
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;
}
//==비즈니스 로직==//
public void cancel(){
getItem().addStock(count);
}
//==조회 로직==//
/**
* 주문상품 전체 가격 조회
*/
public int getTotalPrice(){
return getOrderPrice() * getCount();
}
}
생성 메서드( createOrderItem() ): 주문 상품, 가격, 수량 정보를 사용해서 주문상품 엔티티를 생성한다. 그리고 item.removeStock(count) 를 호출해서 주문한 수량만큼 상품의 재고를 줄인다.
강의2. 주문 리포지토리 개발
강의3. 주문 서비스 개발
OrderService.java
1.주문
코드를 잘 보면, delivery를 따로 db에 save 해주지도 않았고, orderItem도 JPA에 넣는 과정 없이 42번줄에서 createOrder를 했다.
어째서 가능?
cascade (ALL) 덕분에 그렇다.
order를 persist 하면, OrderItem도 강제로 persist 날려준다.
마찬가지로 delivery도 강제로 persist 해준다.
따라서 45번줄처럼 하나만 저장을 해줘도 item과 delivery가 자동으로 persist 된것이다.
@NoArgsConstructor 어노테이션을 사용하면, OrderItem orderItem1 = new OrderItem();의 사용을 막을 수 있다.
객체 생성 방식을 제한한다. 직접 생성하지 못하도록.
2. 취소
JPA를 활용하면, 엔티티안에서 데이터를 바꾸면, JPA가 알아서 변경포인트를 찾아서 데이터베이스에 업데이트 쿼리를 자동으로 날려준다 (dirty checking).
cancel() 함수 호출 시 stockQuantity 등 바뀌는 로직이 있었는데, SQL을 그대로 사용했다면 전부 다 일일이 작성해야 했었다.
3. 검색
+++++참고:
주문 서비스의 주문과 주문 취소 메서드를 보면 비즈니스 로직 대부분이 엔티티에 있다.
서비스 계층 은 단순히 엔티티에 필요한 요청을 위임하는 역할을 한다. 이처럼 엔티티가 비즈니스 로직을 가지고 객체 지향의 특성을 적극 활용하는 것을 도메인 모델 패턴이라 한다.(JPA 사용 시)
반대로 엔티티에는 비즈니스 로직이 거의 없고 서비스 계층에서 대부분 의 비즈니스 로직을 처리하는 것을 트랜잭션 스크립트 패턴이라 한다.(보통 SQL 사용 시)
강의4. 주문 기능 테스트
package jpabook.jpashop.service;
import ...
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class OrderServiceTest {
@Autowired
EntityManager em;
@Autowired
OrderService orderService;
@Autowired
OrderRepository orderRepository;
@Test
public void 상품주문() throws Exception{
//given
Member member = createMember();
Book book = createBook("시골 JPA", 10000, 10);
int orderCount = 2;
//when
Long orderId = orderService.order(member.getId(), book.getId(), orderCount);
//then
Order getOrder = orderRepository.findOne(orderId);
Assert.assertEquals("상품 주문시 상태는 ORDER", OrderStatus.ORDER, getOrder.getStatus());
Assert.assertEquals("주문한 상품 종류 수가 정확해야 한다.", 1, getOrder.getOrderItems().size());
Assert.assertEquals("주문 가격은 가격 * 수량이다", 10000*2, getOrder.getTotalPrice());
Assert.assertEquals("주문 수량 만큼 재고가 줄어야 한다", 8, book.getStockQuantity());
}
@Test(expected = NotEnoughStockException.class)
public void 상품주문_재고수량초과() throws Exception{
//given
Member member = createMember();
Item item = createBook("시골 JPA", 10000, 10);
int orderCount = 11;
//when
orderService.order(member.getId(), item.getId(), orderCount);
//then
Assert.fail("재고 수량 부족 예외가 발생해야 한다. ");
}
@Test
public void 주문취소() throws Exception{
//given
Member member = createMember();
Item item = createBook("시골 JPA", 10000, 10);
int orderCount = 2;
Long orderId = orderService.order(member.getId(), item.getId(), orderCount);
//when
orderService.cancelOrder(orderId);
//then
Order getOrder = orderRepository.findOne(orderId);
Assert.assertEquals("주문 취소시 상태는 CANCEL 이다.", OrderStatus.CANCEL, getOrder.getStatus());
Assert.assertEquals("주문이 취소된 상품은 그만큼 재고가 증가해야 한다.", 10, item.getStockQuantity());
}
private Book createBook(String name, int price, int stockQuantity) {
Book book = new Book();
book.setName(name);
book.setPrice(price);
book.setStockQuantity(stockQuantity);
em.persist(book);
return book;
}
private Member createMember() {
Member member = new Member();
member.setName("회원1");
member.setAddress(new Address("서울", "강가", "123-123"));
em.persist(member);
return member;
}
}
상품주문() : 상품주문이 정상 동작하는지 확인하는 테스트다. Given 절에서 테스트를 위한 회원과 상품을 만들고 When 절에서 실제 상품을 주문하고 Then 절에서 주문 가격이 올바른지, 주문 후 재고 수량이 정확히 줄었 는지 검증한다.
주문취소() : 주문을 취소하려면 먼저 주문을 해야 한다. Given 절에서 주문하고 When 절에서 해당 주문을 취소했다. Then 절에서 주문상태가 주문 취소 상태인지( CANCEL ), 취소한 만큼 재고가 증가했는지 검증한다.
강의5. 주문 검색 기능 개발 - OrderSearch
JPA에서 동적 쿼리를 어떻게 해결해야 하는가?
JPQL 쿼리를 문자열로 생성하기는 번거롭고, 실수로 인한 버그가 충분히 발생할 수 있다.
JPA Criteria는 JPA 표준 스펙이지만 실무에서 사용하기에 너무 복잡하다. 결국 다른 대안이 필요하다. 많 은 개발자가 비슷한 고민을 했지만, 가장 멋진 해결책은 Querydsl이 제시했다. Querydsl 소개장에서 간 단히 언급하겠다. 지금은 이대로 진행하자.
'Spring' 카테고리의 다른 글
[스프링핵심원리-기본편] BeanFactory와 ApplicationContext (0) | 2022.08.08 |
---|---|
[스프링핵심원리-기본편] 스프링 빈 조회 (0) | 2022.08.07 |
[스프링부트와JPA활용1]섹션5. 상품 도메인 개발 (0) | 2022.08.06 |
[스프링핵심원리-기본편] SOLID 원칙 (0) | 2022.08.05 |
[스프링부트와JPA활용]섹션4. 회원 도메인 개발 (0) | 2022.08.02 |