이슈 내용
개발자가 제어할 수 없는 요소에 대한 테스트 용이성과 직접적으로 의존하면 안 되는 모듈의 클래스를 의존하고 있진 않은지 테스트하기 위해 인터페이스를 두고 테스트 더블을 직접 구현할 수 있도록 설계했다.
코드의 크기가 작을 때는 아무런 문제가 되지 않았지만, 기능과 의존성이 추가될 때마다 직접 구현할 테스트 더블이 증가했고 이는 생산성 저하와 더불어 테스트 전체의 크기가 커지는 영향을 미쳤다. 해당 문제가 발생한 이유와 해결방법에 대해 정리해보려 한다.
테스트 더블을 직접 구현한 이유
Mockito 라이브러리를 사용하면 쉽게 테스트 더블을 생성할 수 있다. 하지만 이전 프로젝트에서 적용해본 결과 비즈니스 로직에서 bootstrap, framework 모듈의 클래스를 직접 의존하진 않는지 파악할 수 없는 문제점이 존재했다.
이번 프로젝트는 헥사고날 아키텍처를 적용하여 정책이 세부사항을 의존하지 않도록 설계했다. 때문에 테스트 코드를 작성하며 실제로 정책이 세부사항을 의존하지 않는지 파악하기 위해 정책에 해당하는 모듈에서는 테스트 더블을 직접 구현하는 방식을 채택하였다.
조금 더 자세하게 말하면 정책은 인터페이스를 의존하고, 해당 인터페이스를 세부사항이 구현하는 방식으로 정책이 세부사항을 의존하지 않도록 설계할 수 있다. 테스트를 통해 정말 의존 방향이 올바른지를 파악하려면 ‘정책이 의존하고 있는 인터페이스를 구현하여 주입할 수 있냐’를 확인하면 된다.
또 다른 문제점도 존재한다. 서비스 코드애서 LocalDateTime.now()
, Random
과 같은 개발자가 테스트에서 제어할 수 없는 요소를 의존하고 있다면 테스트하기 상당히 곤란해진다. 물론 시간을 직접 제어하지는 못하겠지만 Mockito 라이브러리로 예외를 발생한는 등 흉내는 낼 수 있겠지만 이게 의미가 있나 싶다. 이렇게 개발자가 제어하지 못하는 요소는 인터페이스로 분리하고 DI 해줌으로써 테스트에서 원하는 테스트더블로 교체할 수 있다.
테스트 더블을 직접 구현한 코드 1
개발자가 제어할 수 없는 요소를 의존하고 있는 코드는 Access 토큰을 생성하고, 검증하는 ActorTokenService에서 볼 수 있다. 해당 서비스는 LocalDateTime.now()
를 사용하여 현재 시간을 기준으로 토큰을 생성하고 2일이 지난 토큰은 인증에 실패한다. 테스트 시 토큰을 생성하고 2일이 지난 상황을 구현하여 테스트 가능해야 한다.
따라서 DateGenerator 인터페이스를 두고, 구현체 Bean을 주입해줌으로써 테스트 가능하게 변경했다.
@Component
public class ActorTokenDateGenerator implements DateGenerator {
@Override
public Date getCurrentDate() {
return new Date(System.currentTimeMillis());
}
@Override
public Date getExpireDate(long exp) {
long currentTime = getCurrentDate().getTime();
return new Date(currentTime + exp);
}
}
public interface DateGenerator {
Date getCurrentDate();
Date getExpireDate(long exp);
}
테스트 시에는 DateGenerator
를 구현하는 MockDateGenerator
를 주입하며 테스트하면 된다.
public class MockDateGenerator implements DateGenerator {
private Date currentDate;
public MockDateGenerator() {
currentDate = new Date(System.currentTimeMillis());
}
@Override
public Date getCurrentDate() {
return currentDate;
}
@Override
public Date getExpireDate(long exp) {
return new Date(currentDate.getTime() + exp);
}
// 테스트 환경에 맞는 시간을 주입해줄 수 있도록 구현했다.
public void setCurrentDate(Date date) {
currentDate = date;
}
}
테스트 더블을 직접 구현한 코드 2
application(비즈니스로직) 모듈의 일부인 MemberService를 보면 아래와 같이 여러 인터페이스와 구현하며, 의존하고 있다. 비즈니스 로직은 세부사항과 의존하면 안 되기에 인터페이스를 두었고 세부사항을 직접 의존하고 있진 않은지 테스트가 필요하다.
해당 코드가 문제였다. 기능이 추가됨에 따라 의존하는 OutputPort 인터페이스가 계속해서 증가했고, 이에 따라 구현해야 할 테스트 더블이 지속적으로 증가했다.
해결방법
정책과 세부사항을 다른 모듈로 분리한다.
현재 프로젝트 구조는 정책과 세부사항이 각각 다른 모듈에 구현되어 있고, 정책은 세부사항의 모듈을 의존하고 있지 않다. 따라서 정책 클래스에서는 세부사항의 클래스를 직접 의존하는 것이 불가능 했다.
동일한 모듈에서 정책과 세부사항을 모두 구현하는 구조라면 직접 테스트 더블을 통해 테스트해야 할 것 같은데, 오히려 테스트에 들어가는 공수가 더 커지는 것을 경험했다. 따라서 모듈단위로 분리하는 것이 최선으로 보인다.
제어 불가능한 요소만 테스트 더블을 직접 구현한다.
LocalDateTime.now()
같은 시간 정보는 테스트에서 원하는 값을 얻기 힘들다. 따라서 이런 이슈가 발생할 시에만 인터페이스를 통해 테스트 더블을 직접 구현하도록 한다.
결론
테스트 용이성은 예외가 발생할 수 있는 상황을 구현할 수 있느냐에 따라 결정되는 것 같다. 예외가 발생할 수 있는 상황을 Mockito 와 같은 라이브러리로 구현할 수 없는 상황이라면 추상화를 두고 직접 구현할 수 있게 하는 것도 좋은 해결책으로 보인다.
'중고 거래 플랫폼 API 서버 개발 > 트러블 슈팅' 카테고리의 다른 글
SpringBoot 멀티 모듈 프로젝트 Gradle 빌드 시 bootJar 태스크 실행 오류 (1) | 2024.02.10 |
---|---|
모듈간 의존성은 있는데 소스 정보를 읽어오지 못하는 이슈 (Plain Archive의 사용처?) (1) | 2024.02.10 |