코드 그라데이션
shop 구현 (13) 주문 기능 구현 본문
주문 기능 구현
ItemService
@Transactional(readOnly = true)
public Page<MainItemDto> getMainItemPage(ItemSearchDto itemSearchDto, Pageable pageable){
return itemRepository.getMainItemPage(itemSearchDto, pageable);
}
ItemRepositoryCustom
public interface ItemRepositoryCustom {
Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable);
Page<MainItemDto> getMainItemPage(ItemSearchDto itemSearchDto, Pageable pageable);
}
여기서 서비스가 하는 일은 리포지토리 한 줄 부르고 끝.
이런 경우에는 컨트롤러에서 직접 리포지토리를 부르는 것도 괜찮은 선택일 수 있다.
정리하자면 이런 구조이다.
ItemFormDto에서는 아이템 이미지 + 아이템 정보 = 하나의 폼.
@Transactional(readOnly = true)
public ItemFormDto getItemDtl(Long itemId){
List<ItemImg> itemImgList = itemImgRepository.findByItemIdOrderByIdAsc(itemId);
//DB에서 데이터를 가지고 옵니다.
List<ItemImgDto> itemImgDtoList = new ArrayList<>();
// 이미지 가져오고
for(ItemImg itemimg : itemImgList){
ItemImgDto itemImgDto = ItemImgDto.of(itemimg);
itemImgDtoList.add(itemImgDto); //먼저 디티오에 넣어준다.
}
// 아이템 가져와서
Item item = itemRepository.findById(itemId).orElseThrow(EntityNotFoundException::new);
ItemFormDto itemFormDto = ItemFormDto.of(item); // Entity -> Dto
itemFormDto.setItemImgDtoList(itemImgDtoList); // 아이템도 디티오를 만들어 넣어주고.
return itemFormDto;
}
<head>
<meta name="_csrf" th:content="${_csrf.token}"/>
<meta name="_csrf_header" th:content="${_csrf.headerName}">
</head>
=> Csrf관련 방어를 위한 token, header를 메터에 적용 Security 확인
<button type="button" class="btn btn-primary btn-lg" onclick="order()">주문하기</button>
이벤트 리스너 개념과 매우 유사. 어떤 이벤트를 듣고 있다가 어딘가에 담아서 작동할 수 있도록 붙여줌.
잘못된 요청일 경우에 검증부터 하고 그 결과를 먼저 돌려줘야 한다.
그냥 로직만 처리한다면 빠르기야 하겠지만 문제 생길 가능성 매우 커짐.
매개변수(혹은 클래스 파라미터)에 @Valid를 붙이고, 그 결과를 BindingResult에 담는다.
Principal 은 시큐리티 관련. 현재 로그인한 유저 정보를 담고 있다.
자바스크립트에서 보안 검증 방법
리턴 타입에 붙는 어노테이션은 없다.
@PostMapping(value = "/order")
public @ResponseBody
ResponseEntity order(@RequestBody @Valid OrderDto orderDto, BindingResult bindingResult,
Principal principal){
이거는
@PostMapping(value = "/order")
@ResponseBody
public ResponseEntity order(@RequestBody @Valid OrderDto orderDto, BindingResult bindingResult,
Principal principal){
}
이것과 동일하다.
에러 처리 로직도 항상 비슷하다고 생각하면 된다.
에러가 있으면 에러에다가 정보를 담아서 반환하다.
그런데 저번 에러처리와는 조금 다르다.
저번에는 그냥 리다이렉트 시켰었는데,
아래는 @ResponseBody 붙어있다.
이게 붙어있으면 동작을
우리가 리턴하는 게 실제로 Strimg으로 넘어간다.
안그러면 원래 html 문서로 인식했다.
String email = principal.getName(); // 현재 로그인된 유저의 이메일을 추출
Long orderId;
try {
orderId = orderService.order(orderDto,email);
}catch (Exception e){
return new ResponseEntity<String>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<Long>(orderId, HttpStatus.OK);
오더서비스가 주문을 하면 새로운 order가 테이블의 새로운 row로 추가가 되고 order 테이블의 key 값은 orderId니까 OrderId를 주나보다 하고 유추를 할 수 있다.
그 과정에서 예외가 발생했다면 그 때 상태 코드를 bad Request 를 준다.
OrderService 로직
오더아이템은 여러가지 있을 수 있으니 리스트를 쓴다.
createOrderItem은 스태틱이었다. 객체 생성하면 항상 일일이 넣어줘야 하니까 귀찮은데, 스태틱 메서드를 만들어놓으면
of도 객체생성 방법(이펙티브 자바 에 자세히 나와있음)
그래서 이거는
내가 id를 주고 가져온 대상이 Optional item인데 이건 item을 감쌌다는 것이고, 거기에 orElseThrow를 붙였으니까 Item이 없으면 EntityNotFoundException을 던지고, 있으면 그것을 item 변수에 담는다는 말.
궁금증. Optiona> Item인데 왜 이 코드에서는 Item item으로 받고 있을까?
일반적으로 우변이 다 끝난 결과를 좌변에 담는다.
itemRepository.findById 여기까지 진행했을 때 리턴은 Optional item이다. 그런데 뒤에 달린 것 까지가 한 문장이다.
결국 itemRepository.findById(orderDto.getItemId()) 이 메서드의 리턴의
.orElseThrow(EntityNotFoundException::new); 메서드의 리턴이 Item item에 담긴다는 소리.
헷갈리면 쪼개자.
Optional<Item> x = itemRepository.findById(orderDto.getItemId())
Item item = x.orElseThrow(EntityNotFoundException::new); 이렇게!
그런데 Member 객체도 없을 수도 있는데 왜 아래는 Optional처리가 안 되었을까
이건 JPA에서 제공하는 메서드가 아닌 내가 직접 구현한 것이기 때문
직접 구현했다 = 리턴도 취향대로 만들 수 있다.
리포지토리에 가보면
public interface MemberRepository extends JpaRepository<Member,Long> {
Member findByEmail(String email);
}
Member라고 박았는데 결과가 없으면 jpa는 리턴에 null을 그냥 담아서 준다.
<리턴에 null을 그냥 담아서 준다>
public static void main(String[] args) {
Member a = findByEmail("abc@google.com");
}
이런 상황에서 이 이메일에 해당하는 멤버가 존재하지 않는다면,
우선 여기서 NPE는 발생하지 않는다.
Null인 대상에 아직 접근하지 않았기 때문.
현재는 그냥 a 가 null인 상태일 뿐.
없으니까 그냥 Member a에 null을 jpa는 넣어줄 뿐이다.
그런데 문제는
a.~~~ 하는 순간 NPE가 발생한다.
정리하자면 정확히 findByEmail("abc@google.com") 이 결과가 null이고 이 결과가 a에 담긴다는 것이고,
이 자체로는 NPE가 발생하지 않는데,
이 a에 점 찍고 뭔가를 하려고 하면 그때부터 NPE가 발생하게 된다.
Optional은 단독의 객체다.
그래서 다시 정리하자면
Member member = memberRepository.findByEmail(email);
if (member == null) {
throw new MemberNotFoundException();
} 이렇게까지 해주면
이것과
Item item = x.orElseThrow(EntityNotFoundException::new);
이것은 의미적으로 완전히 동일한 게 된다.
public static OrderItem createOrderItem(Item item, int count){
OrderItem orderItem = new OrderItem(); //주문 아이템 객체생성
orderItem.setItem(item);
orderItem.setCount(count);
orderItem.setOrderPrice(item.getPrice());
item.removeStock(count);
return orderItem;
}
이렇게 해주게 되면 new를 안 불러도 되어서 가독성 매우 향상
이게 지금 orderItem 코드인데, item에서 있는 메서드를 활용해서 재고를 감소시켜버림.
public void removeStock(int stockNumber){
int restStock = this.stockNumber - stockNumber;
if(restStock<0){
throw new OutOfStockException("상품의 재고가 부족합니다.(현재 재고 수량: "+this.stockNumber+")");
}
this.stockNumber = restStock;
}
상품 주문 로직을 그림으로 정리하면 이러한 모양이 된다.
'Spring > SpringShop' 카테고리의 다른 글
shop 구현 (15) 주문 취소하기 (0) | 2023.07.22 |
---|---|
shop 구현 (14) 주문 이력 조회 (0) | 2023.07.22 |
shop 구현 (12) 상품 상세 페이지 (0) | 2023.07.20 |
shop 구현 (11) 메인 페이지 만들기 (0) | 2023.07.19 |
shop 구현 (10) 상품 관리하기 페이지 (0) | 2023.07.15 |