[워밍업 클럽 4기 백엔드] 3주차 발자국

[워밍업 클럽 4기 백엔드] 3주차 발자국

3주차에 배운 것들

📌 Layered Architecture(레이어드 아키텍처) 테스트

image

💡관심사를 분리하여 계층을 나누고, 책임을 나누어 유지보수에 용이하게 하는 방법

  • 테스트 하기 복잡해 보이지만, 무엇을 어떻게 테스트 할 지 알아보자!


📌 Persistence Layer

  • Data Access의 역할

  • 비즈니스 가공 로직이 포함되어서는 안 된다.

    • Data에 대한 CRUD에만 집중한 레이어이기 때문

@Test
@DisplayName("상품번호 리스트로 상품들을 조회한다.")
void findAllByProductNumberIn() {
    // given
    Product product1 = createProduct("001", HANDMADE, SELLING, "아메리카노", 4000);
    Product product2 = createProduct("002", HANDMADE, HOLD, "카페라떼", 4500);
    Product product3 = createProduct("003", HANDMADE, STOP_SELLING, "팥빙수", 7000);

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

    // when
    List<Product> products = productRepository.findAllByProductNumberIn(List.of("001", "002"));

    // then
    assertThat(products).hasSize(2)
            .extracting("productNumber", "name", "sellingStatus")
            .containsExactlyInAnyOrder(
                    tuple("001", "아메리카노", SELLING),
                    tuple("002", "카페라떼", HOLD)
            );
}

특징

  • 작성한 쿼리가 우리의 의도대로 동작하는지 확인하는 단위 테스트의 성격을 가진다.

     

  • 해당 쿼리가 미래에 어떠한 구현체로 변형될지 모르기 때문에 테스트 코드로 보장해주어야 한다.

     


    📌 Business Layer

  • 비즈니스 로직을 구현하는 역할

  • Persistence Layer와의 상호작용(Data를 읽고 쓰는 행위)을 통해 비즈니스 로직을 전개시킨다.

  • 트랜잭션을 보장해야 한다.

    • 트랜잭션 : "쪼갤 수 없는 업무 처리의 최소 단위"로 원자성, 일관성, 격리성, 지속성 일명 ACID를 보장해야함

@Test
@DisplayName("주문번호 리스트를 받아 주문을 생성한다.")
void createOrderTest() {
    // given
    Product product1 = createProduct(HANDMADE, "001", 1000);
    Product product2 = createProduct(HANDMADE, "002", 3000);
    Product product3 = createProduct(HANDMADE, "003", 5000);
    productRepository.saveAll(List.of(product1, product2, product3));

    OrderCreateRequest request = OrderCreateRequest.builder()
            .productNumbers(List.of("001", "002"))
            .build();

    // when
    LocalDateTime registeredDateTime = LocalDateTime.now();
    OrderResponse orderResponse = orderService.createOrder(request.toServiceRequest(), registeredDateTime);

    // then
    assertThat(orderResponse.getId()).isNotNull();
    assertThat(orderResponse)
            .extracting("registeredDateTime", "totalPrice")
            .contains(registeredDateTime, 4000);
    assertThat(orderResponse.getProducts()).hasSize(2)
            .extracting("productNumber", "price")
            .containsExactlyInAnyOrder(
                    tuple("001", 1000),
                    tuple("002", 3000)
            );
}

특징

  • 작성한 비즈니스 로직이 의도대로 동작하는지 확인하는 통합 테스트의 성격을 가진다.

     

  • Business Layer는 Persistence Layer를 의존하므로 이 둘을 통합적으로 테스트한다.


📌Presentation Layer

  • 외부 세계의 요청을 가장 먼저 받는 계층

  • 파라미터에 대한 최소한의 검증을 수행

  • 넘어온 값들의 검증(validation)이 중요 → 비즈니스 로직은 필요없음

    • Business Layer에서 비즈니스 로직을 전개하기 전에 값들이 유효한지 검증

       

class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @MockitoBean
    private ProductService productService;

    @Test
    @DisplayName("신규 상품을 등록한다.")
    void createProduct() throws Exception {
        // given
        ProductCreateRequest request = ProductCreateRequest.builder()
            .type(ProductType.HANDMADE)
            .sellingStatus(ProductSellingStatus.SELLING)
            .name("아메리카노")
            .price(4000)
            .build();

        // when // then
        // body 에 값을 넣어서 직렬화와 역직렬화가 발생
        mockMvc.perform(
                post("/api/v1/products/new")
                    .content(objectMapper.writeValueAsString(request))
                    .contentType(MediaType.APPLICATION_JSON)
            )
            .andDo(print())
            .andExpect(status().isOk());
    }
}

MockMvc : Mock(가짜) 객체를 사용해 스프링 MVC 동작을 재현할 수 있는 테스트 프레임워크

objectMapper : Java 객체를 JSON 문자열로 변환(직렬화), JSON 문자열을 Java 객체로 변환(역직렬화)을 수행

  • content(objectMapper.writeValueAsString(request)) -> 직렬화

  • controller의 @RequestBody 에 의해 역직렬화 수행

특징

  • Business Layer와 Persistence Layer는 Mocking을 사용하여 정상 동작을 하는 것으로 간주하고 테스트한다.

    • @MockitoBean 을 사용하여 ProductService를 Mocking

  • 애플리케이션을 띄우지 않고도 HTTP 요청/응답 흐름을 테스트할 수 있다.

  • Validation이 정상적으로 작동하는지 테스트할 수 있다.


📌 @DataJpaTest vs @SpringBootTest

가장 큰 차이점은 @Transactional 유무의 차이이다. @DataJpaTest 는 트랜잭션을 지원해서 테스트 종료 후 롤백을 실행하는 반면, @SpringBootTest 는 트랜잭션을 지원하지 않아 테스트 간에 간섭을 받지 않기 위해서는 클랜징 작업(deleteAllInBatch)이 필요하다.

추가적으로 @DataJpaTest@Entity, @Repository만 Bean으로 등록하기 때문에 Persistence Layer 테스트를 할 때만 사용하는 것이 바람직하다.

 

그렇다면 @SpringBootTest@Transactional 를 같이 사용하면 되는거 아닐까?

둘이 같이 사용하게 되면 테스트에서 롤백을 해줘서 편하지만 주의해서 사용해야한다. 만약 service(Business Layer)에서 @Transactional 을 빼먹어도, 테스트에서 사용한 트랜잭션으로 인해 테스트는 정상 통과되고, 기능에 문제가 없다고 판단할 경우가 생길 수도 있기 때문이다.

그렇기 때문에 편리함을 위해 사용해도 되지만 service에 @Transactional 이 빠지지 않았는지, 실수할 수도 있다는 인지를 하면서 사용하는 것이 좋다.


📌 Validation

org.springframework.boot:spring-boot-starter-validation 을 사용하여 클라이언트의 요청 값에 대한 검증을 할 수 있다.

@Getter
@NoArgsConstructor
public class ProductCreateRequest {

    @NotNull(message = "상품 타입은 필수입니다.")
    private ProductType type;

    @NotNull(message = "상품 판매 상태는 필수입니다.")
    private ProductSellingStatus sellingStatus;

    @NotBlank(message = "상품 이름은 필수입니다.")
    private String name;

    @Positive(message = "상품 가격은 양수여야 합니다.")
    private int price;
}
.
.
.
@PostMapping("/api/v1/products/new")
public ApiResponse<ProductResponse> createProduct(
    @Valid @RequestBody ProductCreateRequest request
) {
    return ApiResponse.ok(productService.createProduct(request));
}

💡 클라이언트의 요청을 받는 시점에 @Valid 가 적용된 객체를 검증할 수 있다.

String 검증

  • @NotNull : 빈 문자열(””), 공백 문자열 통과(” “)

  • @NotEmpty : 공백 문자열 통과(” “)

  • @NotBlank : 둘 다 통과할 수 없음 → 문자가 필수로 있어야 함

상품 이름은 20자 제한이 주어진다면

  • controller에서 valid를 사용해서 해야하는지 고민

  • 유효한 문자열이라면 가져야하는 합당한 검증과 도메인 정책에 맞는 특수한 형태의 검증을 구분할 줄 아는 시야를 길러야 한다.

     

    20자 제한은 서비스 계층, 생성자로 생성을 할 때 검증을 할 수 있다.


미션을 통해 배운 것들

📌 Day 11

스터디카페 프로젝트의 테스트 코드를 작성하면서 많은 고민들을 통해 배울 수 있었다. 테스트를 통해서 기능을 검증하는 것을 넘어서, '이 테스트를 왜 해야 하는지'에 초점을 두고 이번 미션을 진행했다. '안다고 생각하는 것'과 '아는 것' 의 차이는 정말 크구나 다시 한번 느낄 수 있었다.

강의를 보면서 강사님의 코드를 따라 칠때는 "생각보다 쉬운데?"라는 생각을 했다. 하지만 내 스스로 어떤 테스트를 해야 적합한지 생각하고, 이해하기 쉬운 DisplayName을 생각하는 것도 어렵게 느껴졌다. 실제로 "사물함을 구매할 수 없다" vs "사물함을 사용할 수 없다" 이 두 문장을 가지고 20분은 넘게 고민도 하고, 열심히 짜둔 테스트 코드가 적합하지 않은 것 같아서 싹 지우고 다시 작성하기도 했다.

테스트를 직접 작성하면서 "나는 지금까지 무책임한 개발을 하고 있었구나"라는 생각이 들었다. 실무에서의 테스트는 이 예제보다 더 복잡하고, 생각할 것도 많을텐데 개발을 하면서 테스트 코드 작성 경험을 하지 못한 것은 참 아쉽다는 생각도 들었다. 그래도 이번 미션을 통해 그 감을 조금 익힐 수 있었고, 부족한 부분과 보완해야 할 점을 알 수 있는 좋은 기회가 되었다.

 

출처: Readable Code: 읽기 좋은 코드를 작성하는 사고법

댓글을 작성해보세요.

채널톡 아이콘