주문 기능 구현

jihan kong·2023년 2월 28일
0
post-thumbnail

지금까지 상품을 등록하고, 고객이 등록된 상품을 볼 수 있도록 구현했다. 이번에는 고객이 등록된 상품을 주문하는 기능을 구현하기로 했다.


⚙️ Item Order Logic ⚙️

상품 주문 또한, 기존 상품 등록이나 수정과 같이 Entity 를 구성하고 ServiceController 를 설계해야한다. 일단은 그전에 앞서, 주문에 대한 메카니즘이 어떻게 이루어지는지를 생각해봤다.

고객이 주문 Form을 통해 수량을 입력하고 상품을 주문하면, 처음 등록한 상품의 재고에서 주문 수량 만큼 재고를 감소시켜 주문을 성공시키는 로직이다. 그런데 만약 재고가 없어 주문 수량만큼 주문할 수 없다면 이를 예외처리하는 과정이 필요하다.

기본 주문 흐름을 바탕으로 이제 실제로 주문 기능을 수행하기 위해 생성해야하는 Entity 들과 비즈니스단에서의 로직들을 생각해보았다.

1. 재고부족 -> Exception 정의

com.shop 패키지 아래에 exception 패키지를 생성한 후 RuntimeException을 상속받는 OutOfStockException 클래스를 생성했다.


OutOfStockException.java

package com.shop.exception;

public class OutOfStockException extends RuntimeException {

    public OutOfStockException(String message) {
        super(message);
    }
}

❗왜 RuntimeException 으로 예외처리를 했는가?
-> 실행 중에 발생하며 시스템 환경적으로 혹은 Input 값이 잘못된 경우, 또는 의도적으로 프로그래머가 잡아내기 위한 조건 등에 부합할 때 사용하기에 알맞은 예외처리이다. 우리 프로젝트에서는 "재고가 없다" 라는 조건으로 예외처리를 실행하였다.


2. Item Entity -> 재고 감소 로직 추가

상품을 주문했을 때, 상품의 재고를 감소시키는 로직을 구현했다. Entity 클래스에 직접 메소드로 작성했다.

Item.java

package com.shop.entity;

import com.shop.exception.OutOfStockException;

@Entity
@Table(name="item")
@Getter
@Setter
@ToString
public class Item extends BaseEntity {

	// 코드 생략

    public void removeStock(int stockNumber) {
        int restStock = this.stockNumber - stockNumber;
        if(restStock < 0) {
            throw new OutOfStockException("상품의 재고가 부족합니다." +
                    "(현재 재고 수량: " + this.stockNumber + ")");
        }
        this.stockNumber = restStock;
    }

3. OrderItem 구현

주문할 상품과 수량을 세팅해주고, 주문 수량만큼 상품의 재고 수량을 감소시키는 메소드를 작성했다.

OrderItem.java

package com.shop.entity;

// ..import 생략

@Entity
@Getter
@Setter
public class OrderItem extends BaseEntity {
	// 코드 생략

    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;
    }

    public int getTotalPrice() {
        return orderPrice * count;
    }
 }

4. Order Entity 구현

주문 상품 객체를 이용해 주문 객체를 만드는 메소드를 만든다.

Order.java

package com.shop.entity;

// ..import 생략

@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order extends BaseEntity{

	// ..코드 생략
    
    public void addOrderItem(OrderItem orderItem) {  
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }

    public static Order createOrder(Member member, List<OrderItem> orderItemList) {
        Order order = new Order();
        order.setMember(member);
        for(OrderItem orderItem : orderItemList) {
            order.addOrderItem(orderItem);
        }
        order.setOrderStatus(OrderStatus.ORDER);
        order.setOrderDate(LocalDateTime.now());
        return order;
    }

    public int getTotalPrice() {
        int totalPrice = 0;
        for(OrderItem orderItem : orderItems) {
            totalPrice += orderItem.getTotalPrice();
        }
        return totalPrice;
    }
}
  • orderItems 에 주문 상품 정보들을 담아줌.
  • orderItem.setOrder(this); -> Order 엔티티와 OrderItem 엔티티가 양방향 참조 관계이기 때문에 orderItem 객체에도 order 객체 세팅
  • order.setMember(member); -> 상품을 주문한 회원 정보 세팅
  • order.setOrderStatus(OrderStatus.ORDER); -> 주문 상태를 'ORDER' 로 세팅
  • getTotalPrice() 메소드 -> 총 주문 금액을 구함

5. OrderDto 구현

상품 상세 페이지(Form) 에서 주문할 상품의 ID와 수량을 전달받기 위해선 Dto가 필요하다. 주문관련 Dto를 설계하였다. 최소 주문 수량은 1개, 최대 주문 수량은 999개로 하였다.

OrderDto.java

package com.shop.dto;

import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

@Getter
@Setter
public class OrderDto {

    @NotNull(message = "상품 아이디는 필수 입력 값입니다.")
    private Long itemId;

    @Min(value = 1, message = "최소 주문 수량은 1개 입니다.")
    @Max(value = 999, message = "최대 주문 수량은 999개 입니다.")
    private int count;
}

6. 실제 주문 로직 구현 - OrderService

본격적인 주문 로직을 구현하기 위해 먼저 OrderService 를 생성했다.
주문 상품과 회원 정보가 필요하기 때문에 주입해주었고, 상품 Entity와 주문 수량을 받아서 주문 상품 Entity를 생성하는게 핵심이었다.

orderService.java

package com.shop.service;

import com.shop.dto.OrderDto;
import com.shop.entity.*;
import com.shop.repository.ItemRepository;
import com.shop.repository.MemberRepository;
import com.shop.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityNotFoundException;
import java.util.ArrayList;
import java.util.List;

@Service
@Transactional
@RequiredArgsConstructor
public class OrderService {

    private final ItemRepository itemRepository;
    private final MemberRepository memberRepository;
    private final OrderRepository orderRepository;

    public Long order(OrderDto orderDto, String email) {
        Item item = itemRepository.findById(orderDto.getItemId())
                .orElseThrow(EntityNotFoundException::new);
        Member member = memberRepository.findByEmail(email);

        List<OrderItem> orderItemList = new ArrayList<>();
        OrderItem orderItem = OrderItem.createOrderItem(item, orderDto.getCount());
        orderItemList.add(orderItem);

        Order order = Order.createOrder(member, orderItemList);
        orderRepository.save(order);

        return order.getId();
    }
}
  • itemRepository 에서 주문 상품을 조회
  • memberRepository 에서 회원 정보를 조회
  • OrderItem.createOrderItem(item, orderDto.getCount()); 에서 주문할 상품 Entity와 수량을 이용하여 주문 상품 Entity를 생성
  • Order order = Order.createOrder(member, orderItemList); : 회원 정보와 주문할 상품 리스트 정보를 이용해 주문 Entity 생성
  • orderRepository.save(order); : 생성한 주문 Entity 저장

7. 실제 주문 로직 구현 - OrderController

주문 관련 요청들을 처리하기 위해 Controller 패키지에 OrderController 클래스를 생성하였다. 상품 주문을 페이지 새로고침을 하지 않고 서버에 주문을 요청하기 위해 비동기 방식을 사용했다.

OrderController.java

package com.shop.controller;

import com.shop.dto.OrderDto;
import com.shop.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.validation.Valid;
import java.security.Principal;
import java.util.List;

@Controller
@RequiredArgsConstructor
public class OrderController {

    private final OrderService orderService;

    @PostMapping(value = "/order")
    public @ResponseBody ResponseEntity order(@RequestBody @Valid OrderDto orderDto
            , BindingResult bindingResult, Principal principal){    
        if(bindingResult.hasErrors()){
            StringBuilder sb = new StringBuilder();
            List<FieldError> fieldErrors = bindingResult.getFieldErrors();

            for (FieldError fieldError : fieldErrors) {
                sb.append(fieldError.getDefaultMessage());
            }

            return new ResponseEntity<String>(sb.toString(), HttpStatus.BAD_REQUEST);
        }

        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);
    }
}
  • if(bindingResult.hasErrors()) : 주문 정보를 받는 orderDto 객체에 데이터 바인딩 시 에러가 있는지 검사
  • try { orderId = orderService.order(orderDto, email); } : 화면으로부터 넘어오는 주문 정보와 회원의 이메일 정보를 이용하여 주문 로직 호출
  • return new ResponseEntity<Long>(orderId, HttpStatus.OK); : 결과 값으로 주문번호, HTTP 응답 코드 반환

동작 화면 🕹️

상품 등록 기능을 구현할 때, 예시로 상품을 하나 등록했었다. 이 게시물을 클릭하게 되면

이렇게 상품 상세 정보가 나오게 되고, 주문하기 버튼을 클릭하면...

위와 같이 주문이 완료되었다는 메세지가 뜨게 된다.


배운것 ✍️

  • 스프링에서 비동기 처리를 할 때 @RequestBody@ResponseBody 어노테이션을 사용한다. 사실 왠만하면 비동기처리이다.
    • @RequestBody : HTTP 요청의 본문 body에 담긴 내용을 자바 객체로 전달
    • @ResponseBody : 자바 객체를 HTTP 요청의 Body로 전달

String email = principal.getName(); 에서 principal
-> @Controller 어노테이션이 선언된 클래스에서 메소드 인자로 principal 객체를 넘겨줄 경우 해당 객체에 직접 접근할 수 있다. OrderController 객체에서 로그인한 유저의 정보(이메일 정보)를 얻기 위해 사용했다.

profile
학습하며 도전하는 것을 즐기는 개발자

0개의 댓글