spring

단일 모듈 프로젝트 멀티 모듈 구성하기 -4

HJHStudy 2024. 6. 15. 02:14
728x90

저번 포스팅에 이어 애플리케이션 계층인 서비스에 대한 모듈을 구성해 보고자 한다.

기존 프로젝트에서 REST API와 웹 로직을 분리해서 작성했기 때문에 모듈을 분리하는 데 있어서는 큰 어려움이 없을 것 같다.

하지만 기존 서비스 로직을 하나 확인해 보자.

package shoppingmall.project.service;

import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import shoppingmall.project.additional.web.session.SessionConst;
import shoppingmall.project.domain.Market;
import shoppingmall.project.domain.User;
import shoppingmall.project.domain.dto.ItemDto;
import shoppingmall.project.domain.dto.MarketPayDto;
import shoppingmall.project.domain.dto.MarketPayDtoV2;
import shoppingmall.project.domain.dto.PurchasePayDto;
import shoppingmall.project.domain.item.Item;
import shoppingmall.project.domain.subdomain.Tier;
import shoppingmall.project.repository.*;

import java.util.*;

@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class MarketService {

    private final MarketRepository marketRepository;

    private final UserRepository userRepository;
    private final ItemRepository itemRepository;

    public void addToCart(Long itemId, int quantity, HttpSession session, Item item) {
        session.setAttribute("itemId", itemId);
        session.setAttribute("quantity", quantity);

        User loginUser = (User) session.getAttribute(SessionConst.LOGIN_USER);

        Market market = new Market(quantity, loginUser, item);
        //세션에 장바구니로 추가한 아이템정보 추가
        session.setAttribute(SessionConst.SHOPPING_CART,market);
        marketRepository.save(market);
    }
    @Transactional(readOnly = true)
    public List<MarketPayDtoV2> purchaseItem(Long userId) {
        return marketRepository.shoppingBasketV2(userId);
    }

    @Transactional(readOnly = true)
    public List<ItemDto> purchaseItemV2(HttpSession session) {
        User loginUser = (User) session.getAttribute(SessionConst.LOGIN_USER);
        if (loginUser == null) {
            log.error("로그인 사용자를 찾을 수 없습니다.");
            return null;
        }

        // 로그인 한 사용자의 아이디를 사용하여 구매할 상품 목록을 가져옵니다.
        List<Item> itemsByUserId = itemRepository.findItemsByUserId(loginUser.getId());

        //장바구니의 아이템 리스트의 아이디들 반환
        List<Long> purchaseCartItemId = new ArrayList<>();
        for (Item item : itemsByUserId) {
            purchaseCartItemId.add(item.getId());
        }
        //장바구니 리스트의 아이디에 대한 아이템 반환
        return marketRepository.findItemAndFile(purchaseCartItemId,loginUser.getId());
    }





    public void deleteMarketUser(Long id){
        marketRepository.deleteMarketOfUser(id);
    }
    public void deleteMarketItem(Long id, Long userId) {
        marketRepository.deleteMarketOfItem(id, userId);
    }


    /**
     * user id를 받아 구매 목록을 결제하기 위한 메서드
     * @param id
     * @return
     */
    @Transactional(readOnly = true)
    public PurchasePayDto payRequest(Long id) {
        try {
            List<MarketPayDto> marketPay = marketRepository.shoppingBasket(id);
            if (marketPay.isEmpty()) {
                log.info("구매 목록이 없습니다.");
                return null;
            }

            PurchasePayDto purchasePayDto = new PurchasePayDto();
            int pur_total_price = 0;
            int pur_quantity = 0;
            int count = 0;
            for (MarketPayDto marketPayDto : marketPay) {
                pur_total_price += marketPayDto.getOrderQuantity() * marketPayDto.getOrderQuantity();
                pur_quantity += marketPayDto.getOrderQuantity();
                count ++;
            }

            purchasePayDto.setItemName(marketPay.get(0).getName() + "외 " + --count + "개"); // 첫 번째 아이템의 이름으로 설정
            purchasePayDto.setUserId(marketPay.get(0).getId());
            purchasePayDto.setTotal_price(String.valueOf(pur_total_price));
            purchasePayDto.setQuantity(String.valueOf(pur_quantity));
            return purchasePayDto;
        } catch (Exception e) {
            log.error("구매 목록을 조회하는 중 오류가 발생했습니다.", e);
            return null;
        }
    }


    public int purchaseTotalPriceV2(List<ItemDto> itemDto, User user) {
        int totalPrice = 0;

        for (ItemDto dto : itemDto) {
            totalPrice += dto.getPrice() * dto.getQuantity();
        }

        totalPrice = discountLogic(user, totalPrice);

        return totalPrice;

    }




    public int purchaseTotalPrice(List<MarketPayDtoV2> items, User user) {
        int totalPrice = 0;
        for (MarketPayDtoV2 item : items) {
            totalPrice += item.getPrice() * item.getOrderQuantity();
        }
        totalPrice = discountLogic(user, totalPrice);

        return totalPrice;
    }
    public int discountAmount(int totalPrice, Tier tier) {
        int discount = 0;

        if (tier == Tier.BRONZE) {
            totalPrice = (int) (totalPrice / (1 - Tier.BRONZE.getValue()));
            discount = (int) (totalPrice * Tier.BRONZE.getValue());
        } else if (tier == Tier.SILVER) {
            totalPrice = (int) (totalPrice / (1 - Tier.SILVER.getValue()));
            discount = (int) (totalPrice * Tier.SILVER.getValue());
        } else if (tier == Tier.GOLD) {
            totalPrice = (int) (totalPrice / (1 - Tier.GOLD.getValue()));
            discount = (int) (totalPrice * Tier.GOLD.getValue());
        }

        return discount;
    }

    private int discountLogic(User user, int totalPrice) {
        int discountAmount = 0;
        Optional<User> optionalUser = userRepository.findById(user.getId());
        User findUser = optionalUser.orElseThrow(null);

        if (findUser.getTier() == Tier.BRONZE) {
            discountAmount = (int) (totalPrice * Tier.BRONZE.getValue());
            totalPrice -= discountAmount;
        } else if (findUser.getTier() == Tier.SILVER) {
            discountAmount = (int) (totalPrice * Tier.SILVER.getValue());
            totalPrice -= discountAmount;
        } else if (findUser.getTier() == Tier.GOLD) {
            discountAmount = (int) (totalPrice * Tier.GOLD.getValue());
            totalPrice -= discountAmount;
        }
        return totalPrice;
    }

}

 

이처럼 핵심 비즈니스 로직과 유스케이스 로직이 함께 포함되어 있는 서비스 로직이 존재하기도 한다.

시스템과 연관 없은 핵심 비즈니스 로직은 도메인에 존재하는 것이 좋다.

몇 개의 비즈니스 로직은 도메인 내부에서 비즈니스 로직으로 구현하기도 한 부분이 있다.

위 서비스 코드에서 purchase에 대한 로직은 DTO를 사용하는데 이 DTO는 웹에 존재하기 때문에 비즈니스 로직을 어떻게 구성할까 고민이 많이 되었다. 일단은 뒤로 미루기로 했다. 우선 모듈을 먼저 구성하고 정리를 하기로 했다.

new-web과 new-api 모듈을 생성해서 구성해 보도록 하겠다.

 

구성하던 도중 인터셉터를 원래 웹에 접근에 대한 부분이니 웹에서 생성하려 했지만 생각해 보니 시큐리티를 사용하지 않고 접근 부분을 사용하다 보니 만약 인터셉터가 아닌 시큐리티를 사용한다면 내가 서비스 모듈에 넣어서 사용하는 것이 맞을까 라는 생각이 들었고 역시 분리하는 것이 맞다고 결정했다. 기존 overall에 위치하던 인터셉터는 따로 인터셉터 모듈에 옮겼다. 인터셉터와 aop가 한 곳에 존재하면 aop는 전역적으로 적용되어야 하는데 인터셉터는 별개로 사용되는 곳이 있고 아닌 곳이 있기 때문에 별도로 나누었다.

 

위치는 core 모듈이 적합하다 생각했고 core에 위치하도록 만들었다.

web에 필요한 서비스와 컨트롤러를 구성하던 와중 database 모듈을 주입 받아 사용하게 되었는데 추상화를 통해 database 모듈에 DatabaseRepository 인터페이스를 두고 하위 모듈에 이 인터페이스를 상속받는 MysqlRepository를 두고 사용하게 됐는데 실제 사용해 보니 생각과 다른 동작을 하게 되었다. 

잘못 설계한 것에 대해서 알아보자.

원래 필자의 설계 생각은 상위 데이터베이스 모듈을 두고 인터페이스를 만들어서 여러 데이터베이스를 사용한다면 각 데이터베이스 구현체를 두고 상위 인터페이스를 상속 받아 만들게 되면 깔끔히 관리할 수 있겠다고 생각했지만 실제 구성해 보니 생각과는 달랐다. 너무 과도한 추상화이다. 이미 각 도메인마다 인터페이스 리포지토리 구현체를 가지는데 이를 하나로 묶어서 인터페이스로 제공하고 이 인터페이스도 상위를 상속받게 된다. 글로 하니 난잡한 부분이 있어 그림으로 확인해 보자. 

 

 

위와 같은 이미지의 추상화를 기대해서 상위 모듈의 인터페이스로 하위 각 인터페이스를 사용하려 했지만 너무 과도한 추상화이다.

코드 복잡성도 증가하고 하위 인터페이스를 사용하는데 있어 명확한 정의가 어렵다. 지금은 하위 모듈에 2개씩 구현체를 예시로 보이지만 사실은 많은 구현체가 있기 때문에 이는 올바르지 못한 설계라고 생각한다.

모듈로 구성하는 것으로 각 데이터베이스 기술에 대한 의존성을 분리하고 원하는 기능에 대한 구현체를 사용할 수 있게끔 이미 구성이 되었기 때문에 인터페이스는 없애고 사용하도록 하겠다.

 

리포지토리를 없애고 web 모듈을 생성해서 web 서비스에 필요한 정보를 구성했다.

필자는 일단 web과 api 서비스를 제공하도록 구현할것인데 이 둘은 같은 도메인을 사용하고 같은 dto와 같은 form을 사용한다 그래서 dto와 form 부분을 분리해서 모듈을 구성하기로 했다. 이 dto와 form을 어디에 둘지 고민이고 원래 프로젝트에서 mvc 패턴에 따라 controller, service, repository를 나눴고 web과 api에도 각 패턴이 나뉘어 있다. respository는 이미 나뉘어 있고 controller와 service를 어떻게 할지 고민 중이고 일단 현재까지 필자가 구성한 모듈 계층을 둘러보고 가보자.

 

현재 모듈은 이렇게 구성되어 있다.

MVC 패턴 계층에 적용을 하자면 web과 api 모듈에는 각 controller와 service로직이 존재하고 core에서 respository 저장소와 관련된 부분을 진행하게 된다. 컨트롤러와 서비스를 분리하는 것도 생각을 해보았는데 현재 필자의 프로젝트에서는 프로젝트 크기가 크지 않아서 고민이 많이 됐다. 분리하지 않는다면 복잡성은 조금 낮고 데이터를 접근하는 계층으로 분리된다. 그리고 배포 일관성도 유지할 수 있다. 만약 분리하게 된다면 독립적으로 관리할 수 있고 모듈이 명확한 책임을 갖게 되므로 구성상 좋다.

이 둘 사이에 고민이 됐다. 결정한 것은 분리하지 않기로 했다. 왜냐하면 단일 프로젝트 단일 모듈로 프로젝트 진행할 때 web과 REST API부분을 나눠서 각 mvc 계층별로 따로 구분해서 개발했었다. api에 대한 mvc 패턴 로직은 api를 붙여서 만들어 놓았기 때문에 이미 역할과 책임은 어느정도 구분이 되어있다. 그러므로 이번 멀티 모듈에는 서비스와 컨트롤러를 붙여서 구성하도록 하겠다.

dto와 form을 어디에 위치 시켜야할지 고민이었는데 도메인과 관계없이 도메인이 가지는 값들을 응답과 요청 구조에 맞게 구성되기 때문에 core에 위치할까 하다가 application이라는 모듈을 생성해서 거기에 위치하게 하고 application 모듈에 web과 api도 위치하게 해서 사용하기로 했다.

 

1차적으로 구성은 다 끝났다. 아직 오류가 있는 부분이 있긴 하지만 web과 api의 리포지토리를 분리한 곳에서 api가 web의 repository를 사용하기 때문이고 이는 쿼리를 옮기는 방식이나 둘 다 같은 쿼리를 사용 중이라면 멀티 모듈에 대해 공부할 때 어느 정도 중복은 허용되어야 한다는 말을 참고해서 중복으로 제공해서 제거하도록 하겠다. 오늘은 여기까지이고 내일은 조금 전에 말 한 오류 제거랑 빠진 클래스 없는지 확인하고 각 모듈과 클래스 전체적으로 그림을 통해서 역할과 책임을 알아보도록 하겠다. 알아보면서 비즈니스 로직도 정리하도록 하고 다 하고 나서 추후에는 yml 작성한 것을 정리해서 application모듈의 web과 api에 yml 설정 파일을 넣어주도록 하고 설정이 필요한 곳에 설정을 하고 web과 api가 필요로하는 빈들이 다른 모듈에 있기 때문에 bean으로 스캔할 수 있도록 구성할 것이고 gradle도 각자 의존성만 주입했어서 bootjar와 jar를 정리하고 각 모듈 gradle을 종합적으로 정리해서 web과 api 각각 main을 둬서 실행하게 하고 종합적으로 실행하게 해 볼 것이다. 

 

 

728x90