레이어드 아키텍처 특징 및 테스트 작성법 알아보기

글은 박우빈님의 Practical-Testing 강의 참조하여 작성한 글입니다.

 

레이어드 아키텍처(Layered Architecture)란?

소프트웨어 시스템을 계층(layer)으로 나누어 구성하는 설계 방식으로, 각 계층이 특정한 역할과 책임을 갖도록 설계하는 것을 의미한다.

이렇게 각 계층들을 관심사를 기준으로 분리함으로써 계층의 응집도를 높이고 결합도를 낮출 수 있다.
-> 유지보수성이 올라감!

 

레이어드 아키텍처의 주요 특징

 

  1. 계층 분리

     

    Layered Architecture에서는 보통 3개의 Layer로 구성되어 있다.

  • Presentation Layer (UI 계층): 사용자의 요청 및 응답을 처리하며 상호작용 한다.

  • Business Layer (서비스 계층): 비즈니스 로직을 수행하는 책임을 지닌다.

  • Persistence Layer (비즈니스 계층): DB에 접근하여 상호작용(데이터 CRUD) 한다.

     

     

 

  1. 독립성

상위 계층은 하위 계층에 의존하지만,
하위 계층은 상위 계층에 대한 지식이나 정보를 가지지 않아야 한다.

e.g, Presentation Layer에서는 하위 계층인 Business Layer를 의존한다.

이때 Business Layer는 상위 계층인 Presentation Layer에서 넘어온 데이터로 비즈니스 로직을 처리할 뿐이며,

Presentation Layer 에 대해 알고 있지 않다!

  1. 유연성

     

특정 계층의 구현을 변경하더라도 다른 계층에는 영향을 미치지 않도록 설계할 수 있다.

 

  1. 테스트 용이성

계층별로 테스트가 가능하므로, 각 계층의 단위 테스트를 독립적으로 수행할 수 있다.

 

각 계층 테스트 코드 작성법

 

Presentation Layer

  • API의 요청-응답 흐름과 응답 형식(HTTP 상태 코드와 JSON 응답 데이터)을 검증하는 테스트를 작성한다.

  • @WebMvcTest 를 통해 테스트 하고자 하는 컨트롤러를 등록해준다.

  • 해당 컨트롤러를 테스트 하기위해 필요한 Business 계층(Service 클래스)를 @MockBean을 통해 주입하며, MockMvc 또한 의존성 주입해준다.

  • 이 때 json과의 소통이 필요하므로 객체를 json, json을 객채로 변환할 수 있게끔 ObjectMapper 또한 주입해주자.

@WebMvcTest(controllers = OrderController.class)
class OrderControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    @MockBean
    OrderService orderService;

    @DisplayName("신규 주문을 등록한다.")
    @Test
    void createOrder() throws Exception {
        // given
        OrderCreateRequest request = OrderCreateRequest.builder()
                .productNumbers(List.of("001"))
                .build();

        // when // then
        mockMvc.perform(
                        post("/api/v1/orders/new")
                                .content(objectMapper.writeValueAsString(request))
                                .contentType(MediaType.APPLICATION_JSON)
                )
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value("200"))
                .andExpect(jsonPath("$.status").value("OK"))
                .andExpect(jsonPath("$.message").value("OK"));
    }

    @DisplayName("신규 주문을 등록할 때 상품번호는 1개 이상이어야 한다.")
    @Test
    void createOrderWithEmptyProductNumbers() throws Exception {
        // given
        OrderCreateRequest request = OrderCreateRequest.builder()
                .build();
        // when // then
        mockMvc.perform(
                        post("/api/v1/orders/new")
                                .content(objectMapper.writeValueAsString(request))
                                .contentType(MediaType.APPLICATION_JSON)
                )
                .andDo(print())
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("400"))
                .andExpect(jsonPath("$.status").value("BAD_REQUEST"))
                .andExpect(jsonPath("$.message").value("상품 번호 리스트는 필수입니다."))
                .andExpect(jsonPath("$.data").isEmpty());
    }
}

 

Business Layer

  • 작성한 비지니스 로직이 의도한 대로 작동하는지 검증하는 테스트를 작성한다.

  • @SpringBootTest 을 통해 모든 Bean 의존성 주입함으로써 통합 테스트가 가능하다.

  • 해피케이스 뿐만 아니라 예외 테스트, 경계값 테스트 등 여러 케이스에 대해 테스트를 작성하자.

  • @Transactional vs sql을 이용한 데이터 삭제

  • 실제 코드에서는 @Transactional을 사용하지 않는데, 단순히 롤백만을 위해 테스트코드에서 @Transactional을 사용하면, 실제 작동 방식과 다르게 작동할 수 있다.
    따라서 테스트 코드 작성시 Transactional의 부작용에 대해 인지하고 사용할 것!!

@SpringBootTest
class ProductServiceTest {

    @Autowired
    private ProductService productService;

    @Autowired
    private ProductRepository productRepository;

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

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

        productRepository.save(product1);

        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", "price", "name")
                .contains("002", HANDMADE, SELLING, 5000, "카푸치노");

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

 

Persistence Layer

  • 데이터에 접근하는 역할로 데이터 CRUD와 연관된 메서드들을 테스트 한다.

     

  • @DataJpaTest

     

    • JPA와 관련된 의존성들만 주입해준다 -> @SpringBootTest보다 가볍다.

    • 어노테이션 내부에 @Transactional이 포함되어 있어 테스트 후 데이터가 롤백된다.

@DataJpaTest // @DataJpaTest안에 @Transactional이 걸려있어서 자동으로 rollback이 됨
class ProductRepositoryTest {
    @Autowired
    private ProductRepository productRepository;

    @DisplayName("원하는 판매상태를 가진 상품들을 조회한다.")
    @Test
    void findAllBySellingStatusIn() {
        //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.findAllBySellingStatusIn(List.of(SELLING, HOLD));
        //then
        assertThat(products).hasSize(2)
                .extracting("productNumber", "name", "sellingStatus")
                .containsExactlyInAnyOrder(
                        tuple("001", "아메리카노", SELLING),
                        tuple("002", "카페라떼", HOLD)
                );
    }
}

Reference

https://www.inflearn.com/course/practical-testing-%EC%8B%A4%EC%9A%A9%EC%A0%81%EC%9D%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%80%EC%9D%B4%EB%93%9C/dashboard

 

댓글을 작성해보세요.


채널톡 아이콘