2 minute read

시간에 따라 테스트가 실패한다?

지금 진행하고 있는 프로젝트인 체크메이트는 출결 관리 서비스입니다. 출석이라는 도메인 자체가 시간과 밀접해있기 때문에 시간에 따라 도메인의 상태가 많이 변경하게 됩니다.

예를 들어, 미팅 시작 시간 30분 전부터 미팅 시작 시간 5분 후 까지는 출석부가 수정이 허용이 되고, 출석 허용시간이 끝나면 출결 상태가 지각으로 바뀌어야 하는 등 기능 요구사항들과 시간이 굉장히 밀접하다는 것을 확인할 수 있습니다.

시간과 밀접한 기능에 대한 로직을 작성할 때, LocalDateTime.now()(요청이 들어오는 시간) 를 이용하게 되니 시간에 따라 테스트의 성공 여부가 달라지게 되었고 시간에 의존적이지 않은 테스트 코드를 작성하기 위해 고민하게 되었습니다.


시간에 따라 테스트가 실패했던 예시에 대해 살펴보겠습니다.

아래의 코드는 출석체크 하는 기능으로 전체적인 플로우에 대해 설명을 해보자면,

  • meetingId와 UserId에 대해 존재하는 id인지 확인한다.
  • 조회하려는 날짜에 미팅의 이벤트가 있는지 확인한다. (아래 그림의 빨간색 부분 -> 테스트를 실패하게 했던 요인)
    • 조회하려는 날짜에 미팅의 이벤트가 존재하지 않으면 예외를 발생한다.
  • 일정이 있는 경우에 참가자에 대한 출석처리를 한다.

AttendanceService.java

테스트를 수행하는 시간에 따라, LocalDate.now의 값이 변경하게 되면서 시간에 매우 의존적인 상황입니다.

예를 들어, 데이터 베이스에 2022년 8월 21일의 일정만 저장되어 있다면, 2022년 8월 21일에만 테스트를 성공 시킬 수 있었습니다.


실패했던 테스트 코드에 대해서 살펴보겠습니다.

image

위 테스트 코드에서 이벤트 객체는 2022년 8월 1일로 저장하였습니다.

현재 프로덕션 코드에서는 LocalDate.now로 조회를 하고 있기 때문에, 테스트 돌리는 날짜에 따라 이벤트를 조회하는 날짜가 달라지게 되었고 아래의 테스트 코드는 2022년 8월 1일에만 통과하게 되었습니다.

테스트를 실행한 날짜는 2022년 11월 1일로, 일정이 존재하지 않는다는 에러를 확인할 수 있습니다.

image


추상 클래스를 사용하여 production과 test를 분리하자

위 문제를 해결하기 위해 요청 시간을 관리하는 객체를 만들어, test에서는 시간을 지정할 수 있게 하자는 목적으로 production과 test에서 사용될 객체를 각각 정의했습니다.

먼저 LocalDateTime (요청 시간)을 필드로 가지는 DateTime이라는 추상 클래스를 정의했고, changeDateTime 추상 메서드를 production과 test에서 각각 다르게 구현하였습니다.

image

  • 테스트코드에서 사용 될 FakeDateTime은 changeDateTime으로 시간을 지정할 수 있게 하였고,

image

  • 프로덕션 코드에서 사용될 ServerDateTime은 changeDateTime 메서드가 호출되면 예외가 발생하게 하였습니다.

image


changeDateTime 호출 전, 후로 디버깅을 찍어 확인해보았을 때, 2022년 11월 1일에서 2022년 8월 1일로 변경된 것을 확인할 수 있습니다.

image

image

아래는 전체적인 구조입니다.

image


테스트 코드에서는 어떻게 FakeDateTime이 주입되게 하였나

FakeDateTime의 객체 위치를 test 패키지 하위에 배치했고, @Primary 어노테이션을 붙여주었습니다.

  • DateTime을 의존하고 있는 ServerTimeManager가 만들어질 때, 처음에는 main 패키지에 존재하는 ServerDateTime을 주입받아 빈이 구성이 되지만,
  • test 패키지를 빌드하면서 @Primary가 붙어 있는 FakeDateTimeServerDateTime의 빈을 덮어씌우게 하였습니다.


싱글톤 객체는 상태를 가지고 있으면 안된다

Spring bean의 빈 스코프의 기본 값은 싱글톤입니다.

싱글톤 방식은 여러 클라이언트에서 하나의 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지하게 설계하면 안되는데요.

위에서 설계한 DateTime 빈은 LocalDateTime이라는 상태를 가지고 있기 때문에, 상태가 다른 클라이언트에서도 공유되지 않게 해야했습니다.

그래서 빈 스코프를 prototype으로 할지, request로 할 지 고민이 들었는데 request가 더 간편해서 request로 결정하게 되었습니다.

그 이유로는 DateTime을 의존하고 있는 ServerTimeManager는 싱글톤 빈인데, 싱글톤 빈과 프로토타입 스코프 빈을 함께 사용할 때 고려해야하는 부분이 있습니다.

싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 처음 주입받은 프로토타입 빈과 함께 계속 유지되어 사용할 때마다 ApplicationContext에서 직접 조회해서 사용하거나 Provider 같은 라이브러리를 사용해야 합니다.

반면 request 스코프는 요청 하나가 들어오고 나갈 때까지만 유지가 되므로 더 알맞다고 생각했습니다.