BACKEND/JAVA & SPRING

장바구니 api - Create 추가 #2 (Feat. SpringBoot)

이-프 2023. 12. 21. 19:15

장바구니 api - Create 추가 #1 (Feat. SpringBoot) 에서 설명한 Table 구조에 따른 장바구니 추가 api 코드에 대해 설명하겠다. v1와 v2 기간을 거치면서 장바구니 api를 고도화했다. 간단한 api일줄 알았으나, 직접 개발을 하면서 여러 에러를 겪고 해결하며 성장할 수 있었다. 🌱

로직 설명

단기투숙

  • startDate = 2023-11-29
  • endDate = 2023-11-30
  • roomProduct |2023-11-29| stock : 2
  1. 장바구니 = 11월 29일만 담긴다.
  1. 만약, 11월 29일의 재고가 이미 cart에 2번 담겨있다면, “상품의 재고 부족으로 장바구니 담기가 불가합니다.”이 발생한다.

장기투숙

  • startDate = 2023-11-29
  • endDate = 2023-12-01
  • room1번 =⇒ 장바구니
  • roomProduct |2023-11-29| stock : 2 | room1
  • roomProduct |2023-11-30| stock : 4 | room1
  • roomProduct |2023-12-01| stock : 6 | room1
  1. 장바구니 = 11월 29일~ 11월 30일 까지 담긴다. (12월 1일은 담기지 않는다.)
  1. 만약, 11월 29일 ~ 11월 30일까지 room의 재고가 0인 순간이 있다면 장바구니에 담길 수 없다.
    (-> 애초에 이런 경우라면 숙소 상세 조회시, 안나옵니다!)
  1. 11월29일~11월30일 의 최소재고가 2개이므로, 11월29일~11월30일 room1번은 장바구니에 2번만 담길 수 있다.

    ⇒ 만약, 2번 이상 담으려고 하면 상품의 재고 부족으로 장바구니 담기가 불가합니다.” 발생

예외처리

  • startDate 가 endDate보다 후순위 일 경우 → “시작일/종료일을 다시 확인해주세요.”
  • 만약, roomProduct가 11월29일~11월 30일까지만 있는데, 12월1일~12월3일을 요청시 → Room Product가 존재하지 않습니다.

개발 에러 상황

  1. 단기투숙 ↔ 장기투숙을 구별하지 않았다.
  1. 11월 01일 ~ 11월 03일 투숙시, 11월 1일과 2일만 roomCart에 담겨야 했다.
  1. 단기투숙 ↔ 장기투숙을 구별한 뒤, 로직을 전체 수정해야했는데 다 수정하지 못했다.

    ⇒ Repository ERROR

V1 service

@Service
@Transactional
@RequiredArgsConstructor
public class RoomCartService {

    private final RoomRepository roomRepository;
    private final CartRepository cartRepository;
    private final RoomCartRepository roomCartRepository;

    public RoomCartResponseDTO postRoomCart(Long member_id, Long room_id) {
        Room room = roomRepository.findById(room_id).get();
        Cart cart = cartRepository.findByMemberId(member_id).get();
        if (room.getStock() > 0) {
            room.updateRoomStock(room.getStock() - 1);
            RoomCart roomCart = roomCartRepository.save(new RoomCart(cart, room));
            cart.postRoomCarts(roomCart);
            return new RoomCartResponseDTO(cart);
        } else {
            throw new OutOfStockException();
        }
    }
}

V2 service

  • RoomProduct table 생성

    문제발생…⚠️💣

    • 시작날짜, 종료날짜가 포함되어 객실의 stock을 체크해야하는게 아닌가 ?
      • 결과적으로, RoomCart Table에 startDate, endDate가 포함되어야하는게 아닌가?

      EXAMPLE

      • 11월 3일 ~ 11월 4일 숙소#1 방#1 : 4개
      • 11월 3일 ~ 11월 4일 숙소#1 방#2 : 1개

      ⇒ 숙소#1 방#1은 장바구니에 4번 담을 수 있다.

      RequestDto(roomId, startDate, endDate)

      RequestDto(1,11월3일,11월4일) → 장바구니 담김 → 숙소#1 방#1 : 3개

      RequestDto(1,11월3일,11월4일) → 장바구니 담김 → 숙소#1 방#1 : 2개

      RequestDto(1,11월3일,11월4일) → 장바구니 담김 → 숙소#1 방#1 : 1개

      RequestDto(1,11월3일,11월4일) → 장바구니 담김 → 숙소#1 방#1 : 0개

      RequestDto(1,11월3일,11월4일) → 장바구니 담김 → OutOfStockException

      결과적으로, 계속 Room의 Stock Update가 필요한데, 이게 DB에 저장이 되어야하니까 객실에 Stock 칼럼 외에 CntStock이 또 추가되어야하는게 아닌가?

    • 임시방편으로 생각해본 해결방안..
      • 프론트측에서 Stock을 알고있으니 OutOfStockException을 맡고, 백에서는 검증하지 않는다.
        • Ajax라는걸쓰면 순간적으로 Stock의 개수를 - 해서 검증할 수 있다고 하는데 이게 프론트 영역에서 검증하는 방식인것같습니다.
      • 아니면, 장바구니 버튼 클릭으로 넘어가지 말고,
        • 장바구니 버튼 클릭 → 몇개 담으실건가요? 팝업창 (개수 확인) → 그 개수보다 만약 객실 stock이 적다면 error

          ⇒ 이 루틴으로 간다면 프론트한테 데이터 넘겨받고 일이 2번이 된다…

      • Room Entity에 CntStock 컬럼을 추가하여 Update(담길때마다 -1)하여 관리한다.

변경사항

  • Cart가 존재하지 않을 경우, Cart 생성
  • checkContinualDate : 해당 기간동안 roomProduct가 모두 List에 담겼는지 확인 메서드 구현
  • findMinStockRoomProduct : 가장 적은 재고를 가진 RoomProduct를 찾는 메서드 구현
  • roomProduct의 stock에서 가장 적은 재고를 가진 roomProduct가 roomCart에 담긴 횟수를 빼서 0보다 크면 roomCart에 저장
  • roomCartRepository에 이를 저장하고 cart의 List에 추가
@Transactional
    public RoomCartResponse postRoomCart(Long member_id, Long room_id,
        RoomCartRequest roomCartRequest) {

        Optional<Cart> optionalCart = cartRepository.findByMemberId(member_id);
        Cart cart = optionalCart.orElseGet(() -> {
            Member member = memberRepository.findById(member_id).orElseThrow(
                MemberNotFoundException::new);
            return cartRepository.save(new Cart(member));
        });

        List<RoomProduct> roomProductList = roomProductRepository.findByRoomIdAndStartDateAndEndDate(
            room_id,
            roomCartRequest.getStartDate(), roomCartRequest.getEndDate());

        checkContinualDate(roomProductList, roomCartRequest);

        RoomProduct roomProductMinStock = findMinStockRoomProduct(roomProductList);

        for (RoomProduct roomProduct : roomProductList) {
            List<RoomCart> roomCartList = roomCartRepository.findByRoomProductId(
                roomProductMinStock.getId());
            if (roomProduct.getStock() - roomCartList.size() > 0) {
                RoomCart roomCart = RoomCart.builder().cart(cart).roomProduct(roomProduct).build();
                roomCartRepository.save(roomCart);
                cart.postRoomCarts(roomCart);
            } else {
                throw new OutOfStockException();
            }
        }
        return new RoomCartResponse(cart);
    }

    private void checkContinualDate(List<RoomProduct> roomProductList,
        RoomCartRequest roomCartRequest) {
        LocalDate startDate = roomCartRequest.getStartDate();
        LocalDate endDate = roomCartRequest.getEndDate();
        long betweenDays = ChronoUnit.DAYS.between(startDate, endDate);
        if (roomProductList.size() != betweenDays) {
            throw new RoomProductNotFoundException();
        }
    }

    private RoomProduct findMinStockRoomProduct(List<RoomProduct> roomProductList) {
        int minStock = Integer.MAX_VALUE;
        for (RoomProduct roomProduct : roomProductList) {
            minStock = Math.min(roomProduct.getStock(), minStock);
        }
        RoomProduct minStockRoomProduct = roomProductRepository.findByStock(minStock).get();
        return minStockRoomProduct;
    }

V2 service #2

변경이유

  • 단기투숙과 장기투숙 로직을 구별하고 수정했습니다.
  • 단기투숙시, startDate의 roomProduct만 넘기고
  • 장기투숙시, startDate ~ endDate-1만 넘겨야 하므로 수정했습니다.
  • RoomProductNotFound Exception Handler를 추가했습니다.
@Transactional
    public RoomCartResponse postRoomCart(Long member_id, Long room_id,
        RoomCartRequest roomCartRequest) {
        checkStartDateEndDate(roomCartRequest);
        Optional<Cart> optionalCart = cartRepository.findByMemberId(member_id);
        Cart cart = optionalCart.orElseGet(() -> {
            Member member = memberRepository.findById(member_id).orElseThrow(
                MemberNotFoundException::new);
            return cartRepository.save(new Cart(member));
        });

        List<RoomProduct> roomProductList;
        if(roomCartRequest.getStartDate().equals(roomCartRequest.getEndDate().minusDays(1))){
            roomProductList = roomProductRepository.findByRoomIdAndStartDate(
                room_id, roomCartRequest.getStartDate());
        } else {
            roomProductList = roomProductRepository.findByRoomIdAndStartDateAndEndDate(
                room_id,
                roomCartRequest.getStartDate(), roomCartRequest.getEndDate().minusDays(1));
        }

        checkContinualDate(roomProductList, roomCartRequest);

        RoomProduct roomProductMinStock = findMinStockRoomProduct(roomProductList);
        for (RoomProduct roomProduct : roomProductList) {
            List<RoomCart> roomCartList = roomCartRepository.findByRoomProductId(
                roomProductMinStock.getId());
            if (roomProduct.getStock() - roomCartList.size() > 0) {
                RoomCart roomCart = RoomCart.builder().cart(cart).roomProduct(roomProduct).build();
                roomCartRepository.save(roomCart);
                cart.postRoomCarts(roomCart);
            } else {
                throw new OutOfStockException();
            }
        }
        return new RoomCartResponse(cart);
    }
private void checkContinualDate(List<RoomProduct> roomProductList,
        RoomCartRequest roomCartRequest) {
        LocalDate startDate = roomCartRequest.getStartDate();
        LocalDate endDate = roomCartRequest.getEndDate();
        long betweenDays = ChronoUnit.DAYS.between(startDate, endDate);
        if (roomProductList.size() != betweenDays) {
            throw new RoomProductNotFoundException();
        }
    }
    private RoomProduct findMinStockRoomProduct(List<RoomProduct> roomProductList) {
        int minStock = Integer.MAX_VALUE;
        for (RoomProduct roomProduct : roomProductList) {
            minStock = Math.min(roomProduct.getStock(), minStock);
        }
        RoomProduct minStockRoomProduct = roomProductRepository.findByStock(minStock).get();
        return minStockRoomProduct;
    }
    private void checkStartDateEndDate(RoomCartRequest roomCartRequest) {
        LocalDate startDate = roomCartRequest.getStartDate();
        LocalDate endDate = roomCartRequest.getEndDate();
        if(startDate.isAfter(endDate)){
            throw new WrongDateException();
        }
    }

V2 service #3

변경이유

ERROR 발생

⚠️ UnsupportedOperationException

⚠️ query did not return a unique result

수정사항

  • 단기투숙 ↔ 장기투숙 로직 완전 다르게 진행
@Transactional
    public RoomCartResponse postRoomCart(Long member_id, Long room_id,
        RoomCartRequest roomCartRequest) {

        checkStartDateEndDate(roomCartRequest);

        Optional<Cart> optionalCart = cartRepository.findByMemberId(member_id);
        Cart cart = optionalCart.orElseGet(() -> {
            Member member = memberRepository.findById(member_id).orElseThrow(
                MemberNotFoundException::new);
            return cartRepository.save(new Cart(member));
        });

        List<RoomProduct> roomProductList = new ArrayList<>();
        if(roomCartRequest.getStartDate().equals(roomCartRequest.getEndDate().minusDays(1))){
            RoomProduct roomProduct = roomProductRepository.findByRoomIdAndStartDate(
                room_id, roomCartRequest.getStartDate()).orElseThrow(RoomProductNotFoundException::new);
            List<RoomCart> roomCartList = roomCartRepository.findByRoomProductId(roomProduct.getId());
            if(roomProduct.getStock() - roomCartList.size() > 0) {
                RoomCart roomCart = RoomCart.builder().cart(cart).roomProduct(roomProduct)
                    .personnel(roomCartRequest.getPersonnel()).build();
                roomCartRepository.save(roomCart);
                cart.postRoomCarts(roomCart);
                return new RoomCartResponse(cart);
            } else {
                throw new OutOfStockException();
            }
        } else {
            roomProductList = roomProductRepository.findByRoomIdAndStartDateAndEndDate(
                room_id,
                roomCartRequest.getStartDate(), roomCartRequest.getEndDate().minusDays(1));
            checkContinualDate(roomProductList, roomCartRequest);

            RoomProduct roomProductMinStock = findMinStockRoomProduct(roomProductList);

            for (RoomProduct roomProduct : roomProductList) {
                List<RoomCart> roomCartList = roomCartRepository.findByRoomProductId(
                    roomProductMinStock.getId());
                if (roomProduct.getStock() - roomCartList.size() > 0) {
                    RoomCart roomCart = RoomCart.builder().cart(cart).roomProduct(roomProduct).build();
                    roomCartRepository.save(roomCart);
                    cart.postRoomCarts(roomCart);
                } else {
                    throw new OutOfStockException();
                }
            }
            return new RoomCartResponse(cart);
        }

    }
private void checkContinualDate(List<RoomProduct> roomProductList,
        RoomCartRequest roomCartRequest) {
        LocalDate startDate = roomCartRequest.getStartDate();
        LocalDate endDate = roomCartRequest.getEndDate();
        long betweenDays = ChronoUnit.DAYS.between(startDate, endDate);
        if (roomProductList.size() != betweenDays) {
            throw new RoomProductNotFoundException();
        }
    }

<-- 변경 사항 -->
    private RoomProduct findMinStockRoomProduct(List<RoomProduct> roomProductList) {
        roomProductList.sort(new Comparator<RoomProduct>() {
            @Override
            public int compare(RoomProduct o1, RoomProduct o2) {
                if(o1.getStock() > o2.getStock()){
                    return 1;
                } else if (o1.getStock() < o2.getStock()) {
                    return -1;
                }else {
                    return 0;
                }
            }
        });
        return roomProductList.get(0);
    }
<------------->

    private void checkStartDateEndDate(RoomCartRequest roomCartRequest) {
        LocalDate startDate = roomCartRequest.getStartDate();
        LocalDate endDate = roomCartRequest.getEndDate();
        if(startDate.isAfter(endDate)){
            throw new WrongDateException();
        }
    }

배운점

  1. yaml파일의 profile active : dev 로 되어있으면 RDS랑 연결된다.
  1. Putty로 ssh 접속
    1. cd app
    1. tail -f nohup.out : error 확인 가능

      http://43.202.50.38:8080/v2/accommodations/356?startDate=2023-11-29&endDate=2023-11-30&personnel=1

      localhost로 수정 ⇒ http://localhost:8080/v2/accommodations/356?startDate=2023-11-29&endDate=2023-11-30&personnel=1 으로 보면, 이제 log들을 IntelliJ에서 볼 수 있다.

  1. 생각보다 예외처리가 다양하고 꼼꼼히 해야한다.

    사용자 입장에서 이 api를 통해 서비스를 사용했을 때, 매끄럽게 운영되어야 한다. 그러한 점에서 개발하기 전에 로직을 꼼꼼히 작성해보고, 예외처리를 구체적으로 작성해둬야함을 깨닫게 됐다.

    아직까지 로직을 뚝딱 생각할 정도로 빠른 개발자는 아니라고 생각한다. 하지만 배워나아가는 입장에서 여기서 포기할 수는 없다. 👶🏻🐣

    아이패드에 사용자입장 + 개발자 입장을 통틀어서 마구잡이로 정리하면서 로직을 하나씩 써내려갔다.

    덕분에 생각도 정리되고, 로직도 구체적으로 적어 내려갈 수 있었다.

    앞으로도 꼼꼼히 기록하는 사람이 되야겠다. 🌱


Uploaded by N2T