단위 테스트 (3) - 테스트 코드 작성 스타일
2025.03.31 - [테스트 코드] - 단위 테스트 (1) - 테스트를 바라보는 관점
단위 테스트 (1) - 테스트를 바라보는 관점
2025.03.31 - [테스트 코드] - 단위 테스트 (1) - 테스트를 바라보는 관점 단위 테스트 (1) - 테스트를 바라보는 관점테스트에는 단위 테스트, 통합 테스트, E2E 테스트가 있다. 단위 테스트는 가장 작은
iseongmin.tistory.com
2025.04.09 - [테스트 코드] - 단위 테스트 (2) - 가치 있는 테스트
단위 테스트 (2) - 가치 있는 테스트
2025.03.31 - [테스트 코드] - 단위 테스트 (1) - 테스트를 바라보는 관점 단위 테스트 (1) - 테스트를 바라보는 관점테스트에는 단위 테스트, 통합 테스트, E2E 테스트가 있다. 단위 테스트는 가장 작은
iseongmin.tistory.com
2025.05.05 - [테스트 코드] - 단위 테스트 (3) - 테스트 코드 작성 스타일
단위 테스트 (3) - 테스트 코드 작성 스타일
테스트에는 단위 테스트, 통합 테스트, E2E 테스트가 있다. 단위 테스트는 가장 작은 단위의 테스트로 하나의 메서드나 클래스를 독립적으로 테스트한다. 통합 테스트는 여러 모듈이나 컴포넌트
iseongmin.tistory.com
테스트에는 단위 테스트, 통합 테스트, E2E 테스트가 있다. 단위 테스트는 가장 작은 단위의 테스트로 하나의 메서드나 클래스를 독립적으로 테스트한다. 통합 테스트는 여러 모듈이나 컴포넌트가 함께 동작할 때 올바르게 연결되는지 테스트한다. E2E 테스트는 실제 사용자가 애플리케이션을 이용하는 흐름을 시뮬레이션하여, 전체 시스템이 의도대로 작동하는지 테스트한다.
나는 실무를 진행하면서, 다른 테스트는 아니더라도 단위 테스트만큼은 꼭 작성하려고 노력하고 있다. 물론, 모든 코드에 테스트를 작성하는 것은 아니고, 비즈니스에 핵심적인 도메인 로직 위주로 테스트를 작성한다. 단위 테스트는 다른 종류의 테스트를 작성하기 위한 기초이기 때문에, 통합 테스트와 E2E 테스트로 나아가기 위한 첫 걸음이라고 생각한다.
사실 이 책을 읽기 전에도 단위 테스트를 작성하고 있었지만, 어느 순간부터 내가 작성하는 코드가 단위 테스트인지 통합 테스트인지 경계가 흐려지기 시작했다. 목업(mock)이 점점 과해졌고, 지나친 세부 구현에 대한 검증을 하다 보니 테스트 코드가 유리처럼 깨지기 운 상태가 되어버렸다. 작은 리팩터링만 해도 테스트가 우수수 깨지기 시작했고, 테스트가 오히려 개발의 발복을 잡는 상황이 생기곤 했다.
그런 경험이 반복되면서 테스트 코드 작성 자체가 점점 부담으로 느껴졌고, 어느새 "원래 테스트 코드 작성은 이런건가?"라는 의문이 들기 시작했다. 이 의문을 풀어보고 싶다는 생각에, 이 책을 읽게 되었다.
단위 테스트에는 출력 기반, 상태 기반, 통신 기반이 있다.
단위 테스트 스타일에는 출력 기반, 상태 기반, 통신 기반이 있다. 출력 기반 스타일의 테스트가 품질이 좋고, 상태 기반 테스트는 두 번째로 좋은 선택이며, 통신 기반 테스트는 간헐적으로만 사용해야 한다.
1. 출력 기반
출력 기반 스타일은 테스트 대상 시스템(SUT)에 입력을 넣고 생성된 출력을 점검하는 방식이다. 이러한 테스트 스타일은 전역 상태나 내부 상태를 변경하지 않는 코드에만 적용되므로 반환 값만 검증하면 된다.
public class PriceEngine
{
public decimal CalculateDiscount(param Product[] products)
{
decimal discount = products.length * 0.01m;
return Math.min(discount, 0.2m);
}
}
[Fact]
public void Discount_of_two_products()
{
var product1 = new Product("Hand wash");
var product2 = new Product("Shampoo");
var sut = new PriceEngine();
decimal discount = sut.CalculateDiscount(product1, product2);
// 반환 값을 점검
Assert.Equal(0.02m, discount);
}
2. 상태 기반
상태 기반 스타일은 작업이 완료된 후 시스템 상태를 확인하는 방식이다. 여기서 상태는 SUT나 데이터베이스 또는 파일 시스템 등의 외부 의존성의 상태를 의미한다.
public class Order
{
private readonly List<Product> _products = new List<Product>();
public IReadOnlyList<Product> Products => _products.ToList();
public void AddProduct(Product product)
{
_products.Add(product);
}
}
[Fact]
public void Adding_a_product_to_an_order()
{
var product = new Product("Hand wash");
var sut = new Order();
sut.AddProduct(product);
// SUT의 상태를 검증
Assert.Equal(1, sut.Products.Count);
Assert.Equal(product, sut.Products[0]);
}
3. 통신 기반
통신 기반 테스트 스타일은 목을 사용해 테스트 대상 시스템과 협력자 간의 통신을 검증하는 방식이다.
[Fact]
public void Sending_a_greeting_email()
{
var emailGatewayMock = new Mock<IEmailGateway>();
var sut = new Controller(emailGatewayMock.Object);
sut.GreetUser("user@email.com");
emailGatewayMock.Verify(
x => x.SendGreetingEmail("user@email.com"),
Times.Once
)
}
어떤 스타일을 사용해야 할까
세 가지 스타일 모두 회귀 방지, 피드백 속도 지표에서는 점수가 같다. 그래서 리팩토링 내성과 유지 비용을 기반으로 평가할 수 있다. 상태 기반과 통신 기반은 구현 세부 사항과 결합할 가능성이 높고, 크기도 커서 유지 비용이 크다. 그래서 출력 기반 테스트를 선호해야 하지만 함수형으로 적상된 코드에만 적용할 수 있다.
출력 기반 | 상태 기반 | 통신 기반 | |
리팩토링 내성 | 높음 | 중간 | 중간 |
유지비 | 낮음 | 중간 | 높음 |
함수형 프로그래밍의 핵심 목표는 비즈니스 로직과 사이드 이펙트를 분리하는 것이다.
함수형 프로그래밍의 핵심 목표는 비즈니스 로직과 사이드 이펙트를 분리하는 것이다. 함수형 아키텍처는 사이드 이펙트를 비즈니스 연산의 가장자리로 밀어내 분리를 이루는 데 도움이 된다.
함수형 아키텍처는 전체 코드를 두 가지 범주로 나눈다:
- 함수형 코어: 순수 함수들로 구성되며, 입력을 받아 결정을 내린다.
- 가변 셸: 외부와의 상호작용(입력 수집, 출력 처리 등)을 담당하며, 입력을 코어에 전달하고 코어의 출력을 사이드 이펙트로 변환한다.
함수형 아키텍처는 모든 사이드 이펙트를 도메인 계층 밖으로 밀어낸다는 점에서, 육각형 아키텍처와 구분된다. 육각형 아키텍처는 도메인 계층 내부에서 발생하는 사이드 이펙트는 허용하지만, 함수형 아키텍처는 오직 도메인 바깥에서만 사이드 이펙트를 처리한다는 점이 핵심적인 차이다.
함수형 아키텍처와 전통적인 아키텍처 사이의 선택은 성능과 유지 보수성 간의 절충이다.
함수형 아키텍처와 전통적인 아키텍처 사의 선택은 성능과 유지 보수성 간의 절충이다. 성능 영향이 그다지 눈에 띄지 않는 일부 시스템에서는 함수형 아키텍처를 사용해 유지 보수성을 향상 시키는 편이 낫다. 다른 경우라면, 반대로 선택해야 할 수 있다. 두루 적용되는 해결책은 없다.
함수형 아키텍처는 코드의 복잡도를 줄이고 유지 보수성을 향상시킨다는 장점이 있지만, 초기 설계와 구현에 더 많은 노력이 필요하다. 하지만 모든 프로젝트가 그 정도의 초기 투자가 필요한 만큼의 복잡도를 가지는 것은 아니다. 어떤 코드베이스는 너무 단순하거나, 비즈니스 관점에서 큰 의미가 없어서 초기 투자만큼의 효과를 얻지 못할 수도 있다. 따라서 함수형 아키텍처는 시스템의 복잡도와 비즈니스적 중요도를 고려해 전략적으로 적용하는 것이 바람직하다. 무조건적인 도입보다는, "이 시스템이 함수형 아키텍처를 도입할 만큼 충분히 복잡하고 중요한가?"를 먼저 자문해야 한다.
마지막으로, 함수형 방식에서 순수성에 많은 비용이 든다면 순수성을 따르지 않아도 된다. 대부분의 프로젝트에서는 모든 도메인 모델을 불변으로 할 수 없기 때문에 출력 기반 테스트에만 의존할 수 없다. 대부분의 경우 출력 기반 스타일과 상태 기반 스타일을 조합하게 되며, 통신 기반 스타일을 약간 섞어도 괜찮다.