기록

[JpaTest] Spring Layered Architecture와 Layer 테스트 전략 본문

교육/강의

[JpaTest] Spring Layered Architecture와 Layer 테스트 전략

youngyin 2025. 2. 3. 00:00

시작하면서

Spring 애플리케이션에서 각 레이어는 명확한 책임 분리를 통해 유지보수성과 테스트 가능성을 향상시키며, 각 레이어에 적합한 테스트 전략을 적용할 수 있습니다. 예를 들어, Controller Layer는 클라이언트 요청을 처리하고 Service Layer를 호출하며, Service Layer는 비즈니스 로직을 처리하고 Repository Layer는 데이터 접근을 담당합니다. 아래는 Controller, Service, Repository 레이어 각각의 테스트 방법과 그 전략을 설명합니다.


1. Controller Layer

역할:

  • 클라이언트의 요청(Request)을 수신하고, 적절한 Service를 호출하여 응답(Response)을 생성합니다.
  • 입력값 검증과 같은 최소한의 논리만 처리합니다.

테스트 전략:

  • Mock 객체 사용: Controller는 비즈니스 로직을 직접 포함하지 않으므로, Service를 Mocking하여 독립적으로 테스트합니다. 이는 Controller Layer의 테스트가 Service Layer의 로직에 의존하지 않고, 입력 검증 및 올바른 Service 호출 여부에 집중할 수 있도록 합니다.
  • 테스트 방법:
    • Spring MVC 테스트를 활용하여 요청과 응답의 동작을 검증합니다.
    • 요청 파라미터 검증 및 적절한 HTTP 응답 코드 반환을 확인합니다.

예제 코드:

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

    // when // then
    mockMvc.perform(
                post("/api/v1/products/new")
                        .content(objectMapper.writeValueAsString(request))
                        .contentType(MediaType.APPLICATION_JSON)
            )
            .andDo(print())
            .andExpect(status().isOk());
}

2. Service Layer

역할:

  • 비즈니스 로직을 구현하며, 데이터 접근을 위해 Repository를 호출합니다.
  • 트랜잭션 관리를 담당합니다.

테스트 전략:

  • 실제 객체 사용: Mock 객체 대신 실제 Repository를 주입받아 비즈니스 로직을 검증합니다.
  • 테스트 방법:
    • JUnit과 Spring Context를 이용하여 Service 계층의 동작을 통합적으로 검증합니다. Spring Context를 사용하면 애플리케이션의 실제 동작 환경을 모방하여 서비스 계층의 로직과 데이터 접근 계층 간의 상호작용을 테스트할 수 있습니다. 예를 들어, 트랜잭션 롤백 동작이나 실제 데이터베이스 상호작용을 포함한 테스트를 수행할 수 있습니다.
    • 다양한 입력값과 경계값에 대한 비즈니스 로직 처리를 테스트합니다.

예제 코드:

@Autowired
private ProductService productService;

@Autowired
private ProductRepository productRepository;

@DisplayName("신규상품을 등록할때, 상품번호는 가장 최근 상품의 상품번호에서 1 증가한 값으로 사용한다.")
@Test
void createProduct() {
    // given
    Product product1 = createProduct("001", HANDMADE, SELLING, "아메리카노", 4000);
    productRepository.saveAll(List.of(product1));

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

    // when
    ProductResponse response = productService.createProduct(createRequest);

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

3. Repository Layer

역할:

  • 데이터 접근 및 저장을 담당하며, CRUD 연산을 처리합니다.
  • 비즈니스 로직은 포함되지 않습니다.

테스트 전략:

  • 실제 객체 사용: Repository 계층은 데이터베이스와의 상호작용을 검증하기 위해 실제 데이터베이스를 사용합니다.
  • 테스트 방법:
    • Spring Data JPA의 @SpringBotTest를 활용하여 Repository의 동작을 테스트합니다.
    • 쿼리 메서드의 정확성과 데이터 일관성을 검증합니다.

예제 코드:

@Autowired
private ProductRepository productRepository;

@DisplayName("판매상태로 상품을 조회한다.")
@Test
void findAllBySellingStatusIn() {
    // given
    Product product1 = Product.builder()
            .productNumber("001")
            .type(HANDMADE)
            .sellingStatus(SELLING)
            .name("아메리카노")
            .price(4000)
            .build();
    Product product2 = Product.builder()
            .productNumber("002")
            .type(HANDMADE)
            .sellingStatus(HOLD)
            .name("카페라떼")
            .price(4500)
            .build();

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

    // when
    List<Product> products = productRepository.findAllBySellingStatusIn(List.of(SELLING, HOLD));

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

추가 배경: 테스트 기준에 대한 논의

사이드 프로젝트 중, "서비스 테스트에서 왜 Mock만 사용하냐? 실제 객체를 사용하는 기준은 무엇이냐?"라는 질문을 팀원에게 받은 경험이 있습니다. 이 질문을 통해 각 계층에서 Mock과 실제 객체의 사용 기준을 재검토하게 되었고, 이를 기반으로 테스트 전략을 구체화할 수 있었습니다. 이러한 기준은 각 레이어의 책임을 분리하고 테스트 목적에 맞는 적합한 방법을 선택하는 데 도움을 주었습니다. 이 질문을 계기로 테스트 방법에 대한 강의를 들었고, 해당 강의에서는 다음과 같은 기준으로 Mock과 실제 객체 사용을 구분하였습니다:

  • Mock 사용:
    • 외부 의존성이 있는 경우, 해당 의존성을 Mock으로 대체하여 독립적으로 테스트합니다.
    • 특정 계층의 기능만 검증할 때 사용됩니다 (예: Controller Layer).
  • 실제 객체 사용:
    • 비즈니스 로직의 복잡성을 검증하거나, 데이터베이스와의 상호작용을 포함한 통합 테스트를 수행할 때 사용합니다.
    • Repository 및 Service 계층에서는 실제 객체를 활용하여 더 깊이 있는 검증이 가능합니다.

이 기준을 통해, 각 계층에서의 테스트 전략을 더 명확히 구분하고, 필요한 수준의 검증을 적절히 수행할 수 있었습니다.


결론

위의 테스트 전략을 통해 각 레이어에 적합한 검증을 수행함으로써, 애플리케이션의 신뢰성을 높이고 유지보수성을 향상시킬 수 있습니다. Controller는 Mock 객체로 경량화된 테스트를 수행하고, Service와 Repository는 실제 객체를 사용하여 더욱 깊이 있는 검증을 진행합니다. 또한, Mock과 실제 객체의 사용 기준을 명확히 함으로써 테스트 설계의 명확성을 높였습니다.

Comments