챕터 15: 테스팅과 유지보수
소프트웨어의 품질을 보장하기 위해서는 철저한 테스팅과 유지보수가 필수적입니다. 이 장에서는 단위 테스트와 통합 테스트, 테스트 도구(JUnit과 Mockito), 그리고 클린 코드와 리팩토링에 대해 다룹니다.
15.1 단위 테스트와 통합 테스트
테스팅은 소프트웨어의 결함을 발견하고 품질을 향상시키는 중요한 과정입니다. 단위 테스트는 개별 모듈의 기능을 검증하고, 통합 테스트는 모듈 간의 상호작용을 검증합니다.
배경과 역사
테스팅은 소프트웨어 개발 초기부터 중요한 과정으로 인식되어 왔습니다. 단위 테스트는 1990년대에 TDD(Test-Driven Development) 방법론과 함께 널리 사용되기 시작했습니다. 통합 테스트는 단위 테스트의 한계를 보완하기 위해 도입되었습니다.
15.1.1 JUnit을 이용한 단위 테스트
JUnit은 Java 언어로 작성된 단위 테스트 프레임워크입니다. JUnit을 사용하면 테스트를 자동화하고 반복적으로 실행할 수 있습니다.
예제: JUnit을 이용한 단위 테스트
- JUnit 의존성 추가
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 테스트 클래스 작성
UserServiceTest.java
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class UserServiceTest {
private UserService userService;
@BeforeEach
public void setUp() {
// 테스트 실행 전 UserService 객체를 초기화합니다.
userService = new UserService(new UserRepository());
}
@Test
public void testCreateUser() {
// Arrange: 테스트에 필요한 객체와 상태를 설정합니다.
User user = new User();
user.setName("John Doe");
user.setEmail("john.doe@example.com");
// Act: 테스트할 동작을 수행합니다.
userService.createUser(user);
// Assert: 기대하는 결과를 검증합니다.
assertNotNull(userService.getAllUsers()); // 사용자 목록이 null이 아닌지 확인합니다.
assertEquals(1, userService.getAllUsers().size()); // 사용자 목록 크기가 1인지 확인합니다.
assertEquals("John Doe", userService.getAllUsers().get(0).getName()); // 첫 번째 사용자의 이름이 "John Doe"인지 확인합니다.
}
}
설명:
- @BeforeEach: 각 테스트 메서드 실행 전에 실행될 메서드를 정의합니다.
- @Test: 테스트 메서드를 정의합니다.
- assertNotNull: 객체가 null이 아닌지 확인합니다.
- assertEquals: 예상 값과 실제 값이 같은지 확인합니다.
- setUp: 테스트 실행 전에 필요한 초기 설정을 수행합니다.
- testCreateUser: 사용자 생성 기능을 테스트합니다.
추가 예제: JUnit 단위 테스트
ProductServiceTest.java
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class ProductServiceTest {
private ProductService productService;
@BeforeEach
public void setUp() {
// ProductService 객체를 초기화합니다.
productService = new ProductService(new ProductRepository());
}
@Test
public void testAddProduct() {
// Arrange: 테스트에 필요한 객체와 상태를 설정합니다.
Product product = new Product();
product.setName("Laptop");
product.setPrice(1200.00);
// Act: 테스트할 동작을 수행합니다.
productService.addProduct(product);
// Assert: 기대하는 결과를 검증합니다.
assertNotNull(productService.getAllProducts()); // 제품 목록이 null이 아닌지 확인합니다.
assertEquals(1, productService.getAllProducts().size()); // 제품 목록 크기가 1인지 확인합니다.
assertEquals("Laptop", productService.getAllProducts().get(0).getName()); // 첫 번째 제품의 이름이 "Laptop"인지 확인합니다.
assertEquals(1200.00, productService.getAllProducts().get(0).getPrice()); // 첫 번째 제품의 가격이 1200.00인지 확인합니다.
}
@Test
public void testRemoveProduct() {
// Arrange: 테스트에 필요한 객체와 상태를 설정합니다.
Product product = new Product();
product.setName("Laptop");
product.setPrice(1200.00);
productService.addProduct(product);
// Act: 테스트할 동작을 수행합니다.
productService.removeProduct(product);
// Assert: 기대하는 결과를 검증합니다.
assertEquals(0, productService.getAllProducts().size()); // 제품 목록 크기가 0인지 확인합니다.
}
}
설명:
- testAddProduct: 제품 추가 기능을 테스트합니다.
- testRemoveProduct: 제품 제거 기능을 테스트합니다.
15.1.2 Spring과 통합 테스트
Spring 통합 테스트는 애플리케이션 컨텍스트를 로드하고 실제 환경과 유사한 조건에서 테스트를 수행합니다.
예제: Spring 통합 테스트
- SpringBootTest 설정
UserServiceIntegrationTest.java
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.jdbc.Sql;
@SpringBootTest
public class UserServiceIntegrationTest {
@Autowired
private UserService userService;
@Test
@Sql("/test-data.sql")
public void testGetAllUsers() {
// 데이터베이스 초기화 후 사용자 목록 크기 검증
assertThat(userService.getAllUsers()).hasSize(2); // 사용자 목록 크기가 2인지 확인합니다.
}
}
test-data.sql
INSERT INTO users (name, email) VALUES ('John Doe', 'john.doe@example.com');
INSERT INTO users (name, email) VALUES ('Jane Smith', 'jane.smith@example.com');
설명:
- @SpringBootTest: Spring 애플리케이션 컨텍스트를 로드하여 통합 테스트를 수행합니다.
- @Sql: 테스트 전에 실행될 SQL 스크립트를 지정합니다.
- assertThat: 테스트 조건을 검증합니다.
- testGetAllUsers: 사용자 목록 조회 기능을 통합 테스트합니다.
추가 예제: Spring 통합 테스트
ProductServiceIntegrationTest.java
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.jdbc.Sql;
@SpringBootTest
public class ProductServiceIntegrationTest {
@Autowired
private ProductService productService;
@Test
@Sql("/product-test-data.sql")
public void testGetAllProducts() {
// 데이터베이스 초기화 후 제품 목록 크기 검증
assertThat(productService.getAllProducts()).hasSize(2); // 제품 목록 크기가 2인지 확인합니다.
}
}
product-test-data.sql
INSERT INTO products (name, price) VALUES ('Laptop', 1200.00);
INSERT INTO products (name, price) VALUES ('Phone', 800.00);
설명:
- testGetAllProducts: 제품 목록 조회 기능을 통합 테스트합니다.
- product-test-data.sql: 테스트용 제품 데이터를 삽입하는 SQL 스크립트입니다.
15.2 테스트 도구 (JUnit, Mockito)
테스트 도구를 사용하면 단위 테스트와 통합 테스트를 효율적으로 작성할 수 있습니다. JUnit은 테스트 프레임워크이고, Mockito는 목 객체(Mock Object)를 생성하는 도구입니다.
배경과 역사
Mockito는 2008년에 처음 발표되었으며, 객체 지향 언어의 유닛 테스트를 지원하기 위해 설계되었습니다. Mockito는 테스트를 위해 외부 의존성을 모킹하여 테스트 환경을 단순화하고, 코드의 복잡성을 줄이는 데 도움을 줍니다.
15.2.1 Mockito를 이용한 목 객체 생성
Mockito를 사용하면 외부 의존성을 모킹하여 단위 테스트를 수행할 수 있습니다.
예제: Mockito를 이용한 목 객체 생성
- Mockito 의존성 추가
pom.xml
<dependencies>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.9.0</version>
<scope>test</scope>
</dependency>
</dependencies>
- 목 객체 생성
UserServiceTest.java
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
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; // 목 객체 생성
@InjectMocks
private UserService userService; // 목 객체를 주입하여 UserService 초기화
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this); // 목 객체 초기화
}
@Test
public void testCreateUser() {
// Arrange: 테스트에 필요한 객체와 상태를 설정합니다.
User user = new User();
user.setName("John Doe");
user.setEmail("john.doe@example.com");
// Act: 테스트할 동작을 수행합니다.
userService.createUser(user);
// Assert: 기대하는 결과를 검증합니다.
verify(userRepository, times(1)).save(user); // userRepository의 save 메서드가 한 번 호출되었는지 검증
}
}
설명:
- @Mock: 목 객체를 생성합니다.
- @InjectMocks: 목 객체를 주입하여 테스트 대상 객체를 생성합니다.
- MockitoAnnotations.openMocks: 목 객체를 초기화합니다.
- verify: 특정 메서드가 호출되었는지 검증합니다.
추가 예제: Mockito를 이용한 목 객체 생성
ProductServiceTest.java
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class ProductServiceTest {
@Mock
private ProductRepository productRepository; // 목 객체 생성
@InjectMocks
private ProductService productService; // 목 객체를 주입하여 ProductService 초기화
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this); // 목 객체 초기화
}
@Test
public void testAddProduct() {
// Arrange: 테스트에 필요한 객체와 상태를 설정합니다.
Product product = new Product();
product.setName("Laptop");
product.setPrice(1200.00);
// Act: 테스트할 동작을 수행합니다.
productService.addProduct(product);
// Assert: 기대하는 결과를 검증합니다.
verify(productRepository, times(1)).save(product); // productRepository의 save 메서드가 한 번 호출되었는지 검증
}
@Test
public void testRemoveProduct() {
// Arrange: 테스트에 필요한 객체와 상태를 설정합니다.
Product product = new Product();
product.setName("Laptop");
product.setPrice(1200.00);
// Act: 테스트할 동작을 수행합니다.
productService.removeProduct(product);
// Assert: 기대하는 결과를 검증합니다.
verify(productRepository, times(1)).delete(product); // productRepository의 delete 메서드가 한 번 호출되었는지 검증
}
}
설명:
- testAddProduct: 제품 추가 기능을 테스트합니다.
- testRemoveProduct: 제품 제거 기능을 테스트합니다.
15.2.2 테스트 코드 작성 기법
테스트 코드를 작성할 때는 명확하고 유지보수 가능한 코드를 작성하는 것이 중요합니다.
테스트 코드 작성 기법:
- Arrange-Act-Assert 패턴: 테스트 코드를 세 단계로 나눕니다.
- Arrange: 테스트에 필요한 객체와 상태를 설정합니다.
- Act: 테스트할 동작을 수행합니다.
- Assert: 기대하는 결과를 검증합니다.
예제: 테스트 코드 작성 기법
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
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;
@InjectMocks
private UserService userService;
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
public void testCreateUser() {
// Arrange: 테스트에 필요한 객체와 상태를 설정합니다.
User user = new User();
user.setName("John Doe");
user.setEmail("john.doe@example.com");
// Act: 테스트할 동작을 수행합니다.
userService.createUser(user);
// Assert: 기대하는 결과를 검증합니다.
verify(userRepository, times(1)).save(user);
}
}
설명:
- Arrange-Act-Assert 패턴을 사용하여 테스트 코드를 작성했습니다.
- verify 메서드를 사용하여 userRepository.save 메서드가 한 번 호출되었는지 검증했습니다.
추가 예제: 테스트 코드 작성 기법
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class ProductServiceTest {
@Mock
private ProductRepository productRepository;
@InjectMocks
private ProductService productService;
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
public void testAddProduct() {
// Arrange: 테스트에 필요한 객체와 상태를 설정합니다.
Product product = new Product();
product.setName("Laptop");
product.setPrice(1200.00);
// Act: 테스트할 동작을 수행합니다.
productService.addProduct(product);
// Assert: 기대하는 결과를 검증합니다.
verify(productRepository, times(1)).save(product);
}
@Test
public void testRemoveProduct() {
// Arrange: 테스트에 필요한 객체와 상태를 설정합니다.
Product product = new Product();
product.setName("Laptop");
product.setPrice(1200.00);
// Act: 테스트할 동작을 수행합니다.
productService.removeProduct(product);
// Assert: 기대하는 결과를 검증합니다.
verify(productRepository, times(1)).delete(product);
}
}
설명:
- testAddProduct: 제품 추가 기능을 테스트합니다.
- testRemoveProduct: 제품 제거 기능을 테스트합니다.
15.3 클린 코드와 리팩토링
클린 코드는 가독성과 유지보수성이 높은 코드를 의미합니다. 리팩토링은 코드의 기능을 유지하면서 구조를 개선하는 과정입니다.
배경과 역사
클린 코드는 2008년 로버트 C. 마틴(Robert C. Martin)의 저서 '클린 코드: 애자일 소프트웨어 장인 정신'에서 처음 개념화되었습니다. 리팩토링은 1999년 마틴 파울러(Martin Fowler)의 저서 '리팩토링: 기존 코드의 구조를 개선하는 방법'에서 자세히 설명되었습니다.
15.3.1 클린 코드의 원칙
클린 코드는 다음과 같은 원칙을 따릅니다:
- 가독성: 코드를 읽기 쉽고 이해하기 쉽게 작성합니다.
- 간결성: 불필요한 코드나 복잡성을 줄이고 간결하게 작성합니다.
- 일관성: 코드 스타일과 명명 규칙을 일관되게 유지합니다.
- 의미 있는 이름: 변수, 메서드, 클래스에 의미 있는 이름을 사용합니다.
- 단일 책임 원칙: 각 클래스와 메서드는 하나의 책임만 가지도록 작성합니다.
예제: 클린 코드 작성
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// 사용자 생성
public void createUser(User user) {
validateUser(user); // 사용자 유효성 검사
userRepository.save(user); // 사용자 저장
}
// 사용자 유효성 검사
private void validateUser(User user) {
if (user.getName() == null || user.getName().isEmpty()) {
throw new IllegalArgumentException("User name must not be empty");
}
if (user.getEmail() == null || user.getEmail().isEmpty()) {
throw new IllegalArgumentException("User email must not be empty");
}
}
// 모든 사용자 조회
public List<User> getAllUsers() {
return userRepository.findAll();
}
// 사용자 업데이트
public void updateUser(User user) {
validateUser(user); // 사용자 유효성 검사
userRepository.save(user); // 사용자 저장
}
// 사용자 삭제
public void deleteUser(int id) {
userRepository.deleteById(id); // 사용자 삭제
}
}
설명:
- validateUser 메서드를 사용하여 사용자 객체의 유효성을 검사합니다.
- 각 메서드는 단일 책임 원칙을 따르며, 하나의 기능만을 수행합니다.
- 의미 있는 이름을 사용하여 변수, 메서드, 클래스의 역할을 명확히 합니다.
추가 예제: 클린 코드 작성
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
// 제품 추가
public void addProduct(Product product) {
validateProduct(product); // 제품 유효성 검사
productRepository.save(product); // 제품 저장
}
// 제품 유효성 검사
private void validateProduct(Product product) {
if (product.getName() == null || product.getName().isEmpty()) {
throw new IllegalArgumentException("Product name must not be empty");
}
if (product.getPrice() <= 0) {
throw new IllegalArgumentException("Product price must be greater than zero");
}
}
// 모든 제품 조회
public List<Product> getAllProducts() {
return productRepository.findAll();
}
// 제품 업데이트
public void updateProduct(Product product) {
validateProduct(product); // 제품 유효성 검사
productRepository.save(product); // 제품 저장
}
// 제품 삭제
public void removeProduct(Product product) {
productRepository.delete(product); // 제품 삭제
}
}
설명:
- validateProduct 메서드를 사용하여 제품 객체의 유효성을 검사합니다.
- 각 메서드는 단일 책임 원칙을 따르며, 하나의 기능만을 수행합니다.
- 의미 있는 이름을 사용하여 변수, 메서드, 클래스의 역할을 명확히 합니다.
15.3.2 리팩토링 기법과 사례
리팩토링은 코드의 기능을 유지하면서 구조를 개선하는 과정입니다. 다음은 몇 가지 주요 리팩토링 기법입니다:
- 메서드 추출 (Extract Method): 긴 메서드를 작은 메서드로 분리하여 가독성을 향상시킵니다.
- 클래스 추출 (Extract Class): 단일 책임 원칙을 위반하는 클래스를 분리하여 책임을 명확히 합니다.
- 중복 코드 제거 (Remove Duplicate Code): 중복된 코드를 제거하여 코드의 재사용성을 높입니다.
- 매개변수 객체화 (Introduce Parameter Object): 관련 매개변수를 객체로 묶어 전달합니다.
예제: 리팩토링
리팩토링 전 코드:
public class OrderService {
public void createOrder(String customerName, String customerEmail, String product, int quantity) {
// 고객 정보 유효성 검사
if (customerName == null || customerName.isEmpty()) {
throw new IllegalArgumentException("Customer name must not be empty");
}
if (customerEmail == null || customerEmail.isEmpty()) {
throw new IllegalArgumentException("Customer email must not be empty");
}
// 주문 처리 로직
System.out.println("Order created for customer: " + customerName + " with product: " + product);
}
}
리팩토링 후 코드:
public class OrderService {
public void createOrder(Customer customer, OrderDetails orderDetails) {
validateCustomer(customer); // 고객 정보 유효성 검사
processOrder(orderDetails); // 주문 처리 로직
}
private void validateCustomer(Customer customer) {
if (customer.getName() == null || customer.getName().isEmpty()) {
throw new IllegalArgumentException("Customer name must not be empty");
}
if (customer.getEmail() == null || customer.getEmail().isEmpty()) {
throw new IllegalArgumentException("Customer email must not be empty");
}
}
private void processOrder(OrderDetails orderDetails) {
// 주문 처리 로직
System.out.println("Order created for customer: " + orderDetails.getCustomerName() +
" with product: " + orderDetails.getProduct());
}
}
설명:
- **Customer**와 OrderDetails 클래스를 도입하여 매개변수를 객체화했습니다.
- **validateCustomer**와 processOrder 메서드를 추출하여 코드를 모듈화하고 가독성을 높였습니다.
15.3.3 클린 코드의 원칙 요약
- 가독성: 코드를 읽기 쉽고 이해하기 쉽게 작성합니다.
- 간결성: 불필요한 코드나 복잡성을 줄이고 간결하게 작성합니다.
- 일관성: 코드 스타일과 명명 규칙을 일관되게 유지합니다.
- 의미 있는 이름: 변수, 메서드, 클래스에 의미 있는 이름을 사용합니다.
- 단일 책임 원칙: 각 클래스와 메서드는 하나의 책임만 가지도록 작성합니다.
이로써 단위 테스트와 통합 테스트, 테스트 도구(JUnit과 Mockito), 그리고 클린 코드와 리팩토링에 대해 자세히 설명했습니다. 이를 통해 소프트웨어의 품질을 보장하고 유지보수성을 높일 수 있게 됩니다. 다음 챕터에서는 실전 프로젝트에 대해 다루겠습니다.
'IT 강좌(IT Lectures) > Java' 카테고리의 다른 글
16강. 실전 프로젝트 (0) | 2024.07.03 |
---|---|
14강. 데이터베이스 연동 (0) | 2024.07.01 |
13강. Spring 프레임워크 (0) | 2024.06.30 |
12강. 보안과 암호화 (0) | 2024.06.29 |
11강. Java 성능 최적화 (0) | 2024.06.28 |