테스트는 시스템의 일부이며, 아키텍처에도 관여한다.
시스템 컴포넌트인 테스트
아키텍처 관점에서는 모든 테스트가 동일하다.
TDD 로 생성한 아주 작은 테스트이든, 아니면 대규모의 FitNesse, Cucumber, SpecFlow, JBehave 테스트이든, 이들 테스트는 아키텍처적으로 모두 동등하다.
세부적이며 구체적인 것으로, 의존성은 항상 테스트 대상이 되는 코드를 향한다.
실제로 테스트는 아키텍처에서 가장 바깥쪽 원으로 생각할 수 있다.
시스템 내부 어떤 것도 테스트에는 의존하지 않으며, 테스트는 시스템의 컴포넌트를 향해, 항상 원의 안쪽으로 의존한다.
1) 테스트는 의존성 규칙을 따른다.
- 테스트는 세부적, 구체적인 것으로 의존성은 항상 테스트 대상이 되는 코드를 향한다.
- 실제로 테스트는 아키텍처에서 가장 바깥쪽 원으로 생각할 수 있다.
- 시스템 내부의 어떤 것도 테스트에는 의존하지 않으며, 테스트는 시스템의 컴포넌트를 향해, 항상 원의 안쪽으로 의존한다.
2) 테스트는 독립적으로 배포 가능하다.
- 대다수의 경우 테스트는 테스트 시스템에만 배포하며, 상용 시스템에는 배포하지 않는다.
3) 테스트는 시스템 컴포넌트 중 가장 고립되어있다.
- 테스트가 시스템 운영에 꼭 필요하지 않다.
테스트를 고려한 설계
하지만, 그렇다고 해서 아키텍처 설계 시, 테스트를 설계 범위 밖에 있다고 여기면 안된다.
테스트가 시스템의 설계와 잘 통합되지 않으면, 테스트는 깨지기 쉬워지고, 시스템은 뻣뻣해져서 변경하기가 어려워진다.
시스템의 공통 컴포넌트가 변경되면 수백, 심지어 수 천 개의 테스트가 망가질 수 있다.
이 문제는 깨지기 쉬운 테스트 문제(Fragile Tests Problem)로 알려져 있다.
깨지기 쉬운 테스트는 시스템을 뻣뻣하게 만든다는 부작용을 낳을 때가 많다.
시스템에 가한 간단한 변경이 대량의 테스트 실패로 이어진다는 사실을 알게 되면, 개발자는 그러한 변경을 하지 않으려 들 것이기 때문이다.
문제를 해결하려면 테스트를 고려해서 설계해야 한다.
소프트웨어 설계의 첫 번째 규칙은 언제나 같다.
변동성이 있는 것에 의존하지 말라.
GUI 는 변동성이 크다.
GUI 로 시스템을 조작하는 테스트 스위트는 분명 꺠지기 쉽다.
따라서 시스템과 테스트를 설계할 때 GUI 를 사용하지 않고 업무 규칙을 테스트 할 수 있게 해야 한다
테스트 API
위 목표를 달성하려면 값 비싼 자원(DB)은 건너뛰고 보안 제약사항은 무시하며 모든 업무 규칙을 검증할 수 있는 API를 만들면 된다.
그리고 이 테스트 API 는 시스템을 테스트 가능한 특정 상태로 강제하는 강력한 힘을 지녀야 한다.
이 API는 사용자 인터페이스가 사용하는 인터랙터 Interactor 와 인터페이스 어댑터 들의 상위집합이 될 것이다.
테스트 API는 테스트를 애플리케이션으로부터 분리할 목적으로 사용한다.
*테스트API란?
핵심 차이점
구분 | API (일반 API) | 테스트 API |
---|---|---|
목적 | 실제 서비스나 애플리케이션에서 기능을 제공 소프트웨어의 기능을 테스트하고 검증 | 소프트웨어의 기능을 테스트하고 검증 |
동작 | 클라이언트와 서버 간의 실제 요청/응답 처리 | 테스트 목적으로 가짜 데이터나 환경에서 요청/응답 처리 |
형식 | REST, SOAP, GraphQL 등 다양한 형태 | 테스트 자동화, 모킹 등을 통해 테스트에 특화된 API |
배포 | 실제 서비스에 배포되어 동작 | 테스트 환경에만 배포됨 |
테스트 API에서의 상위 집합
// 테스트 API에서 상위집합을 사용하는 예시
public class UserRequestAdapterTest {
private UserInteractor userInteractor;
private UserRequestAdapter userRequestAdapter;
@Before
public void setUp() {
// UserRepository와 UserInteractor를 mock 처리하여 의존성 주입
UserRepository mockUserRepository = mock(UserRepository.class);
UserInteractorImpl userInteractorImpl = new UserInteractorImpl(mockUserRepository);
userRequestAdapter = new UserRequestAdapterImpl(userInteractorImpl);
}
@Test
public void testHandleRequest() {
// given
UserRequest userRequest = new UserRequest("user123");
// when
UserResponse response = userRequestAdapter.handleRequest(userRequest);
// then
assertNotNull(response);
assertEquals("user123", response.getUser().getId());
}
}
단순히 UI 로부터 분리하는 것만이 아닌 텍스트 구조를 애플리케이션 구조로부터 결합을 분리하는게 목표이다
구조적 결합
테스트 결합 중에 가장 강하다.
테스트 스위트는 애플리케이션 구조에 강하게 결합되어 잇다.
상용 클래스나 메서드중 하나라도 변경되면 딸려 있는 다수의 테스트도 깨지게 되고, 상용 코드를 뻣뻣하게 만든다.
테스트 API의 역할은 애플리케이션의 구조를 테스트로부터 숨겨 상용코드를 리팩터링 하더라도 테스트에는 전혀 영향을 주지 않는것에 있다.
보안
운영 시스템에 배포하면 위험에 처할 수 잇기에 테스트 API 자체와 테스트 API 중 위험한 구현부는 독립적으로 배포할 수 있는 컴포넌트로 분리한다.
보안
테스트 API 가 지닌 강력한 힘을 운영 시스템에 배포하면 위험에 처할 수 있다.
위험을 피하고 싶다면, 테스트 API 자체와 테스트 API 중 위험한 부분의 구현부는 독립적으로 배포할 수 있는 컴포넌트로 분리해야 한다.
결론
테스트는 시스템 외부에 있지 않다.
오히려 시스템의 일부다.
따라서 테스트에서 기대하는 안정성과 회귀의 이점을 얻을 수 있으려면 테스트는 잘 설계돼야만 한다.
테스트를 시스템의 일부로 설계하지 않으면 테스트는 깨지기 쉽고 유지보수하기 어려워지는 경향이 있다.
이러한 테스트는 유지보수하기가 너무 힘들기 떄문에 결국 방바닥의 휴지처럼 버려지는 최후를 맡는다.
'IT > architecture' 카테고리의 다른 글
[Clean Architecture 정리] 34장. 빠져 있는 장 (1) | 2025.02.15 |
---|---|
[Clean Architecture 정리] 29장. 클린 임베디드 아키텍처 (0) | 2025.02.15 |
[Clean Architecture 정리] 27장. '크고 작은 모든' 서비스들 (0) | 2025.02.15 |
[Clean Architecture 정리] 26장. 메인(Main) 컴포넌트 (0) | 2025.02.15 |
[Clean Architecture 정리] 23장. 프레젠터와 험블 객체 (0) | 2025.02.15 |