코드 그라데이션

shop 구현 (13) 주문 기능 구현 본문

Spring/SpringShop

shop 구현 (13) 주문 기능 구현

완벽한 장면 2023. 7. 21. 08:41

주문 기능 구현

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
}

 


상품 주문 로직을 그림으로 정리하면 이러한 모양이 된다.

728x90
Comments