[Spring] 쇼핑몰 2 - 상품 등록

춤인형의 개발일지·2025년 2월 27일

Spring실습

목록 보기
36/40

상품 등록

상품을 등록할 땐, 상품 옵션과 상품 옵션의 디테일까지 저장해야된다.

이런 생각을 가지고 다른 쇼핑몰들을 찾아봤을 때, 옵션이 2개인 것도 있고, 그 옵션들끼리 관계를 가지고 있기 때문에 이제 굉장히 헷갈리기 시작했다.

첫번째 방법

한번에 저장한다.

나는 처음에 그냥 option을 저장하면 될 줄알았다. 그냥 자연스럽게 될 줄 알았다. 그니까 내가 생각했던것이

{
  "productName": "신발",
  "price": 50000,
  "description": "편안한 신발입니다.",
  "category": SHOSE,
  "productCondition": "NEW",
  "options": [
  {
  "optionGroupName": "색상",
  "optionValues": [
  { "value": "빨강", "stock": 5 },
  { "value": "검정", "stock": 10 }
  ] },
  {
  "optionGroupName": "사이즈",
  "optionValues": [
  { "value": "230", "stock": 3 },
  { "value": "240", "stock": 7 },
  { "value": "250", "stock": 5 }
  ]
  }
  ]
  }

이런 느낌? 색상안에는 빨강, 검정이 있는거고 또 옵션이 하나가 더 있어서 사이즈도 고를 수 있게 생각을 했다. 하지만 이런 구조는 문제가 있었다.

❓왜냐하면 색상이 빨간색인 상품의 사이즈가 230인 상품의 재고로 관리해야되는거 아닌가?

두번째 방법

그럼 상품의 옵션이 있고 또 그 옵션의 옵션이 있는게 되는 것이다. 즉, 데이터를 다시 구상했봤을 때

{
  "optionName": "색상",
  "optionValue": null,
  "stock": null,
  "subOptions": [
    {
      "optionName": "빨강",
      "optionValue": null,
      "stock": null,
      "subOptions": [
        {
          "optionName": "사이즈",
          "optionValue": "230",
          "stock": 3,
          "subOptions": null
        },
        {
          "optionName": "사이즈",
          "optionValue": "240",
          "stock": 7,
          "subOptions": null
        }
      ]
    },
    {
      "optionName": "파랑",
      "optionValue": null,
      "stock": 2,
      "subOptions": null
    }
  ]
}

이렇게 구성이 되어야한다. 이러면 상품을 관리하는 입장에서 편하게 할 수 있으니까?

이걸 이제 어떻게 구현할 것인가를 생각해봤을 때 너무 어려울 것 같았다.
그래서 다른 방법을 생각해봤는데,

  • ❓상품을 하나 만들고, 옵션을 만들 때 그 옵션을 만드는 함수를 호출해서 상품에 add해주면 된다고 생각했다.
    -> 이건 그러면 각 옵션에 대해 연관관계를 맺는건 안되는 것 같다.
  • 그래서 나온 생각이 위에와 같은 json형태로 만들기 위해서는 계속해서 옵션의 옵션을 재귀함수로 호출해주면 어떨까?

나 근데 재귀함수 처음해봐서 잘 모르겠다. 일단 상품을 하나 만들고, 옵션을 추가하는 함수를 만들어둔다.

@Transactional
    public ProductCreateResponse save(Admin admin, ProductCreateRequest productCreateRequest) {
        adminRepository.findById(admin.getId())
                .orElseThrow(() -> new NoSuchElementException("관리자가 아닙니다."));

        Product product = productRepository.save(new Product(
                productCreateRequest.productName(),
                productCreateRequest.price(),
                productCreateRequest.description(),
                productCreateRequest.category(),
                productCreateRequest.productCondition(),
                productCreateRequest.createAt()
        ));

        //상품에 새로운 옵션을 추가
        //✅여기 뜯어보기
        if(productCreateRequest.options() != null){
            for (OptionGroup optionRequest : productCreateRequest.options()) {
                saveProductOption(product, optionRequest, null);
            }
        }
        return convertToProductCreateResponse(product);
}

✅뜯어봅시다
productCreateRequest라는 dto는 아래처럼 구성되어 있다.

public record ProductCreateRequest(
        String productName,
        int price,
        String description,
        Category category,
        Condition productCondition,
        List<OptionGroup> options
) {
}

optionGroup도 dto이다. optionGroup은 뭐가 있어야 하냐..
optionGroup은 옵션의 이름, 옵션의 값이 있어야 한다. 그리고 또 subOption이 들어올 수 있으니까 optionGroup을 또 사용하는 셈이 된다. 나 여기서부터 머리 터졌다.
dto안에 dto 안에 dto..

❓dto에 Entity를 쓰면 안되나?
안됨! 왜냐면 일단 dto는 데이터가 이동되는 것이기 때문에 사용자에게 노출된다. 그 말은 entity를 쓰게 되면 다 노출되고, 데이터가 어떻게 구성되어 있는지 알게 된다.

그래서 optionGroup은 아래처럼 구성되어 있다.

public record OptionGroup(
        String optionName, // 옵션 그룹명 (예: 색상)
        List<OptionValue> optionValues, // 옵션 값 리스트 (예: 빨강, 파랑)
        List<OptionGroup> subOptions // 서브 옵션 리스트 (예: 사이즈 옵션)
) {
}

optionValue는 진짜 값과 재고를 나타내줘야한다.

public record OptionValue(
        String optionValue,
        Integer stock
) {
}

이게 바로 추상화..? 엄청 얽혀있는 느낌이다. 이렇게 사용하는게 맞는지 나는 잘 모르겠다.

여튼, 다시 service함수를 보자면,

//✅여기 뜯어보기
        if(productCreateRequest.options() != null){
            for (OptionGroup optionRequest : productCreateRequest.options()) {
                saveProductOption(product, optionRequest, null);
            }
        }

이 부분은 사용자가 option을 넣었냐? -> 넣었으면 그 옵션들을 반복적으로 상품에 저장해라. 라는 의미이다. 그래서 saveProductOption 이 함수를 호출하게 된다.

    private void saveProductOption(Product product, OptionGroup optionGroup, ProductOption parentOption) {
        ProductOption productOption = new ProductOption(optionGroup.optionName(), product);

        if (parentOption != null) {
            parentOption.addSubOption(productOption);  // 부모 옵션에 추가
        } else {
            product.addOption(productOption);  // 최상위 옵션인 경우
        }

        productOptionRepository.save(productOption);

        // 1) 옵션 값 저장 (optionValue & stock 포함)
        if (optionGroup.optionValues() != null) {
            for (OptionValue subOption : optionGroup.optionValues()) {
                ProductOptionSub optionSub = new ProductOptionSub(subOption.optionValue(), subOption.stock());
                productOption.addOptionValue(optionSub);
                productOptionSubRepository.save(optionSub);
            }
        }

        // 2) 하위 옵션 저장 (재귀 호출)
        if (optionGroup.subOptions() != null) {
            for (OptionGroup subOptionRequest : optionGroup.subOptions()) {
                saveProductOption(product, subOptionRequest, productOption);
            }
        }
    }

하나씩 코드를 보자면,

if (parentOption != null) {
            parentOption.addSubOption(productOption);  // 부모 옵션에 추가
        } else {
            product.addOption(productOption);  // 최상위 옵션인 경우
        }

        productOptionRepository.save(productOption);

먼저 옵션이 이미 있는 경우(parentOption : 부모 옵션)
옵션에 subOption을 추가하고,
그렇지 않은 경우(옵션이 없는 경우)에는
그냥 옵션을 추가하면된다.

addSubOption은 productOption Entity에 있다.

//setter
 public void setParentOption(ProductOption parentOption) {
        this.parentOption = parentOption;
    }
    
public void addSubOption(ProductOption productOption) {
        productOption.setParentOption(this);
        this.subOptions.add(productOption);
    }

setter함수를 만들어두고, 변경된 것을 알 수 있게 한 다음, subOptions이 list니까 add로 productOption을 추가해준다.

addOption도 똑같다. product Entity 내부에 작성해놔서 setter함수로 product에 option을 추가해준다.

이제 옵션 값을 추가해주는 로직을 봐보자 이건 옵션 (상위옵션 / 부모옵션)이다.

if (optionGroup.optionValues() != null) {
    for (OptionValue subOption : optionGroup.optionValues()) {
        ProductOptionSub optionSub = new ProductOptionSub(subOption.optionValue(), subOption.stock());
        productOption.addOptionValue(optionSub);
        productOptionSubRepository.save(optionSub);
    }
}

optionGroup.optionValues()는 리스트로 여러개 있으니까 for문으로 subOption을 추가해준다.

sub옵션을 추가해주는 로직을 봐보자 이건 하위옵션이다.(자식옵션)

if (optionGroup.subOptions() != null) {
    for (OptionGroup subOptionRequest : optionGroup.subOptions()) {
        saveProductOption(product, subOptionRequest, productOption);
    }
}

이렇게 구현해줬다.

재귀함수의 문제가 너무 깊이 들어가면 오버플로우가 될 가능성이 높다. 3개정도의 옵션은 괜찮다 했는데, 어쨌든 성능상의 문제가 생길 것 같다. 계속 같은 함수를 호출하고, 그걸 또 가지고오고 약간 이런 느낌이라 여튼 재귀가 불안한 느낌...이 들었다.


개개개 어려움 난이도 최최상...겁나 어려워ㅜㅜㅜ 재귀함수도 사실 내가 생각한게 아니라 하다 보니까 그렇게 된 느낌.. 여튼 알아가면 되지 뭐~

0개의 댓글