////
Search
Duplicate
🏎️

Chapter 16. 테스트와 설계

테스트와 소프트웨어 설계는 긴밀한 상관관계를 가진다.
이 둘은 소프트웨어 개발 프로세스의 핵심적인 부분이며 서로 영향을 주고받으며 공존한다. 다시 말해 이 둘은 상호보완적이다.
그 이유는 좋은 소프트웨어 설계와 테스트가 추구하는 목표가 일정부분 같기 때문이다.
즉, 테스트가 추구하는 가치와 좋은 설계가 추구하는 가치 사이에 일정 부분 교집합이 존재하는 것이다.
좋은 설계는 시스템이 모듈로 분해되고 각 모듈이 독립적으로 개발될 수 있게 하는 것을 추구한다.
이를 통해 모듈화와 재사용성을 높이고 코드를 더 오래, 자주 사용할 수 있게 하며 나아가 시스템이 유연히 확장될 수 있음을 추구한다.
테스트하기 쉬운 코드가 무조건 좋은 설계는 아니지만 높은 확률로 그러하다. 따라서 테스트에 관한 고민이 좋은 설계에 관한 고민을 일정부분 해결해줄 수 있다.
이번 장에서는 테스트가 어떻게 좋은 설계를 유도하는지, 그 이유를 살펴보자. 그리고 이를 위해 좋은 설계라고 했을 때 대표적으로 소개되는 SOLID와 테스트의 상관 관계를 살펴보겠다.

1. 테스트와 SRP

테스트를 작성함으로써 SRP를 위반하는 코드가 SRP을 지키는 방향으로 변경되는 예시를 살펴보자.
우리는 테스트를 작성하면서 테스트 대상 시스템의 의존성을 주입한다.
이때 각 메소드 별로 사용되거나 사용되지 않는 의존성들을 확인하며 책임에 대해서 고민해볼 수 있다.
UserServicelogin, register 메소드가 각각 EmailSender, ClockHolder를 사용있을 때, login에게는 ClockHolder가 불필요한 의존성이고 register에게는 EmailSender가 불필요한 의존성이다.
이는 책임이 잘못 분배되어있다는 신호로 보고 이들을 분리할 수 있다.
이를 테스트 과정에서 확인할 수 있다.

2. 테스트와 ISP

EmailSender의 인터페이스가 다음과 같이 정의되어 있다고 생각해보자.
public interface EmailSender { void sendVerificationRequired(User user); void sendWelcome(User user); void sendAdvertisement(User user); void sendCharge(User user); }
Java
복사
UserServicelogin 메소드는 오직 sendVerificationRequired만을 사용한다.
그러나 우리가 테스트를 작성하기 위해 DummyEmailSenderFakeEmailSender를 만드는 경우, 인터페이스의 모든 메소드들을 구현해주어야 한다.
이는 인터페이스가 통합되어 있기 때문에 발생하는 문제다. 이때문에 테스트의 관심사 밖에 있는 불필요한 메소드들도 구현해야 한다.

3. 테스트와 OCP, DIP

좋은 설계를 갖춘 시스템은 유연하다. 좋은 설계로 개발된 시스템이라면 외부 요구사항이 변경되어 코드를 수정할 때도 변경사항의 범위를 최소화할 수 있어야 한다.
따라서 설계 원칙에는 이런 유연성을 강조하는 원칙이 많다. 그중에서도 OCP에 주목해보자.
테스트에서 의존성을 쉽게 대체할 수 있음이 유연한 설계를 가졌다는 증명이 되어줄 수 있다.
DIP가 추상화를 뜻하는 것은 아니지만 추상화를 통해서 유연성을 추구할 수 있다.
테스트가 의존성 역전 원칙을 유도하는 예는 의존성이 인터페이스가 아닌 구체 클래스에 의존했을 때, 테스트하기가 쉬워지는 지 어려워지는 지를 통해 증명할 수 있다.
단순히, 서비스를 추상에 의존하게 만들면 된다. 역할에 의존하는 코드를 만들면 모든 고민이 해결 된다.

4. 테스트와 LSP

테스트에 대해 많은 주제를 이야기 했으니 이제 어떤 것을 테스트해야 하지에 대한 의논해보자.
이 질문의 답변은 꽤나 다양할 것인데, 그중에서 특히나 많이 듣는 답변은 Right-BICEP과 CORRECT 원칙이다.
Right-BICEP
Right: 결과가 올바른지 확인해 봐야 한다.
Boundary: 경계 조건에서 코드가 정상적으로 동작하는지 확인해야 한다.
Inverse: 역함수가 있다면 이를 실행해 입력과 일치하는지 확인해야 한다.
Cross-Check: 검증에 사용할 다른 수단이 있다면 이를 확인해야 한다.
Error Conditions: 오류 상황에서도 프로그램이 의도한 동작을 하는지 확인해야 한다.
Performance: 프로그램이 예상한 성능 수준을 유지하는지 확인해야 한다.
CORRECT
Conformance(적합성): 데이터 포맷이 제대로 처리되는지 확인해야 한다.
Ordering(정렬): 출력에 순서가 보장되어야 한다면 이를 확인해야 한다.
Range(범위): 입력에 양 끝점이 있다면 양 끝점이 들어갈 때 정상 동작하는지 확인해야 한다.
Reference(참조): 협력 객체의 상태에 따라 어떻게 동작하는지 확인해야 한다.
Existence(존재): null, blank와 같은 값이 입력될 때 어떻게 반응하는지 확인해야 한다.
Cardinality(원소 개수): 입력의 개수가 0, 1, 2, … n개 일때 어떻게 동작하는지 확인해야 한다.
Time(시간): 병렬 처리를 한다면 순서가 보장되는지 확인해야 한다.
사실 이보다 더 간단한 답변이 있다. 유지하고 싶은 상태, 즉 변경되지 않았으면 하는 상태가 있다면 테스트로 작성하라는 말이다.
어떤 시스템에 특정한 테스트 케이스가 없다면 해당 케이스는 시스템에서 변경되어도 상관없는 것일 수 있기 때문이다.
유지하고 싶은 상태가 있다면 테스트로 작성하라.