ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [appling] Product Option 추가 (with. unit test code)
    appling 프로젝트 2024. 9. 12. 15:23
    728x90
    반응형

    🔴 Product Option 추가

    상품을 만들때 옵션을 고려하지 않으려고 했는데 옵션을 추가해달라는 요구사항이 들어와서 구조를 변경해보려고 한다.

    🟠 Product 데이터 정리 및 Option 데이터 추가

    🟢 Product 데이터 정리

    @Entity
    @Table(name = "product")
    @AllArgsConstructor(access = AccessLevel.PROTECTED)
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @Builder
    @Getter
    public class ProductEntity extends CommonEntity {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long productId;
        private String productName;
    
        @Enumerated(EnumType.STRING)
        private ProductType productType;
        @Enumerated(EnumType.STRING)
        private ProductStatus productStatus;
    
        @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)
        private List<ProductOptionEntity> productOptionList;
    
        public void update(PutProductRequest putProductRequest) {
            this.productName = putProductRequest.getProductName();
            this.productType = ProductType.OPTION;
            this.productStatus = putProductRequest.getProductStatus();
        }
    }
    

    먼저 Product 데이터를 정리했다. 해당 부분을 정리하며 Dto, Vo도 수정이 되었는데 해당 부분은 테스트 코드를 실행하면 모두 나오기 때문에 따로 적어두진 않겠다.

    그리고 여기에 연관관계가 추가되어 productOptionList 값이 추가되었다.

    🟢 Product Option 추가

    @Entity
    @Table(name = "product_option")
    @AllArgsConstructor(access = AccessLevel.PROTECTED)
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @Builder
    @Getter
    public class ProductOptionEntity extends CommonEntity {
        @Id
        private Long optionId;
        private int optionSort;
        private String optionName;
        private int optionPrice;
        private int optionStock;
        @Enumerated(EnumType.STRING)
        private OptionStatus optionStatus;
        private String optionDescription;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "product_id")
        private ProductEntity product;
    }

    ProductOption은 다음과 같이 추가되었다.

    🟠 상품 등록 리팩토링

    🟢 Request수정

    @Getter
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public class PostProductRequest {
    
        @NotNull(message = "상품명을 입력해 주세요.")
        @JsonProperty("product_name")
        @Schema(description = "상품명", example = "아리수")
        private String productName;
    
        @NotNull(message = "상품 타입을 입력해 주세요. ex) OPTION")
        @JsonProperty("product_type")
        @Schema(description = "상품 타입", example = "OPTION")
        private ProductType productType;
        @NotNull(message = "상품 옵션을 입력해주세요.")
        @JsonProperty("product_option")
        @Schema(description = "상품 옵션")
        private List<PostProductOptionDto> productOption;
    
    }

    등록시 productOption 값을 추가하여 상품 옵션도 입력 받도록 수정하고

    @Getter
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Schema(description = "상품명 옵션")
    public class PostProductOptionDto {
        @JsonProperty("option_name")
        @NotNull(message = "옵션명을 입력해주세요.")
        @Schema(description = "옵션명", example = "11-12과")
        private String optionName;
        @JsonProperty("option_sort")
        @Schema(description = "옵션 정렬 순서 (미입력시 가장 뒤로 자동 처리)", example = "1")
        private int optionSort;
        @JsonProperty("option_price")
        @NotNull(message = "옵션 가격을 입력해주세요.")
        @Schema(description = "옵션 가격", example = "100000")
        private int optionPrice;
        @JsonProperty("option_stock")
        @NotNull(message = "옵션 재고를 입력해주세요.")
        @Schema(description = "옵션 재고", example = "100")
        private int optionStock;
        @JsonProperty("option_status")
        @NotNull(message = "옵션 상태를 입력해주세요.")
        @Schema(description = "옵션 상태", example = "ON_SALE")
        private OptionStatus optionStatus;
        @JsonProperty("option_description")
        @NotNull(message = "옵션 설명을 입력해주세요.")
        @Schema(description = "옵션 설명", example = "아리수 11-12과 입니다.")
        private String optionDescription;
    }

    입력 받을 옵션 dto도 정의했다.

    🟢 Service 수정

    @Service
    @RequiredArgsConstructor
    @Transactional(readOnly = true)
    public class ProductServiceImpl implements ProductService {
        private final ProductRepository productRepository;
        private final ProductCustomRepository productCustomRepository;
        private final ProductOptionRepository productOptionRepository;
    
        ...
    
        @Transactional
        @Override
        public PostProductResponse createProduct(PostProductRequest postProductRequest) {
            ProductEntity saveProduct = productRepository.save(ProductEntity.from(postProductRequest));
            // saveProduct에 option list 추가하기
            List<ProductOptionEntity> productOptionList = postProductRequest.getProductOption().stream()
                    .map(f -> ProductOptionEntity.from(f, saveProduct))
                    .collect(Collectors.toList());
            productOptionRepository.saveAll(productOptionList);
    
            return PostProductResponse.createFrom(saveProduct);
        }
    
    }

    상품을 등록하는 부분에서 옵션도 함께 등록되도록 수정한다. 그 외에도 소스가 좀 수정되었는데 자세한 내용은 깃 커밋으로 확인하면 좋을것 같다. 테스트 코드와 Entity쪽도 모두 수정이 일어나 이 글에 다 적기는 너무 많더라...ㅜㅜ

    🟠 상품 수정 리팩토링

    수정할때도 옵션 내용이 변경되어야 하기 때문에 리팩토링해야한다. 근데 생각해보니 옵션쪽은 좀 복잡해질거 같다...ㅜ

    🟢 Request 수정

    @Getter
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public class PutProductRequest {
        @JsonProperty("product_id")
        @NotNull(message = "상품 번호를 입력해 주세요.")
        @Schema(description = "상품 번호", example = "1")
        private Long productId;
        @NotNull(message = "상품명을 입력해 주세요.")
        @JsonProperty("product_name")
        @Schema(description = "상품명", example = "시나노 골드")
        private String productName;
        @NotNull(message = "상품 타입을 입력해 주세요. ex) OPTION")
        @JsonProperty("product_type")
        @Schema(description = "상품 타입", example = "OPTION")
        private ProductType productType;
        @NotNull(message = "상품 상태를 입력해 주세요.")
        @JsonProperty("product_status")
        @Schema(description = "상품 상태", example = "SOLD_OUT")
        private ProductStatus productStatus;
        @NotNull(message = "상품 옵션은 1개 이상 등록되어야 합니다.")
        @JsonProperty("product_option")
        @Schema(description = "상품 옵션")
        private List<PutProductOptionDto> productOption;
    }

    등록과 동일하게 수정도 Dto를 추가해주자

    @Getter
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Schema(description = "상품명 옵션 (수정)")
    public class PutProductOptionDto {
        @JsonProperty("product_option_id")
        @Schema(description = "옵션 id (비어있을 경우 새롭게 추가)", example = "1")
        private Long productOptionId;
        @JsonProperty("product_option_name")
        @NotNull(message = "옵션명을 입력해주세요.")
        @Schema(description = "옵션명", example = "11-12과")
        private String productOptionName;
        @JsonProperty("product_option_sort")
        @Schema(description = "옵션 정렬 순서 (미입력시 가장 뒤로 자동 처리)", example = "1")
        private int productOptionSort;
        @JsonProperty("product_option_price")
        @NotNull(message = "옵션 가격을 입력해주세요.")
        @Schema(description = "옵션 가격", example = "100000")
        private int productOptionPrice;
        @JsonProperty("product_option_stock")
        @NotNull(message = "옵션 재고를 입력해주세요.")
        @Schema(description = "옵션 재고", example = "100")
        private int productOptionStock;
        @JsonProperty("product_option_status")
        @NotNull(message = "옵션 상태를 입력해주세요.")
        @Schema(description = "옵션 상태", example = "ON_SALE")
        private ProductOptionStatus productOptionStatus;
        @JsonProperty("product_option_description")
        @NotNull(message = "옵션 설명을 입력해주세요.")
        @Schema(description = "옵션 설명", example = "아리수 11-12과 입니다.")
        private String productOptionDescription;
    }

    그리고 ProductEntity의 update() 메서드를 수정해준다.

    @Entity
    @Table(name = "product")
    @AllArgsConstructor(access = AccessLevel.PROTECTED)
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @Builder
    @Getter
    public class ProductEntity extends CommonEntity {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long productId;
        private String productName;
    
        @Enumerated(EnumType.STRING)
        private ProductType productType;
        @Enumerated(EnumType.STRING)
        private ProductStatus productStatus;
    
        @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)
        private List<ProductOptionEntity> productOptionList;
    
        ...
    
        public void update(PutProductRequest putProductRequest) {
            this.productName = putProductRequest.getProductName();
            this.productType = ProductType.OPTION;
            this.productStatus = putProductRequest.getProductStatus();
    
            // product option 데이터 처리
            List<ProductOptionEntity> newProductOptionList = putProductRequest.getProductOption().stream()
                    .map(f -> ProductOptionEntity.from(f, this))
                    .collect(Collectors.toList());
            newProductOptionList.stream().forEach(f -> f.updateCreateAt(this.productOptionList));
            this.productOptionList.clear();
            this.productOptionList.addAll(newProductOptionList);
        }
    }

    여기서 처리된 부분은 option을 처리할때 새로 수정,삭제,등록에 대해 간단하게 처리하고 싶었다. 그 방법 중 첫번째로

    public class ProductEntity extends CommonEntity {
        // 잘못된 케이스 예시
        ...
    
        public void update(PutProductRequest putProductRequest) {
            this.productName = putProductRequest.getProductName();
            this.productType = ProductType.OPTION;
            this.productStatus = putProductRequest.getProductStatus();
    
            // product option 데이터 처리
            List<ProductOptionEntity> newProductOptionList = putProductRequest.getProductOption().stream()
                    .map(f -> ProductOptionEntity.from(f, this))
                    .collect(Collectors.toList());
            this.productOptionList.clear();
            this.productOptionList.addAll(newProductOptionList);
        }
    }

    list를 clear()하고 들어오는 optionList를 모두 등록해주는 방식으로 처리를 했었는데 여기서 문제가 기존에 있던 데이터가 수정될때 create_at 데이터가 null로 바껴버리는 것이다.

    그 이유는 ProductOptionEntity.from()에서 새롭게 데이터를 반환할때 create_at 값이 null이고 자동으로 처리해둔 create_at 입력이 등록이 아닌 수정이기 때문에 @PrePersist가 작동하지 않기 때문에 null로 입력된걸로 보인다.

    해당 문제 해결을 위해 list를 만들때 기존 데이터와 비교하여 기존의 create_at 값을 넣어주록 했다.

    @Entity
    @Table(name = "product_option")
    @AllArgsConstructor(access = AccessLevel.PROTECTED)
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @Builder
    @Getter
    public class ProductOptionEntity extends CommonEntity {
        ...
    
        public void updateCreateAt(List<ProductOptionEntity> productOptionList) {
            Long targetProductOptionId = this.productOptionId;
            if (targetProductOptionId == null) {
                return ;
            }
    
            Optional<ProductOptionEntity> findProductOption = productOptionList.stream()
                    .filter(f -> f.getProductOptionId().equals(targetProductOptionId))
                    .findFirst();
    
            if (! findProductOption.isPresent()) {
                throw new RuntimeException("해당 상품에 존재하지 않는 옵션 id가 입력되었습니다. proudct_option_id = %d".formatted(targetProductOptionId));
            }
    
            this.createdAt = findProductOption.get().createdAt;
        }
    }

    다음과 같이 도메인에 체크하는 로직을 추가하여 create_at 값이 이전 값으로 유지되도록 처리했다.

    이 방법이 좋은 방법인지는 모르겠지만 우선 등록,수정,삭제시 하나의 로직으로 모두 처리가 가능하여 편하게 사용할 수 있다.

    이제 테스트 코드를 수정을 다 하고 빌드를 눌러보면

    두둥 jacoco에서 터져버렸다. 새롭게 만든 update()를 테스트하지 않아서이다.

    domain에 기능을 넣으면 좋은점은 unit test code가 자연스럽게 작성된다는 점인데 다음과 같이 테스트를 도메인의 기능별로 테스트할 수 있어 통합 테스트가 필요 없고 순수 자바로만 테스트가 가능하여 엄청 빠르고 가볍다.

    🟢 unit Test 작성

    class ProductOptionEntityTest {
    
        @Test
        @DisplayName("product option id 값이 존재하지 않으면 실패한다.")
        void updateFailByProductOptionId() {
            //given
            ProductOptionEntity originProductOptionEntity = ProductOptionEntity.builder()
                    .productOptionId(1L)
                    .productOptionName("11-12과")
                    .productOptionPrice(100000)
                    .productOptionStock(100)
                    .productOptionStatus(ProductOptionStatus.ON_SALE)
                    .build();
    
            // 기존 productOptionId 값은 1인데 2로 요청된 경우임
            ProductOptionEntity requestProductOptionEntity = ProductOptionEntity.builder()
                    .productOptionId(2L)
                    .productOptionName("11-12과")
                    .productOptionPrice(100000)
                    .productOptionStock(100)
                    .productOptionStatus(ProductOptionStatus.ON_SALE)
                    .build();
    
            //when
            //then
            Assertions.assertThatExceptionOfType(RuntimeException.class)
                            .isThrownBy(() -> requestProductOptionEntity.updateCreateAt(List.of(originProductOptionEntity)))
                    .withMessageContaining("해당 상품에 존재하지 않는 옵션 id가 입력되었습니다.");
        }
    
        @Test
        @DisplayName("product option 수정 성공")
        void updateSuccess() {
            //given
            ProductOptionEntity originProductOptionEntity = ProductOptionEntity.builder()
                    .productOptionId(1L)
                    .productOptionName("11-12과")
                    .productOptionPrice(100000)
                    .productOptionStock(100)
                    .productOptionStatus(ProductOptionStatus.ON_SALE)
                    .build();
    
            // 기존 productOptionId 값은 1인데 2로 요청된 경우임
            ProductOptionEntity requestProductOptionEntity = ProductOptionEntity.builder()
                    .productOptionId(1L)
                    .productOptionName("11-12과")
                    .productOptionPrice(100000)
                    .productOptionStock(100)
                    .productOptionStatus(ProductOptionStatus.SOLD_OUT)
                    .build();
    
            //when
            requestProductOptionEntity.updateCreateAt(List.of(originProductOptionEntity));
            //then
            Assertions.assertThat(requestProductOptionEntity.getProductOptionStatus()).isEqualTo(ProductOptionStatus.SOLD_OUT);
        }
    }

    다음과 같이 unit test code를 작성해준다. 이제 build를 하면

    빌드를 성공하는걸 확인할 수 있다!

    728x90
    반응형
Designed by Juno.