[Spring] ❌ Static 메소드를 모킹하지 말자

헌치·2022년 8월 10일
4

Spring

목록 보기
4/13

저번 포스트에서 LocalDateTime.now()를 모킹하기 위해 clock 객체를 Bean으로 등록 후 now() 속 파라미터로 주입했다.

1. static 메소드를 mocking하는건 안티패턴이다

PowerMock 방식과 mockStatic 방식 모두 그렇다.

Mockito 깃허브의 Enable mocking static methods in Mockito#1013 이슈에서 관련 논의가 활발하게 진행되었다.
https://github.com/mockito/mockito/issues/1013

그중 일치하는 의견은 일반적으로

static 메소드를 mocking하는건 안티패턴이며, 권장되지 않는다.”

였다.

이에 대한 몇가지 얘기들을 찾아봤다.

내가 보는 정적 모킹의 주요 사용 사례는 다음과 같습니다.

  • 레거시 코드(나는 정말로, 정말로 단위 테스트를 작성하고 싶지만 고대의 추악한 코드를 감히 변경할 수 없습니다)
  • 일부 어색한 정적 타사 API를 처리합니다. 이 시나리오는 현재 타사 통계 위에 일부 주입 가능한 API 레이어를 개발하여 실행할 수 있습니다. 그러나 해결 방법은 번거롭고 코드베이스의 명확성을 망칠 수 있습니다.

위의 2가지 사용 사례를 다루지 않고 전 세계의 개발자는 Powermockito 등과 같은 도구에서 도움을 찾고 있습니다.

정적 모킹 제공의 가장 큰 단점은 정적 메서드로 가득 찬 진부하고 절차적 코드를 테스트하기가 너무 쉽다는 것입니다. 우리는 코드를 깨끗한 OO/DI로 리팩토링하려는 동기를 제거할 것입니다.

Enable mocking static methods in Mockito · Issue #1013 · mockito/mockito

저는 정적 메서드를 광범위하게 사용하고 테스트에 PowerMock(및 PowerMockito)를 많이 사용하는 여러 대규모 코드베이스에서 작업했습니다. 그 당시 내 주요 기억은 테스트 실행이 정말 느리고(어떤 경우에는 PowerMock을 사용하는 테스트가 그렇지 않은 테스트보다 문자 그대로 10배 느림) 궁극적으로 PowerMock의 모든 사용을 제거해야 한다는 것입니다. t 최신 버전의 Java와 호환됩니다. 또한 세부 사항은 더 이상 기억나지 않지만 일부 라이브러리와 호환되지 않아 테스트를 통과하도록 하는 특수 설정이 필요한 것을 기억합니다.

나쁜 코드를 홍보하는 것에 대한 우려를 이해할 수 있지만 Mockito 사용자로서 성능과 견고성에 미치는 영향에 대해 더 걱정할 것입니다. PowerMock을 사용하는 모든 코드가 레거시가 아닙니다. 그 중 일부는 경험이 없는 팀(내가 속해 있던)이 작성한 새로운 코드였으며 Mockito 개발자가 우리의 디자인 패턴을 승인하지 않았다는 사실을 알고 있었다면 별 차이가 없었을 것입니다.

이것은 몇 년 전의 일이며 그 이후로 정적 메서드를 조롱하는 기술이 크게 향상되었을 수 있습니다. 이 기능을 구현하는 강력하고 성능이 좋은 방법이 있다고 생각한다면 환영합니다(사용할 필요가 없기를 바라지만).

Enable mocking static methods in Mockito · Issue #1013 · mockito/mockito

나는 이것에 찢어졌다.(대충 슬프다는 얘기인 듯) 이러한 가능성을 제공하면 개발자를 잘못된 관행에 초대할 수 있습니다. 개발자가 이 기능을 사용할 때마다 그것이 나쁜 습관임을 매우 명시해야 합니다. 이 면에서 명확하면서도 개발자가 동의할 수 있는 솔루션을 제시할 수 있습니까?

Enable mocking static methods in Mockito · Issue #1013 · mockito/mockito

해당 의견은 이슈를 등록했고 Mockito의 멤버인 mockitoguy 의 의견이다.

나는 우리 모두 정적을 조롱하는 것이 나쁜 습관이며 일반적으로 반패턴이라는 데 동의한다고 생각합니다. 문제는 "정적 조롱 금지" 정책을 시행하는지(예: 도구에서 기능을 제공하지 않음) 또는 사용자가 시행 여부를 결정하도록 하는 것(예: 기능 제공)입니다.

Enable mocking static methods in Mockito · Issue #1013 · mockito/mockito

2. LocalDateTime.now(clock) 을 쓰자

우테코 프로젝트를 진행하며, static methodLocalDateTime.now() 자체를 모킹하는 방법을 고려하고 진행하는 팀도 있었다.

static 메소드 자체를 mocking 했던 이유는 다음과 같다.

  1. 우리는 일반적으로 LocalDateTime.now()clock 객체를 주입하지 않는다. 신입개발자의 경우 clock 객체를 주입하지 않아 테스트에 어려움을 겪을 수도 있다.
  2. 테스트를 위해 비즈니스 로직을 수정하고 Bean 등록을 하는게 부자연스럽다.

이 두 질문에 답하기 위해 자료를 찾아봤다.

1번의,

LocalDateTime.now() 가 아닌 LocalDateTime.now(clock) 형태로 프로덕션 코드를 구성하는 것이 과연 옳은가?

에 대해 궁금해졌다.

실제로 LocalDateTime.now(clock) 형태의 구현이 일반적이지 않은지 궁금했다.

관련해 Oracle Docs를 살펴봤더니 다음과 같이 설명되어 있었다.

LocalDate (Java Platform SE 8 )

public static LocalDate now()

기본 시간대의 시스템 시계에서 현재 날짜를 가져옵니다.

system clock은 현재 날짜를 얻기 위해 기본 시간대에서 쿼리합니다 .

이 방법을 사용하면 clock이 하드 코딩되어 있기 때문에 테스트를 위해 대체 clock을 사용할 수 없습니다.

Returns: null이 아닌 시스템 시계와 기본 시간대를 사용하는 현재 날짜

public static LocalDate now(Clock  clock)

지정된 시계에서 현재 날짜를 가져옵니다.

현재 날짜(오늘)를 얻기 위해 지정된 시계를 쿼리합니다. 이 방법을 사용하면 테스트를 위해 대체 clock을 사용할 수 있습니다. 
dependency injection(DI)을 사용하여 대체 clock을 도입할 수 있습니다.

매개변수: clock- 사용할 시계, null이 아님

Returns: null이 아닌 현재 날짜

즉, LocalDate.now()를 테스트할 수 없는 문제를 해결하기 위해 만들어진 메소드가 LocalDate.now(clock) 이었다!

3. 테스트를 위해 Bean 등록을 해도 된다

그렇다면 이번엔 2번에 대해 궁금해졌다!

테스트를 위해 비즈니스 로직을 수정하고 Bean 등록을 해도 되는가?

관련해 토비의 스프링을 찾아봤다.

5장 서비스 추상화에서 다음과 같은 내용들이 있었다.

DummyMailSender에 대해 좀 더 생각해보자. DummyMailSender 클래스는 아무것도 하는 일이 없다. MailSender 인터페이스를 구현해놨을 뿐 메소드는 비어 있다. 하는 일이 없으면 가치도 없어야 할 텐데, 사실 이 클래스의 가치는 매우 크다.
이 클래스를 이용해 JavaMail로 메일을 직접 발송하는 클래스를 대치하지 않았다면 테스트는 매우 불편해지고 자주 실행하기 힘들었을 것이다.
스프링의 XML 설정파일을 테스트용으로 따로 만든 이유는 무엇인가? 개발자 환경에서 손쉽게 이용할 수 있는 테스트용 DB를 사용하도록 만들기 위해서다. 그렇지 않고 운영 중인 DB 서버를 WAS의 풀링 서비스를 통해 이용하게 만들었다면 테스트를 자주 실행할 엄두도 못냈을지 모른다.
이처럼 테스트 환경에서 유용하게 사용하는 기법이 있다. 대부분 테스트할 대상이 의존하고 있는 오브젝트를 DI를 통해 바꿔치기하는 것이다.(…)

서비스 추상화란 이렇게 원활한 테스트만을 위해서도 충분히 가치가 있다. 기술이나 환경이 바뀔 가능성이 있음에도, JavaMail처럼 확장이 불가능하게 설계해놓은 API를 사용해야 하는 경우라면 추상화 계층의 도입을 적극 고려해볼 필요가 있다. 특별히 외부의 리소스와 연동하는 대부분 작업은 추상화의 대상이 될 수 있다.(…)

테스트 대상 오브젝트의 메소드가 돌려주는 결과뿐 아니라 테스트 오브젝트가 간접적으로 의존 오브젝트에 넘기는 값과 그 행위 자체에 대해서도 검증하고 싶다면 어떻게 해야 할까? 이 경우에는 단순하게 메소드의 리턴 값을 assertThat()으로 검증하는 것으로는 불가능하다.
이런 경우에는 테스트 대상의 간접적인 출력 결과를 검증하고, 테스트 대상 오브젝트와 의존 오브젝트 사이에서 일어나는 일을 검증할 수 있도로 특별히 설계된 목 오브젝트(mock object)를 사용해야 한다.
목 오브젝트는 스텁처럼 테스트 오브젝트가 정상적으로 실행되도록 도와주면서, 테스트 오브젝트와 자신의 사이에서 일어나는 커뮤니케이션 내용을 저장해뒀다가 테스트 결과를 검증하는 데 활용할 수 있게 해준다.(…)

이렇게 테스트 환경을 만들어주기 위해, 테스트 대상이 되는 오브젝트의 기능에만 충실하게 수행하면서 빠르게, 자주 테스트를 실행할 수 있도록 사용하는 이런 오브젝트를 통틀어서 테스트 대역(test double)이라고 부른다.
대표적인 테스트 대역은 테스트 스텁(test stub)이다.
테스트 스텁은 테스트 대상 오브젝트의 의존객체로서 존재하면서 테스트 동안에 코드가 정상적으로 수행할 수 있도록 돕는 것을 말한다.
일반적으로 테스트 스텁은 메소드를 통해 전달하는 파라미터와 달리, 테스트 코드 내부에서 간접적으로 사용된다. 따라서 DI 등을 통해 미리 의존 오브젝트를 테스트 스텁으로 변경해야 한다.(…)

목 오브젝트를 이용한 테스트라는 게, 작성하기는 간단하면서도 기능은 상당히 막강하다는 사실을 알 수 있을 것이다. 보통의 테스트 방법으로는 검증하기가 매우 까다로운 테스트 대상 오브젝트의 내부에서 일어나는 일이나 다른 오브젝트 사이에서 주고받는 정보까지 검증하는 일이 손쉽기 때문이다.(…)

서비스 추상화는 테스트하기 어려운 JavaMail 같은 기술에도 적용할 수 있다. 테스트를 편리하게 작성하도록 도와주는 것만으로도 서비스 추상화는 가치가 있다.

Mockito는 지금까지 나온 목 오브젝트 방식을 지원하는 프레임워크 중에서 가장 사용하기 편리한 기능을 갖고 있다.
처음엔 조금 어렵게 느껴질지 모르겠지만, 목 프레임워크만의 독특한 사용 방법에 익숙해지면, 빠른 속도로 단위 테스트를 만드는 데 강력한 도구가 될 것이다.

<토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리> (이일민 지음)

요약하자면

  1. 대상이 의존하고 있는 오브젝트를 DI를 통해 바꿔치기 하는 것을 서비스 추상화라 한다.
  2. 서비스 추상화는 원활한 테스트만을 위해서도 충분히 가치가 있다.
  3. 외부의 리소스(API)와 연동하는 대부분 작업은 추상화의 대상이 될 수 있다.
  4. 테스트 대상 오브젝트와 의존 오브젝트 사이에서 일어나는 일을 검증할 수 있도로 특별히 설계된 목 오브젝트(mock object)를 사용해야 한다.
  5. 테스트 환경을 만들어주기 위해, 테스트 대상이 되는 오브젝트의 기능에만 충실하게 수행하면서 빠르게, 자주 테스트를 실행할 수 있도록 사용하는 이런 오브젝트를 통틀어서 테스트 대역(test double)이라고 부른다.
  6. 테스트 스텁을 사용할 시 DI 등을 통해 미리 의존 오브젝트를 테스트 스텁으로 변경해야 한다.
  7. 목 오브젝트를 이용한 테스트는 작성하기는 간단하면서도 기능은 상당히 막강하다.

clock 객체를 DIBean으로 등록하고, Mockito 등을 통해 Mock 객체로 만드는 것은 권장되는 일이며, 이상하지 않다!😉

관련 포스팅


그래도 정적 메소드를 모킹하고 싶다면?

Mockito로 정적 메소드 모킹: 예제와 함께 설명
해당 영문 아티클을 참고하자.

profile
🌱 함께 자라는 중입니다 🚀 rerub0831@gmail.com

3개의 댓글

comment-user-thumbnail
2022년 11월 9일

와 헌치.. 대박이다.. 글 잘봤습니다..

혹시 Clock을 bean으로 등록하고 어플리케이션이 돌아가는 중간에도 시간을 또 바꾸고 싶다면 어떻게 해야하나요?

2개의 답글