Presentation 레이어 (1)

qkdk·2024년 2월 1일
0

TDD

목록 보기
7/12

CreateProduct

새로운 상품이 등록되면 가장 최근 상품번호 + 1 한값을 ProductNumber로 가지는 데이터를 생성한다.

  • DB조회를 진행해 최근 상품번호 조회 필요

Red

단위테스트1 : 최근 상품 상품번호 조회

    @DisplayName("가장 마지막 저장한 상품의 상품번호를 읽어온다.")
    @Test
    void findLatestProductNumber() {
        // given
        String targetProductNumber = "003";
        Product product1 = createProduct("001", HANDMADE, SELLING, "아메리카노", 4000);
        Product product2 = createProduct("002", HANDMADE, HOLD, "카페라떼", 4500);
        Product product3 = createProduct(targetProductNumber, HANDMADE, STOP_SELLING, "팥빙수", 7000);

        productRepository.saveAll(List.of(product1, product2, product3));

        // when
        String latestProductNumber = productRepository.findLatestProductNumber();
        // then
        assertThat(latestProductNumber).isEqualTo(targetProductNumber);
    }

    @DisplayName("가장 마지막 저장한 상품의 상품번호를 읽어올때 상품이 하나도 없는 경우에는 null을 반환한다..")
    @Test
    void findLatestProductNumberWhenProductIsEmpty() {
        // when
        String latestProductNumber = productRepository.findLatestProductNumber();
        // then
        assertThat(latestProductNumber).isNull();
    }

상품이 정상적으로 생성되었을때, 상품을 생성할때 상품이 없는경우 두가지 경우 테스트

단위테스트2 : ProductService

얕은 수준의 Service테스트는 Repository서비스와 유사하다.
그럼에도 작성을 하는것이 옳다. 조금의 차이가있을수도 있고, ...

Red

@ActiveProfiles("test")
@SpringBootTest
class ProductServiceTest {

    @Autowired
    private ProductRepository productRepository;
    @Autowired
    private ProductService productService;

    @AfterEach
    void tearDown() {
        productRepository.deleteAllInBatch();
    }

    // 동시성 이유?
    // 있을수 있는데..?
    @DisplayName("신규 상품을 등록한다. 상품번호는 가장 최근 상품의 상품번호에서 1증가한 값이다.")
    @Test
    void creatProduct() {
        // given
        Product product = createProduct("001", HANDMADE, SELLING, "아메리카노", 4000);
        productRepository.save(product);

        ProductCreateRequest request = ProductCreateRequest.builder()
            .type(HANDMADE)
            .sellingStatus(SELLING)
            .name("카푸치노")
            .price(5000)
            .build();

        // when
        ProductResponse productResponse = productService.createProduct(request);

        // then
        assertThat(productResponse)
            .extracting("productNumber", "type", "sellingStatus", "name", "price")
            .contains("002", HANDMADE, SELLING, "카푸치노", 5000);


        List<Product> products = productRepository.findAll();

        assertThat(products).hasSize(2)
            .extracting("productNumber", "type", "sellingStatus", "name", "price")
            .containsExactlyInAnyOrder(
                tuple("001", HANDMADE, SELLING, "아메리카노", 4000),
                tuple("002", HANDMADE, SELLING, "카푸치노", 5000)
            );
    }

    @DisplayName("신규 상품을 등록할때 상품이 없는 경우 등록하면 상품번호는 001이다.")
    @Test
    void creatProductWhenProductIsEmpty() {
        // given
        ProductCreateRequest request = ProductCreateRequest.builder()
            .type(HANDMADE)
            .sellingStatus(SELLING)
            .name("카푸치노")
            .price(5000)
            .build();

        // when
        ProductResponse productResponse = productService.createProduct(request);

        // then
        assertThat(productResponse)
            .extracting("productNumber", "type", "sellingStatus", "name", "price")
            .contains("001", HANDMADE, SELLING, "카푸치노", 5000);

        List<Product> products = productRepository.findAll();

        assertThat(products).hasSize(1)
            .extracting("productNumber", "type", "sellingStatus", "name", "price")
            .contains(
                tuple("001", HANDMADE, SELLING, "카푸치노", 5000)
            );
    }

    private Product createProduct(String productNumber, ProductType type,
        ProductSellingStatus sellingStatus, String name, int price) {

        return Product.builder()
            .productNumber(productNumber)
            .type(type)
            .sellingStatus(sellingStatus)
            .name(name)
            .price(price)
            .build();
    }
}

Green

    public ProductResponse createProduct(ProductCreateRequest request) {
        String nextProductNumber = createNextProductNumber();

        Product product = request.toEntity(nextProductNumber);
        Product savedProduct = productRepository.save(product);

        return ProductResponse.builder()
            .id(savedProduct.getId())
            .productNumber(nextProductNumber)
            .name(request.getName())
            .type(request.getType())
            .price(request.getPrice())
            .sellingStatus(request.getSellingStatus())
            .build();
    }

    private String createNextProductNumber() {
        String latestProductNumber = productRepository.findLatestProductNumber();
        if (latestProductNumber == null) {
            return "001";
        }

        int latestProductNumberInt = Integer.parseInt(latestProductNumber);
        int nextProductNumberInt = latestProductNumberInt + 1;

        return String.format("%03d", nextProductNumberInt);
    }

Blue

    public ProductResponse createProduct(ProductCreateRequest request) {
        String nextProductNumber = createNextProductNumber();

        Product product = request.toEntity(nextProductNumber);
        Product savedProduct = productRepository.save(product);

        return ProductResponse.of(savedProduct);
    }

단위 테스트도 Red - Green - Blue 원칙을 지켜서 작성한다.
Red: 상품을 추가할때, 상품이 없는데 추가할때 경우의 테스트 코드를 작성한다.
Green: 비지니스 로직을 구현하고 Test 돌려서 검증한다.
Blue: Builder를 of 매서드를 이용하도록 리펙토링을 진행하고 Test를 진행해 검증한다.

ETC

Transactional(readOnly = True)

  • CUD에서 동작을 안한다. R만 동작
  • JPA : CUD 스냅샷 저장 X, 변경감지 X -> 성능 향상의 효과
  • CQRS - Command(CUD) / Query(R)의 분리
    • 도메인에 따라 다르지만 대부분 Read의 작업이 압도적으로 많다. (2:8 정도)
    • Read 작업이 몰려서 Command의 작업이 동작하지 않는다면 더 큰 장애로 발생 가능, 반대도 동일
      -> 두 작업의 성격의 차이로 인해 분리하자!
    • DB에 대한 EndPoint를 구분할 수 있다.
      • 실무에선 Command용 DB(MasterDB), 레플리카 된 Read용 DB(SlaveDB)를 사용
      • ReadOnly가 있다면 Read용 DB연결
      • 이를 통해 장애 격리 가능

프로젝트에 적용하는 Tip

  • 클래스 단에 @Transactional(readOnly = true)를 붙여 누락을 방지
    • CUD 작업이 필요한 메서드에만 @Transcational 붙이기
  • Read 서비스와 CUD 서비스의 클래스 분리
profile
qkdk

0개의 댓글

관련 채용 정보