스타트업 개발자로 일하면서 일정에 맞춰 빠르게 기능을 개발하는게 중요하다고 생각해 테스트 코드를 작성하지 않고 Postman으로만 테스트를 해왔다. 하지만 프로젝트가 점점 커지면서 점점 다양한 경우의 수를 커버해야 하는 상황이 많아졌다. 단순히 기능이 동작하는지 확인하는 것만으로는 부족했고, 예상치 못한 시나리오에서 발생하는 오류를 사전에 막아야 했다. 이를 해결하려면 테스트 코드 작성이 필요하다는 것을 느꼈다🧑💻
테스트코드를 작성하기 시작하면서 어떻게 하면 더 효율적으로 접근할 수 있을지 궁금해졌다.
이런내용을 다른 개발자와 커피챗을 하며 이야기했고 테스트 피라미드(Test Pyramid)라는 개념을 알게 되었다.
이 글에서는 테스트 코드 작성의 필요성과 테스트 피라미드를 활용한 효율적인 테스트 방법, 간단한 방법론 등을 정리했다. Java-Spring 환경에서 테스트 코드를 처음 작성하려는 개발자들에게 조금이라도 도움이 되길 바란다! ✨
1. 테스트란 무엇이며, 왜 중요한가?
테스트는 단순히 코드가 제대로 동작하는지 확인하는 과정이 아니다. 코드 품질과 안정성을 높이고, 협업 과정에서 신뢰를 쌓는 중요한 도구다. 특히 Java-Spring 개발 환경에서는 테스트를 통해 예기치 못한 에러를 사전에 방지하고, 코드 변경 시 발생할 수 있는 리스크를 줄일 수 있다.
테스트를 작성하지 않는다면? 작은 코드 변경도 예상치 못한 큰 문제를 일으킬 수 있다. 이러한 문제를 사전에 방지하지 못하면 결국 개발 속도를 늦추게 되고, 팀원 간의 신뢰에도 영향을 미친다. 그래서 테스트 코드는 단순한 검증을 넘어, 안정적인 개발과 협업의 기반이라고 할 수 있다.
2. 테스트 피라미드와 효율적인 테스트 구조
테스트 피라미드(Test Pyramid)는 소프트웨어 개발자인 마틴 파울러(Martin Fowler)가 소개한 것으로, 테스트를 체계적으로 나누고 관리하는 데 유용하다. 이 피라미드는 유닛 테스트, 통합 테스트, E2E 테스트라는 세 가지 계층으로 구성되며, 각 테스트가 개발 프로세스에서 어떤 역할을 해야 하는지 보여준다
테스트 피라미드의 세 계층
- 유닛 테스트:
- 애플리케이션의 가장 작은 단위의 테스트
- 빠른 실행 속도와 쉬운 유지보수성을 갖추고 있으며, 전체 테스트의 60~70%를 차지해야 함
- 단일 메서드나 클래스의 동작을 검증하는 데 적합
- 통합 테스트:
- 여러 컴포넌트가 상호작용할 때의 동작을 검증
- 예를 들어, Service 계층과 Repository 계층 간의 데이터 연동이 잘 이루어지는지 확인
- Spring Boot에서는 @SpringBootTest와 @MockBean 등을 활용해 통합 테스트 작성
- E2E 테스트 (End-to-End):
- 사용자 시나리오를 기반으로 애플리케이션 전체의 흐름 검증
- 작성과 유지보수가 어려우므로, 결제나 회원가입 같은 중요한 경로에만 적용하는 것이 효율적
- Rest Assured나 Selenium과 같은 도구를 사용해 자동화할 수 있음
내가 그동안 해왔던 포스트맨을 활용한 테스트는 API 테스트로 일종에 E2E 테스트 이다.
방법론적 접근과의 연결
테스트 피라미드는 테스트 전략의 방향성을 제시하지만, 구체적인 작성 방법은 별도로 고려해야 한다. 이를 위해 테스트 주도 개발(TDD)이나 동작 주도 개발(BDD) 같은 방법론이 큰 도움을 줄 수 있다.
- TDD(Test-Driven Development):
- 테스트를 먼저 작성하고, 테스트를 통과할 만큼의 코드를 작성한 후 리팩토링하는 방식이다.
- 이 과정에서 코드 품질을 자연스럽게 개선하고, 불필요한 코드 생성을 방지할 수 있다.
- BDD(Behavior-Driven Development):
- 테스트를 사용자 관점에서 작성하며, Given-When-Then 형식으로 요구 사항을 명확히 표현한다.
이처럼 방법론적 접근은 테스트 피라미드를 실제로 구현할 때 유용한 도구가 된다.
3. Java-Spring에서 테스트 작성 방법
Java-Spring 환경에서는 다양한 테스트 도구를 활용해 각 계층의 테스트를 작성할 수 있다
- 유닛 테스트
- JUnit과 Mockito를 사용해 독립적인 테스트를 작성. Mock 객체를 통해 외부 의존성을 제거하여 테스트를 간결하게 만들 수 있음
- 예제: Service 계층의 유닛 테스트
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class UserServiceTest {
@Mock
private UserRepository userRepository; // Mock 객체 생성
@InjectMocks
private UserService userService; // 테스트 대상(Service)
public UserServiceTest() {
MockitoAnnotations.openMocks(this); // Mock 초기화
}
@Test
void testFindUserById() {
// given: Mock 데이터 정의
User mockUser = new User(1L, "yuri", "yuri@gmail.com");
when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
// when: 테스트 대상 호출
User result = userService.findUserById(1L);
// then: 결과 검증
assertNotNull(result);
assertEquals("John Doe", result.getName());
verify(userRepository, times(1)).findById(1L); // Mock 객체 메서드 호출 검증
}
}
- 통합 테스트
- @SpringBootTest와 @MockBean을 활용해 Spring Context에서의 연동 동작을 검증
- 예제: Repository와 Service 간 통합 테스트
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
public class UserServiceIntegrationTest {
@Autowired
private UserService userService; // 실제 Bean
@MockBean
private UserRepository userRepository; // MockBean으로 대체
@Test
void testFindUserByEmail() {
// given: Mock 데이터 정의
User mockUser = new User(1L, "yuri", "yuri@gmail.com");
when(userRepository.findByEmail("yuri@gmail.com")).thenReturn(mockUser);
// when: 서비스 호출
User result = userService.findUserByEmail("yuri@gmail.com");
// then: 결과 검증
assertNotNull(result);
assertEquals("yuri", result.getName());
verify(userRepository, times(1)).findByEmail("yuri@gmail.com");
}
}
- REST API 테스트
- Rest Assured를 사용해 API 호출 및 응답을 자동으로 테스트
- 예제: Controller의 REST API 테스트
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
public class UserControllerApiTest {
@BeforeAll
public static void setup() {
RestAssured.baseURI = "http://localhost";
RestAssured.port = 8080; // 애플리케이션 실행 중이어야 함
}
@Test
void testGetUserById() {
// given: API 요청
Response response = given()
.pathParam("id", 1)
.when()
.get("/api/users/{id}");
// then: 응답 검증
response.then()
.statusCode(200)
.body("name", equalTo("yuri"))
.body("email", equalTo("yuri@gmail.com"));
}
}
Spring은 강력한 테스트 지원 기능을 제공하기 때문에, 이를 잘 활용하면 코드가 안정적으로 동작하는지 효율적으로 검증할 수 있다
4. 스타트업에서의 테스트 현실과 대안
스타트업처럼 빠른 출시가 중요한 환경에서는 테스트가 뒤로 밀리거나 생략되는 경우가 많다. 현실적으로 제한된 리소스와 시간 때문에 테스트보다는 기능 구현이 우선되는 경우가 대부분이다. 이 과정에서 "테스트는 나중에 하자"는 판단을 내리기 쉽다. 그러나 이런 접근은 장기적으로 유지보수 비용을 증가시키고, 예기치 못한 문제를 더 자주 유발할 수 있다.
그렇다면 어떻게 해야 할까?
스타트업 환경에서 테스트를 효율적으로 관리하기 위한 대안은 다음과 같다:
- 핵심 기능에 집중: 결제, 로그인 같은 중요한 기능만 우선적으로 테스트한다.
- 유닛 테스트 우선: 빠르게 작성할 수 있고 기본적인 안정성을 보장한다.
- 자동화 도구 활용: CI/CD 파이프라인에 핵심 테스트를 포함시켜 자동으로 실행되도록 설정한다.
- 점진적 테스트 확장: 처음부터 모든 것을 완벽히 하려고 하지 말고, 간단한 테스트부터 시작해 점진적으로 확장한다.
테스트는 선택이 아니라, 장기적으로 비즈니스 리스크를 줄이는 투자라는 관점을 가져야 할 것 같다
5. 테스트를 통해 얻는 장기적 이점
테스트는 단순히 "추가 작업"이 아니다. 장기적으로 보면, 개발 팀과 비즈니스에 다음과 같은 이점을 제공한다
- 코드 품질 향상: 테스트를 통해 예상치 못한 에러를 사전에 방지할 수 있다.
- 빠른 디버깅: 문제가 발생했을 때, 원인을 빠르게 파악하고 해결할 수 있다.
- 안정적인 배포: 테스트 자동화를 통해 배포 전에 품질을 확실히 보장할 수 있다.
- 협업 강화: 팀원 간 코드 신뢰를 형성하고, 유지보수를 쉽게 만든다.
테스트는 처음엔 작성 시간이 소요되지만, 장기적으로는 코드 변경과 유지보수를 더 빠르고 안전하게 만들어 준다. 테스트를 작성할수록 "왜 미리 하지 않았을까?"라는 생각이 들 정도로, 그 효과는 크다고 한다. 나는 아직 진행 중😅
결론
테스트 코드는 단순히 오류를 막기 위한 도구가 아니다. 팀과 코드에 신뢰를 쌓고, 소프트웨어의 장기적인 성공을 보장하는 핵심 요소다. 테스트 피라미드 같은 가이드라인과 TDD, BDD, 계약 기반 테스트 같은 방법론을 참고하면 테스트 작성의 효율성을 더 높일 수 있다.
Java-Spring 환경에서는 유닛 테스트와 통합 테스트를 통해 코드의 안정성을 확보할 수 있다. 특히 스타트업 환경에서는 처음부터 완벽한 테스트를 목표로 하기보다는, 핵심 기능부터 테스트를 작성하고 점진적으로 확장하는 방식을 선택하면 효과적이다.
테스트는 추가 업무가 아니라, 성공적인 소프트웨어 개발의 기반이라는 점을 잊지 말고 테스트를 지금 바로 시작해보자! 🚀
'개발 하나둘셋 > Java & Spring' 카테고리의 다른 글
서버-클라이언트 연결로 실시간 상태 전달하는 SSE 특징 및 적용기 (0) | 2024.11.17 |
---|---|
Java와 Spring에서의 비동기 처리 @Async와 CompletableFuture (2) | 2024.11.08 |
FFmpeg로 자막(srt) 삽입하여 영상 인코딩 시 자막이 깨지는 문제! Troubleshooting (0) | 2024.06.23 |
Spring Boot 3.x 주요 변경 사항과 마이그레이션 방법 (0) | 2024.02.04 |
Redis 서버 재시작 시 데이터 초기화 문제와 해결 방법: RDB와 AOF (1) | 2024.01.21 |