들어가며
앞서 Controller → Service → Domain → Repository 계층의 데이터 흐름과 각 계층에서 발생할 수 있는 문제를 다루었습니다. 이번 3부에서는 효율적인 테스트 코드 작성 방법과 테스트를 통한 유지보수 전략에 대해 이야기하겠습니다.
왜 테스트 코드가 중요한가요?
- 테스트 코드는 예상하지 못한 오류를 사전에 방지하고, 기능 추가나 수정 후 기존 기능이 잘 동작하는지 확인할 수 있는 "안전망" 역할을 합니다.
- 특히 계층형 아키텍처에서는 각 계층별로 테스트를 작성하여, 각 계층의 역할이 잘 수행되고 있는지 검증할 수 있습니다.
1. 계층별 테스트 작성 전략
테스트 코드는 일반적으로 다음과 같은 범주로 나눌 수 있습니다:
- 단위 테스트(Unit Test): 특정 메서드나 클래스의 동작을 독립적으로 검증.
- 통합 테스트(Integration Test): 여러 계층이 함께 동작할 때의 흐름을 검증.
- 엔드 투 엔드 테스트(End-to-End Test): 실제 사용자 시나리오를 기반으로 전체 시스템을 검증.
각 계층별로 어떤 테스트를 작성해야 하는지 살펴보겠습니다.
1.1 Controller 테스트
- 목적: HTTP 요청이 올바르게 처리되고, Service 계층과 올바르게 연동되는지 검증.
- 테스트 종류
- 단위 테스트: Controller 메서드의 요청 처리와 응답 검증.
- 통합 테스트(MockMvc): 요청 라우팅과 응답 상태 코드 검증.
Controller 테스트 예제 (MockMvc 사용):
@WebMvcTest(PostController.class)
public class PostControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private PostService postService;
@Test
void savePost_shouldReturnCreatedPost() throws Exception {
// 가짜 데이터 설정
PostDto mockPost = new PostDto(1L, "제목", "내용");
when(postService.savePost(any(PostDto.class))).thenReturn(mockPost);
// 요청 실행 및 결과 검증
mockMvc.perform(post("/posts")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\": \"제목\", \"content\": \"내용\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1L))
.andExpect(jsonPath("$.title").value("제목"))
.andExpect(jsonPath("$.content").value("내용"));
}
}
1.2 Service 테스트
- 목적: 비즈니스 로직이 예상대로 동작하며, Repository와 올바르게 연동되는지 검증.
- 테스트 종류
- 단위 테스트: 비즈니스 로직 검증 (Mock 객체를 사용).
- 통합 테스트: 실제 Repository 계층과 연동하여 데이터 흐름 검증.
Service 테스트 예제 (Mock Repository 사용):
java
코드 복사
@ExtendWith(MockitoExtension.class) public class PostServiceTest { @Mock private PostRepository postRepository; @InjectMocks private PostService postService; @Test void savePost_shouldSaveAndReturnPost() { // Mock 데이터 설정 Post post = new Post("제목", "내용"); when(postRepository.save(any(Post.class))).thenReturn(post); // 테스트 실행 PostDto result = postService.savePost(new PostDto(null, "제목", "내용")); // 결과 검증 assertEquals("제목", result.getTitle()); assertEquals("내용", result.getContent()); verify(postRepository, times(1)).save(any(Post.class)); } }
1.3 Domain 테스트
- 목적: 도메인 객체의 규칙과 메서드가 올바르게 동작하는지 검증.
- 테스트 종류
- 단위 테스트: 도메인 객체의 메서드와 규칙 검증.
Domain 테스트 예제:
@ExtendWith(MockitoExtension.class)
public class PostServiceTest {
@Mock
private PostRepository postRepository;
@InjectMocks
private PostService postService;
@Test
void savePost_shouldSaveAndReturnPost() {
// Mock 데이터 설정
Post post = new Post("제목", "내용");
when(postRepository.save(any(Post.class))).thenReturn(post);
// 테스트 실행
PostDto result = postService.savePost(new PostDto(null, "제목", "내용"));
// 결과 검증
assertEquals("제목", result.getTitle());
assertEquals("내용", result.getContent());
verify(postRepository, times(1)).save(any(Post.class));
}
}
1.4 Repository 테스트
- 목적: 데이터베이스와의 연동이 올바르게 작동하는지 검증.
- 테스트 종류
- 통합 테스트: Spring Boot와 실제 데이터베이스(H2 등)를 사용하여 CRUD 동작 검증.
Repository 테스트 예제 (H2 데이터베이스 사용):
@DataJpaTest
public class PostRepositoryTest {
@Autowired
private PostRepository postRepository;
@Test
void saveAndFindPost_shouldWorkCorrectly() {
// 데이터 저장
Post post = new Post("제목", "내용");
Post savedPost = postRepository.save(post);
// 데이터 조회
Post foundPost = postRepository.findById(savedPost.getId()).orElse(null);
// 검증
assertNotNull(foundPost);
assertEquals("제목", foundPost.getTitle());
assertEquals("내용", foundPost.getContent());
}
}
2. 효율적인 테스트 전략
2.1 테스트 우선순위
모든 계층에서 테스트를 작성하려면 많은 리소스가 필요하므로, 중요도가 높은 테스트에 우선순위를 둡니다.
-
최우선
- Service 계층: 비즈니스 로직의 중심이므로 반드시 검증해야 합니다.
- Repository 계층: 데이터 저장/조회 문제가 없도록 통합 테스트를 작성합니다.
-
다음 우선
- Controller 계층: 요청과 응답이 예상대로 동작하는지 MockMvc로 테스트.
-
선택적
- Domain 계층: 간단한 도메인 객체는 테스트를 생략할 수 있지만, 비즈니스 규칙이 복잡한 경우 반드시 검증합니다.
2.2 Mock과 실제 객체의 적절한 사용
-
Mock 객체 사용
- Service 계층 테스트에서, Repository를 Mock 객체로 대체하여 외부 의존성을 제거하고 빠르게 테스트를 실행합니다.
-
실제 객체 사용
- Repository 테스트에서 실제 데이터베이스(H2 등)를 사용하여 CRUD 동작을 검증합니다.
2.3 테스트 환경 자동화
- Spring Boot에서는
@SpringBootTest,@DataJpaTest,@WebMvcTest등의 어노테이션을 통해 테스트 환경을 자동으로 설정할 수 있습니다. - 데이터베이스를 사용하는 테스트에서는 H2와 같은 In-Memory 데이터베이스를 사용하여 빠르고 간단하게 테스트를 실행합니다.
3. 유지보수 전략
3.1 주기적인 테스트 코드 리뷰
- 테스트 코드도 일반 코드처럼 주기적으로 리뷰하여 불필요하거나 중복된 테스트를 제거합니다.
- 테스트의 목적이 명확하게 정의되어 있는지 확인합니다.
3.2 CI/CD 파이프라인에 테스트 통합
- CI/CD 파이프라인에 테스트를 통합하여, 코드 변경 시마다 자동으로 모든 테스트를 실행합니다.
- 실패하는 테스트가 있으면 코드를 배포하지 않도록 설정합니다.
3.3 테스트 커버리지 측정
- Jacoco와 같은 도구를 사용하여 테스트 커버리지를 측정합니다.
- 테스트 커버리지가 낮은 부분을 확인하고, 추가적인 테스트를 작성합니다.
3.4 테스트를 기반으로 리팩토링
- 테스트 코드가 존재하면 리팩토링 중에 발생할 수 있는 오류를 쉽게 발견할 수 있습니다.
- 코드 리팩토링 시, 테스트를 실행하여 기존 기능이 정상 동작하는지 확인합니다.
3부 마무리
이 글에서는 계층별 테스트 작성 방법과 효율적인 유지보수 전략에 대해 알아보았습니다. 테스트 코드는 코드의 품질과 안정성을 유지하는 데 중요한 역할을 하며, 이를 기반으로 코드를 지속적으로 개선할 수 있습니다.
다음 단계에서는 이 계층형 아키텍처를 바탕으로 실제 서비스에 어떻게 적용할 수 있는지에 대해 더 구체적인 사례를 다뤄보겠습니다.