[워밍업 클럽 3기 BE 클린코드&테스트] - 레이어드 아키텍처 주요 특징
Spring에서 애플리케이션을 설계할 때 흔히 사용하는 Layered Architecture(레이어드 아키텍처)는 다음과 같은 3가지 주요 계층으로 구성[Presentation Layer] → [Business Layer] → [Persistence Layer] (Controller) (Service) (Repository) Layered Architecture의 핵심 특징계층 분리 : 각 계층이 독립된 책임을 가지며, 역할이 명확히 나뉨독립성 : 상위 계층은 하위 계층을 의존하지만, 하위 계층은 상위 계층을 모름유연성 : 특정 계층을 변경하더라도 다른 계층에 영향이 적음 테스트 용이성 : 계층별 단위 테스트가 가능해 테스트 작성이 명확하고 쉬움1. Presentation Layer (UI 계층 - Controller)✅ 역할사용자 요청을 받아서 적절한 응답을 반환 (주로 JSON 형태)HTTP 관련 처리 (@RestController, @GetMapping 등)✅ 테스트 전략@WebMvcTest를 사용하여 컨트롤러만 테스트MockMvc + ObjectMapper로 HTTP 요청/응답 흐름을 검증Service는 @MockBean으로 주입📌 예시 코드WebMvcTest(controllers = OrderController.class) class OrderControllerTest { @Autowired MockMvc mockMvc; @Autowired ObjectMapper objectMapper; @MockBean OrderService orderService; @Test void 신규_주문_등록_성공() throws Exception { OrderCreateRequest request = OrderCreateRequest.builder() .productNumbers(List.of("001")) .build(); mockMvc.perform(post("/api/v1/orders/new") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value("200")) .andExpect(jsonPath("$.message").value("OK")); } @Test void 상품번호_없을때_예외() throws Exception { OrderCreateRequest request = OrderCreateRequest.builder().build(); mockMvc.perform(post("/api/v1/orders/new") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").value("상품 번호 리스트는 필수입니다.")); } } 2. Business Layer (서비스 계층 - Service)✅ 역할비즈니스 로직 처리의 중심트랜잭션 처리, 여러 레포지토리 호출, 외부 API 조합 등✅ 테스트 전략@SpringBootTest 또는 순수 JUnit 테스트로 비즈니스 로직 검증Repository는 필요 시 가짜(Mock) 또는 실제 DB 사용Happy Path + 예외 케이스 + 경계값 등 다양한 시나리오 테스트📌 예시 코드@SpringBootTest class ProductServiceTest { @Autowired private ProductService productService; @Autowired private ProductRepository productRepository; @AfterEach void tearDown() { productRepository.deleteAllInBatch(); ; } @DisplayName("신규 상품을 등록한다. 상품번호는 가장 최근 상품의 상품번호에서 1증가한 것이다.") @Test void createProduct() { //given Product product = createProduct("001", HANDMADE, SELLING, "아메리카노", 4000); productRepository.save(product); ProductCreateServiceRequest request = ProductCreateServiceRequest.builder() .type((HANDMADE)) .sellingStatus((SELLING)) .name("카푸치노") .price(5000) .build(); //when ProductResponse productResponse = productService.createProduct(request); //then assertThat(productResponse) .extracting("productNumber", "type", "sellingStatus", "name", "price") .contains("002", HANDMADE, SELLING, "카푸치노", 5000); List<Product> products = productRepository.findAll(); assertThat(products).hasSize(2) .extracting("productNumber", "type", "sellingStatus", "name", "price") .containsExactlyInAnyOrder( tuple("001", HANDMADE, SELLING, "아메리카노", 4000), tuple("002", HANDMADE, SELLING, "카푸치노", 5000) ); }주의: 테스트에서 @Transactional을 사용할 경우, 실제 코드와 작동 방식이 달라질 수 있음 → 필요에 따라 deleteAllInBatch()로 정리3. Persistence Layer (저장소 계층 - Repository)✅ 역할데이터베이스와 직접 상호작용 (CRUD, 조건 검색 등)JPA, MyBatis 등 ORM/쿼리 도구 사용✅ 테스트 전략@DataJpaTest를 사용하여 가벼운 JPA 테스트테스트 종료 시 자동으로 Rollback 처리됨실제 DB 조회/저장 동작 검증📌 예시 코드@ActiveProfiles("test") //@SpringBootTest @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) ); }요약Controller : HTTP 요청/응답 처리, @WebMvcTest + MockMvc로 요청 파라미터, 상태 코드, JSON 응답 값 등을 테스트Service : 비즈니스 로직 처리, @SpringBootTest or JUnit + Mockito로 로직 분기, 예외 처리, 흐름 제어 등 테스트Repository : 데이터 CRUD 및 조회, @DataJpaTest로 쿼리 정확성, 연관 관계 매핑, 조건 검색 등을 테스트