솔솔

[Test] Layered Architecture 구조적 이해와 단계별 테스트 작성법 본문

나의보물들/Test

[Test] Layered Architecture 구조적 이해와 단계별 테스트 작성법

솔솔하네 2025. 3. 25. 15:07
반응형

🍀 Layered Architecture란?


Layered Architecture(계층화 아키텍처)는 소프트웨어 시스템의 구성 요소를 목표한 대상의 역할과 책임에 따라 계층별로 분리(서로 독립적으로 동작)하여 설계하는 아키텍처 패턴이다. 

 

먼저 "아키텍처"라는 용어의 본질을 살펴보면,

"목표한 대상의 구성과 동작 원리, 관계, 환경 등을 설명하는 설계도"

 

즉, 소프트웨어 아키텍처는 시스템이 목표로 하는 동작과 목적을 달성하기 위해 구성 요소(모듈) 간의 역할, 책임, 그리고 상호작용을 명확히 정의하는 체계를 의미한다. 

 

 

🍀  3-Layer Architecture 와 3-Tier Architecture 차이


* 둘다 자주 쓰는 용어인데 헷갈려서 정확히 구분하기 위해 찾아봄

   3-Tier Architecture 애플리케이션 전체의 물리적 배포와 구성에 초점을 둔 설계 방식

   3-Layer Architecture 소프트웨어 설계의 논리적 계층화를 목적으로 한 아키텍처

 

구분 3-Layer Architecture 3-Tier Architecture
초점 애플리케이션의 논리적 설계 구조 시스템의 물리적 배포 구조
구성요소 프레젠테이션, 비즈니스, 데이터 접근 계층 클라이언트(Presentation), 서버(Application), 데이터베이스(Data)
운영방식 계층별로 논리적으로 분리된 코드로 동작 계층별로 다른 서버에 배포 가능
목적 유지보수성, 코드 재사용성, 계층 간 책임 분리 시스템 확장성, 네트워크 통신 최적화, 물리적 분리
에시 Controller + Service + Repository Vue.js(클라이언트) + Spring(서버) + MySQL(데이터베이스)

 

 

🍀 3-Layer Architecture 구성 요소와 역할


 

 

일부 아이콘 커버는 Flaticon.com의 자료를 사용해 디자인되었습니다 / 솔솔제작 불펌 금지

 

1. Presentation Layer (프레젠테이션 계층)

  • 사용자와 애플리케이션 간의 인터페이스 역할을 하며, 요청을 받아 비즈니스 로직에 전달하고, 결과를 사용자에게 반환함
  • @Controller 클래스

2. Business Layer (비즈니스 계층)

  • 애플리케이션의 핵심 비즈니스 로직을 처리하며, Presentation Layer와 Persistence Layer 간의 중재 역할
  • @Service 클래스

3. Persistence Layer (데이터 접근 계층)

  • 데이터베이스와 상호작용하며, 데이터를 저장, 검색, 수정, 삭제하는 역할
  • @Repository 클래스

 

🍀 3-Layer Architecture 각 계층별 테스트 작성법


💡 Layered Architecture에서 계층별 테스트를 해야하는 이유

 

    앞서 설명한 Layered Architecture의 핵심은 각 계층이 "목표한 대상"을 정확히 처리하도록 설계된 것이다.
    각 계층의 책임과 역할이 분리되어 있기 때문에, 테스트도 이에 맞게 독립적으로 수행되어야 한다.

    결론적으로 유지보수성과 확장성을 높이기 위한 목적!!

 

 

1. Presentation Layer 테스트

  • 테스트 목표: 입력 값이 올바르게 검증되고, 비즈니스 계층과 정확히 상호작용하는지 확인
  • 테스트 방식: HTTP 요청/응답 단위 테스트
// @WebMvcTest : Spring MVC 관련 컴포넌트만 테스트할 수 있도록 도와주는 어노테이션
// 컨트롤러 계층만을 테스트하려는 경우에 사용
@WebMvcTest(controllers = OrderController.class) 
class OrderControllerTest { 

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper; // post에서 objectMapper 필요함

	// @MockitoBean : @WebMvcTest와 함께 사용하여 서비스 계층이나 리포지토리 계층을 모킹(mocking)하여 테스트
    @MockitoBean
    private OrderService orderService;
    
    @DisplayName("신규 상품을 등록한다.")
    @Test
    void createProduct() 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"));
    }

}

 

2. Service Layer 테스트

  • 테스트 목표: 비즈니스 규칙이 정확히 적용되고, 다양한 시나리오를 올바르게 처리하는지 확인
  • 테스트 방식: 비즈니스 로직에 대한 단위 테스트
// @SpringBootTest : 전체 Spring 컨텍스트를 로드하여 실제 애플리케이션의 동작 환경에서 통합 테스트를 수행할 수 있도록 해주는 어노테이션
@SpringBootTest
class OrderServiceTest {

    @Autowired
    private OrderService orderService;

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

    @DisplayName("중복되는 상품번호 리스트로 주문을 생성할 수 있다.")
    @Test
    void createOrderWithDuplicateProductNumbers() {
        //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));

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

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

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


    }

}

 

3. Persistens Layer 테스트:

  • 테스트 목표: 데이터베이스와의 연동이 올바르게 동작하며, SQL 쿼리가 예상한 대로 작동하는지 확인.
  • 테스트 방식: Entity 또는 Repository의 CRUD 동작 테스트
// @DataJpaTest : JPA 관련 컴포넌트만 로드하여, 데이터베이스와 관련된 테스트를 수행하는 데 사용되는 어노테이션
@DataJpaTest
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)
                );
    }
}