Search

Layered Architecure 구조별 테스트 작성법

태그
Spring
Test
담당자
생성일
2024/10/25
순서
8
persistence Layer - Data Access의 역할 비지니스 가공 로직이 포함되어서는 안된다. - Data에 대한 CRUD에만 집중한 레이어
business Layer - 비지니스 로직을 구현하는 역할 - Persistence Layer와의 상호작용 (Data를 읽고 쓰는 행위)를 통해 비지니스 로직을 전개시킨다. - 트랜잭션을 보장해야 한다.
Presentation Layer - 외부 세계의 요청을 가장 먼저 받는 계층 - 파라미터에 대한 최소한의 검증을 수행한다.
글을 작성하면서 개인적인 생각을 작성해보도록 하겠습니다.
코드를 작성하면서 가장 자주 마주하는 문제 중 하나는 Persistence Layer에서 발생하는 예외입니다. 객체 초기화 시 누락된 값으로 인한 예외나, null 값을 Enum으로 변환할 때 생기는 예외 등 생각보다 많은 예외가 이 레이어에서 발생하곤 합니다.
많은 사람들이 Business Layer가 중요하다고 생각하지만, 개인적으로는 Persistence Layer의 비중이 더 크다고 봅니다. 비즈니스 로직의 핵심은 검증된 데이터를 가공하는 것이며, 모든 데이터가 정확하고 필요한 위치에 잘 들어가 있다면 비즈니스 로직을 구현하는 것이 오히려 수월해질 수 있습니다. 그러나 데이터가 예상과 다르거나 올바르게 저장되지 않으면, 비즈니스를 구현하기 전에 항상 값의 유효성을 확인하고 검증하는 번거로움이 발생합니다. 이로 인해 데이터에 대한 신뢰가 떨어지게 되고, 코드의 복잡성이 증가하게 됩니다.
따라서, 각 레이어의 테스트 코드가 모두 중요하지만, 개인적으로 Persistence Layer에 대한 테스트의 비중을 더 높게 두는 편입니다. 이 레이어에서의 안정성이 확보되면, 나머지 비즈니스 로직 구현이 훨씬 수월해지기 때문입니다.
강의를 진행하면서 중요한 부분들을 깨알같이 짚어주는 느낌이 듭니다.
특히 아래와 같은 내용들이 인상 깊었습니다
@SpringBootTest, @DataJpaTest, @WebMvcTest의 차이점
@SpringBootTest : @Transaction이 없음
@DataJpaTest : @Transaction이 존재
클래스 상단의 @Transactional 선언과 테스트 코드 일관성: 클래스에 @Transactional을 선언하면 더티 체킹(Dirty Checking)으로 인해 테스트 결과가 달라질 수 있다는 점도 중요한 포인트입니다. 특히 테스트에서의 일관성을 유지하려면 이 부분을 신중하게 처리해야 합니다.
CQRS와 @Transaction, @Transactional(readOnly = true)의 사용법과 master와 slave를 나눠서 사용하는법 :
클래스 상단에 기본으로 @Transactional(readOnly = true) 사용하고 update, insert, delete 는 @Transaction을 통해 데이터를 가공하자, 면접에서 받은 질문 중 하나가 생각납니다. 함수에서 각각의 데이터를 조회할 때 트랜잭션이 걸려 있지 않으면, 매번 새로운 DB 커넥션을 생성하게 되어 비효율적이라는 것이었습니다. 이 문제를 해결하기 위해 @Transactional(readOnly = true)를 사용하면, 조회 작업 시 커넥션 풀을 통해 이미 사용 중인 커넥션을 계속 재사용할 수 있어 훨씬 효율적이라는 것입니다.
serviceRequest 객체와, response 객체가 왜 필요한가? 왜 의존성을 두개로 나눠서 하면 좋은지 차후에 확장성 면에서 좋다는 점도 좋았습니다.
몰랐던 테스트코드 작성법은 작성하고, 적용하겠습니다.
List<Item> itemList = new ArrayList<>(); Item item1 = Item.builder() .id(1L) .name("자전거") .price(new BigDecimal(100000)) .build(); Item item2 = Item.builder() .id(2L) .name("킥보드") .price(new BigDecimal(200000)) .build(); itemList.add(item1); itemList.add(item2); Items items = new Items(itemList); //when //then Assertions.assertThat(items.getItemList()) .hasSize(2) .extracting("id", "name", "price") // 검증하고자 하는 필드 .containsExactlyInAnyOrder( tuple(1L, "자전거", new BigDecimal(100000)), tuple(2L, "킥보드", new BigDecimal(200000)) ); // 순서 상관없이 확인 해준다.
Java
복사
@WebMvcTest(controllers = ProductController.class) class ProductControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; // 직렬화 역직렬화 @MockBean private ProductService productService; @DisplayName("신규 상품을 등록한다.") @Test void createProduct() { // given ProductCreateRequest request = ProductCreateRequest.builder() .type(ProductType.HANDMADE) .sellingStatus(ProductSellingStatus.SELLING) .name("아메리카노") .price(4000) .build(); mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/products/new") .content(objectMapper.writeValueAsString(request)) .contentType(MediaType.APPLICATION_JSON) ).andExpect(MockMvcRequestMatchers.status().isOk()); } }
Java
복사