개발지식 먹는 하마 님의 블로그

[내일배움캠프 51일차] 배달앱 아웃소싱 프로젝트 회고 본문

내일배움캠프 (CS25)

[내일배움캠프 51일차] 배달앱 아웃소싱 프로젝트 회고

devhippo 2025. 4. 29. 10:35

🚐 배달앱 아웃소싱 프로젝트

🟪 요구사항

  기능 조건 달성 여부
공통 테스트 커버리지 30%이상
필수 인증/인가 - Bcrypt 암호화
- 권한 분류
✔️
필수 가게 CRUD - 사장님은 최대 3개의 가게만 운영 가능 ✔️
필수 메뉴 CRUD - 삭제된 메뉴는 가게 메뉴 조회 시,
  나타나지 않는다.
  주문 내역 조회 시에는 나타난다.
✔️
필수 주문 CRUD - 가게 영업 시간에만 주문 가능
- 최소 주문 금액을 만족해야 가능
✔️
필수 리뷰 CRUD - 배달 완료된 주문만 작성 가능 ✔️
도전 (택) 장바구니 - 한 가게의 메뉴만 담을 수 있다
- 24시간 뒤 만료
✔️
도전 (택) 대시보드  
도전 (택) 소셜 로그인   ✔️
도전 (택) 알림 주문 상태 알림 ✔️
도전 (택) 광고 우선 표시 기능   ✔️

 

🟪 맡은 역할

  • 장바구니
  • 소셜 로그인
  • 주문 상태 알림

 

🟪 Github 링크 (기술 스택, ERD, API 명세서 등)

https://github.com/0422team12/delivery

 

GitHub - 0422team12/delivery

Contribute to 0422team12/delivery development by creating an account on GitHub.

github.com


〽️ 테스트 

🟨 테스트 케이스 설계 Entity

☑️ Store Entity 가게 최소 주문 금액 판별

장바구니와 관련된 가게 최소 주문 금액 판별 메서드에 대한 결과를 테스트

    public boolean isOverMinOrderValue(Long totalPrice){
        return totalPrice >= this.minOrderValue;
    }
  • [성공] 장바구니의 총금액이 최소 주문 금액을 넘기면 True를 반환한다.
  • [경계] 장바구니의 총금액이 최소 주문 금액과 같으면 True를 반환한다.
  • [실패] 장바구니의 총 금액이 최소 주문 금액 미만이면 False를 반환한다.

 

🟨 Cart Entity

Cart Entity의 생성 및 기능에 대한 테스트

    private Cart(User user, Store store, LocalDateTime expiredAt) {
        this.user = user;
        this.store = store;
        this.expiredAt = expiredAt;
    }

    public static Cart createCart(User user, Store store, LocalDateTime expiredAt) {
        return new Cart(user, store, expiredAt);
    }
  • [성공] createCart는 정상적으로 Cart를 생성한다.
    public void updateCartExpiredAt() {
        this.expiredAt = LocalDateTime.now().plusDays(1);
    }

    public boolean isExpired() {
        return this.expiredAt.isBefore(LocalDateTime.now());
    }
  • [성공] 장바구니(Cart)가 만료되었는지 확인할 수 있다.
  • [성공] updateCartExpiredAt은 만료 시간을 하루 뒤로 갱신한다.
    public boolean isEqualStoreId(Long storeId) {
        return this.store.getId().equals(storeId); //추후 store entity의 메서드에 따라 변경 가능성
    }
  • [성공] 가게의 Id가 같은지 비교할 수 있다.

 

☑️ CartItem Entity

Cart Entity의 생성 및 기능에 대한 테스트

    private CartItem(Cart cart, Menu menu, int quantity) {
        this.cart = cart;
        this.menu = menu;
        this.quantity = quantity;
        this.priceSnapshot = menu.getPrice();
    }

    public static CartItem createCartItem(Cart cart, Menu menu, int quantity) {
        return new CartItem(cart, menu, quantity);
    }
  • [성공] createCartItem은 정상적으로 CartItem을 생성한다.
    public void updateQuantity(int quantity) {
        this.quantity = quantity;
    }
  • [성공] CartItem의 수량을 수정할 수 있다.

 

🟨 테스트 케이스 설계 Service

☑️ Cart Service

Cart Service

-------  구현 완료 -------

  • [성공] 빈 장바구니에 메뉴 추가 시, 장바구니가 생성된다.
  • [성공] 다른 가게의 메뉴를 고를 경우, 장바구니가 초기화되고 새로운 메뉴가 담긴다.
  • [성공] 동일한 메뉴 추가 시, 수량이 증가한다.
  • [성공] 수량이 정상적으로 수정된다.
  • [성공] 수량이 0인 경우 해당 아이템을 삭제한다.

-------  구현 미완료 -------

  • [실패] 장바구니 삭제 시, 연관된 CartItem도 삭제된다. -> CartItemNotFound 예외 반환
  • [실패] 장바구니 삭제 시, Cart는 조회되지 않는다. -> CartNotFound 예외 반환
  • [실패] 만료된 장바구니는 조회되지 않는다. -> CartNotFound 예외 반환

 

🟪 테스트 커버리지

장바구니 관련된 테스트 위주로만 작성되어 테스트 커버리지는 10% 대에 그쳤다.
(이후 주문 관련된 테스트가 추가되었으나 해당 부분을 추가한 테스트 커버리지는 측정하지 못함)

 


🛠️ 구현 과정

🛍️ 장바구니 

메뉴와 장바구니의 N:M 관계를 CartItem 중간 테이블을 사용하여 유연하게 설정하였다.

✅ 조건) 장바구니는 한 가게의 한 가게의 메뉴만 담을 수 있다

장바구니로 사용자의 선택을 분석하는가? ➡️ 아니요

유저는 한 개의 장바구니만 가질 수 있는가? ➡️ 네

유저 아이디에 따라 고유한 장바구니를 가지도록 설정하였기 때문에 Hard Delete 방식을 사용하여 데이터를 삭제하였다.

       //존재하는 장바구니가 이미 만료되었거나, 장바구니의 가게 id와 메뉴가 속한 가게 id가 다른 경우 초기화한다.
        if (cart.isExpired() || !cart.isEqualStoreId(store.getId())) {
            deleteCart(userId);
            cartRepository.flush(); //삭제를 즉시 반영
            cart = cartRepository.save(Cart.createCart(user, store, LocalDateTime.now().plusDays(1)));
        }

 

✅ 조건) 장바구니는 24시간 후 만료 된다

장바구니의 만료 시간 필드를 설정하고 메뉴 추가 또는 수량 수정 시 업데이트되도록 설정하였다.

만료된 장바구니 삭제 방법을 여러 가지 중에 고민하였다.

  1. Redis
  2. MySQL 스케줄러
  3. Spring 스케줄러

Redis는 팀원과의 협의 및 개발환경 통일, 사용법 학습에 시간이 필요하였기 때문에 사용에 제약이 있어
Spring 스케줄러를 선택하여 구현하였다.

    @Scheduled(fixedRate = 1000 * 60 * 30) //30분마다
    @Transactional
    public void deleteExpiredCarts() {
        //만료시간이 현재보다 이전인 경우를 조회해온다.
        List<Cart> expiredCarts = cartRepository.findAllByExpiredAtBefore(LocalDateTime.now());
        cartRepository.deleteAll(expiredCarts);
    }

    @PostConstruct
    public void onStartupCleanup() {
        deleteExpiredCarts(); // 앱 시작 시 정리
    }

    @PreDestroy
    public void onShutdownCleanup() {
        deleteExpiredCarts(); // 앱 종료 시 정리
    }

Spring 스케줄러는 서버가 실행되는 동안에만 유효하기 때문에
@PostConstruct와 @PreDestroy를 설정하여 프로그램 시작과 종료 시에도 삭제되도록 설정하였다.

 

💡 소셜 로그인

카카오 API 문서가 가장 간략해보여서 우선 카카오 소셜 로그인을 구현하였다.
Security를 사용하지 말라는 조건이 붙어있었기 때문에 RestTemplate로 직접 요청 Parameter를 구성하여 요청을 보냈다.

인증 코드 발급 -> 엑세스 토큰 발급 -> 유저 정보 가져오기

유저의 Email 정보를 가져온 후, 해당 이메일에 대한 계정의 존재 여부를 확인 및 저장한 뒤
프로그램에서 유효한 엑세스 토큰을 발급하였다.

카카오 로그인 화면

다음과 같은 로그인 페이지에서 성공적으로 로그인할 경우 DB에 카카오와 연동된 이메일이 저장되고
아래와 같이 Token이 문자열로 반환된다.

반환된 엑세스 토큰 문자열

🔔 주문 상태 알림

카카오톡으로 알림톡을 보내고 싶었으나 카카오 채널 생성 및 심사 등의 절차가 필요하였고
시간 내에 해결하기 어려울 것 같아 Email 알림으로 대체하였다.

Spring에서 제공하는 EmailSender 라이브러리를 사용하여 쉽게 구현할 수 있었다.

    public void sendEmail(String to, String subject, String text) {
            SimpleMailMessage message = new SimpleMailMessage();
            message.setTo(to);
            message.setSubject(subject);
            message.setText(text);
            emailSender.send(message);
    }

해당 메서드를 주문 상태 변경 서비스 코드에 추가하여 주문 상태가 변경될 때마다 알림이 가도록 설정하였다.


 

튜터님 피드백

  • 도메인 주도의 Entity 설계를 공부해 보면 좋을 것 같다.
  • 메일 전송은 오래 걸리기 때문에 비동기로 처리해보면 어떨까?
  • 백엔드에서 알림을 관리할 수 있는 여러 기술 (Web Socket)을 참고해 보면 좋을 것 같다.

KPT 회고

Keep

  • GitHub를 전보다 더 다양한 방식으로 사용하며 협업할 수 있었다. (실제로 응한 팀원은 적었지만...)
  • 예외 처리 방식을 통합한 방법이 좋았다. 

Problem 

  • 소통이 부족한게 가장 큰 문제였다.
    질문을 해도 꼬오오옥! 필요하다고 느껴지지 않는(?) 질문들에는 답변이 잘 돌아오지 않았다.
    PR 승인을 작성자 외에 3인이 해줘야 하는 Rule이 설정되어 있었는데
    PR 올렸으니 확인해 달라는 대답에도 묵묵부답 혹은 늦은 확인 덕에 진행이 더욱 더뎌졌던 것 같다.

Try

  • 자원 소모, 실행 시간을 더욱 고려해서 구현해나가면 좋을 것 같다.

이번 협업에서는 그야말로 시간에 쫓겼던 것 같다.
장바구니 같은 부분은 사전에 코드를 임시로 짜놔도 실제 상호작용 부분을 맞춰가며 수정하려면 기틀이 되는 코드가
필요한데 병합이 늦어지니 수정도 늦어졌고 그 와중에 발표자료 제작과 테스트 코드가 실제 동작하도록 수정하려다 보니까 후반부에 매우 힘들었다.

프로젝트를 하면서 Redis, Security가 계속 언급이 되었다.
무조건 좋다는 생각에 쓰려는 것은 아니고 일단 쓸지 말지 고민할 때 알고는 있어야 하지 않나라는 생각이 들었다.

다음 프로젝트 전까지 Redis와 Security 사용법과 피드백받은 부분에 대한 내용은 꼭 공부하고 넘어가려고 한다.