1. 서론
Volunteer 프로젝트를 리팩토링 하면서 현재 시간을 기준으로 마감된 일정을 조회하기 위해 LocalDate.now()를 사용했다.
LocaDate.now()는 현재 시간에 의존하기 때문에 테스트하기 어려운 코드이다.
이럴 경우 Controller 계층으로 밀어내고, 나머지 계층에선 메서드 인자(LocalDate)로 받아서 테스트하기 좋은 코드(순수 함수)로 만든다.
즉, 제어하기 힘든 코드는 최대한 가장 바깥쪽 영역(프로그램의 진입 영역)으로 밀어내고 의존하는 범위를 좁히는 것이다.
하지만, 위의 방식에서도 문제점이 여전히 존재한다.
- Controller 계층도 아직 테스트하기 어렵다.
- 제어하기 힘든 코드가 Controller 계층에 몰리게 된다.
- 나머지 계층(Service, Repository 등)에서 제어하기 힘든 값을 메소드 인자로 매번 받아야 한다.
결과적으로 이러한 문제를 의존성 주입을 통해 해결했다.
그 과정 간 고민했던 생각에 대해 포스팅해보려 한다.
2. 테스트 방법
LocalDate.now()를 테스트하는 방법은 여러 가지가 있다.
- LocalDate.now() static method 모킹 하기
- LocalDate.now()를 리턴하는 새 클래스를 생성해서 모킹 하기 -> 사용 시 생성한 클래스 의존성 주입
- Clock을 모킹 해서 LocalDate.now(clock) 방식으로 사용하기 -> 사용 시 Clock 의존성 주입
이처럼 여러 방식들이 있지만 나는 (3) 번 방식을 사용했다.
각 방법의 단점을 통해 그 이유를 살펴보자.
(1) 번 방식의 문제점
기본적으로 Mockito 라이브러리에서는 static method를 모킹 하는 기능을 지원하지 않는다.
(mocking 하기 위해선 mockito-inline 라이브러리 별도 설치 후, mockStatic 사용)
모킹 기능을 지원하지 않는 이유는 다음과 같다.
mockito는 mock 객체를 생성하기 위해서 런타임에 동적 코드(cglib)를 사용한다.
이는 런타임에 클래스를 상속해서 생성하는데, static 멤버는 재정의가 불가능 하다.
mockStatic를 통해 static method를 모킹 하더라도 close()를 통해 자원 해제가 추가로 필요하다.
이를 try-with-resources 패턴을 사용하면 자동으로 처리할 수 있지만, 매번 try문이 필요하다. (자세한 사용 방법은 해당 링크 참조)
결국 static method는 실행하기 전에 이미 결정되기 때문에 제어하기 힘들고, 동시에 테스트도 어려운 것이다.
테스트 시, static/final 멤버 mocking이 필요하다면 설계 구조를 다시 생각해 볼 필요가 있다.
(2) 번 방식의 문제점
앞서 LocalDate.now()가 mocking 하기 어려운 이유를 알아봤다.
해당 방식은 아래와 같이 새로운 클래스로 LocalDate.now()를 생성하고, 의존성 주입을 통해 사용하는 것이다.
LocalDate 값을 원하는 대로 설정이 가능하기 때문에 제어가 가능해진다.
하지만, LocalDate 외에 LocalDateTime, LocalTime 등 필요한 시간변수가 추가될 때마다 일일이 지정해줘야 한다.
또, 현재 날짜를 가져오고 싶을 때 누구나 쉽게 LocalDate.now()를 떠올리고 사용한다.
동료 개발자가 해당 클래스의 존재를 모를 경우, LocalDate.now()를 사용할 것이고 현재 겪은 문제를 똑같이 겪을 것이다.
3. Clock 사용
Clock 클래스를 사용하기 앞서 LocalDate.now() static method 내부 코드를 살펴볼 필요가 있다.
내부에서 now(Clock) 메서드를 호출하는 것을 통해, now(Clock) 메서드로도 현재 날짜를 얻을 수 있다.
또, 내부에서 Clock의 instant() 메서드를 호출하는 것을 볼 수 있다.
해당 방식은 Clock 클래스를 빈으로 등록하고, clock.instant()를 mocking 처리하는 것이다.
실제로 oracle에서도 테스트를 위해서 해당 메소드를 사용하는 것을 권장한다.
This will query the specified clock to obtain the current date - today. Using this method allows the use of an alternate clock for testing. The alternate clock may be introduced using
dependency injection.
3.1 Clock 빈 등록
3.2 사용 시 의존성 주입
3.3 테스트 시 clock.instant() mocking 처리
- clock.instant() 메서드는 static method가 아니므로, mocking 처리가 가능하다.
마무리
결과적으로 의존성 주입을 통해 문제를 해결할 수 있었다.
구현 간에 제어할 수 없는 코드는 줄이고, 최대한 외부로 위치시켜 나머지 계층은 순수 코드로 구현하는 것이 중요하다.
또, 최대한 외부로 위치시키고, 가능한 의존성 주입을 통해 해결하는 것이 좋을 거 같다.
참고자료
'테스트코드' 카테고리의 다른 글
동시성 테스트와 테스트 초기화를 위한 @Transactional 사용의 생각 (0) | 2024.02.21 |
---|