상품을 등록할 땐, 상품 옵션과 상품 옵션의 디테일까지 저장해야된다.
이런 생각을 가지고 다른 쇼핑몰들을 찾아봤을 때, 옵션이 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개정도의 옵션은 괜찮다 했는데, 어쨌든 성능상의 문제가 생길 것 같다. 계속 같은 함수를 호출하고, 그걸 또 가지고오고 약간 이런 느낌이라 여튼 재귀가 불안한 느낌...이 들었다.
개개개 어려움 난이도 최최상...겁나 어려워ㅜㅜㅜ 재귀함수도 사실 내가 생각한게 아니라 하다 보니까 그렇게 된 느낌.. 여튼 알아가면 되지 뭐~