inflearn logo
강의

강의

N
챌린지

챌린지

멘토링

멘토링

N
클립

클립

로드맵

로드맵

지식공유

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

tteokboki
0

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

 

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

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

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

 

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

 

  1. 계층 분리

     

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

 

  1. 독립성

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

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

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

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

  1. 유연성

     

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

 

  1. 테스트 용이성

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

 

각 계층 테스트 코드 작성법

 

Presentation Layer

@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
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

@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

 

백엔드 레이어드아키텍쳐 워밍업클럽 테스트코드

답변 0