박종훈 기술블로그

Spring 기반 프로젝트에서 Service 클래스 안의 로직에 대한 테스트를 시도 하면서 경험한 것 (로직 분리, 함수형 프로그래밍, 순수 함수)

회사에서 최근에 B2B PMS 서비스를 릴리즈 하였다.

릴리즈 하고 끝나는 것이 아니라 릴리즈 된 제품에 대해서도 계속 문제가 없도록 관리를 해줘야 한다.

릴리즈 중 데드라인을 맞추느라 급하게 달려온 감이 있었기 때문에, 비즈니스 로직에 대한 테스트 코드를 추가해야 겠다는 생각이 들어서 최근 조금씩 테스트 코드를 작성하고자 하려 한다. 물론 서비스를 띄워서 수동으로 기능에 대한 확인은 하였고, 지금도 계속 확인해나가고 있는 중이다. 그럼에도 사람을 실수를 할 수 밖에 없기 때문에, 해당 부분에 대한 작업이 필요하다고 생각하며 앞으로 추가적으로 기능들을 만들어 가면서도 기존 기능이 정상동작하는 것에 대한 보증이 되어야 한다고 생각하였다.

개발은 내가 아닌 각 팀(백엔드, 프론트)에서 진행되었다. (아마) 모든 개발자가 알겠지만, 테스트는 중요하다. 하지만 현실은 기능 개발하기에도 촉박하다. 이해는 한다. 그렇기 때문에 할 수 있는 부분부터 진행해야 하는 것 같다.

우선 기간 검증과 그로부터 이어지는 비용 계산에 대한 부분을 테스트 코드를 작성하고자 하였다. 돈과 관련된 부분이 가장 중요하다는 생각이 들었기 때문이였다.

이 두 부분은 특정 Service 클래스를 거치고 있었고 테스트 코드는 무난하게 작성이 되었다. 많은 테스트를 작성하지는 않았고, 기본적인 테스트만 작성해보았다.

그런데 테스트 코드를 작성하고 아쉬웠던 부분은 테스트에서 데이터만 구성 해서 함수만 호출하면 로직에 따라서 결과가 나오면 좋을것 같았는데 service 클래스 안에 있다보니 Spring 프로젝트를 init 한 후에 테스트를 진행한다는 것이였다. 그래서 데이터베이스까지도 켜두고 테스트를 진행해야 했다. (로컬 환경에서는 DB를 따로 docker로 띄워 실행하고 있다.) 결과적으로 테스트를 시작하는데 시간이 오래 걸렸다.

참고로 단위 테스트의 특성은 다음과 같다. (출처 : 블라디미르의 단위테스트 - 단위 테스트의 두 분파)

단위 테스트는

  • 단일 동작 단위를 검증하고
  • 빠르게 수행하고
  • 다른 테스트와 별도로 처리한다.

스프링 init을 거치게 되면 1초 이상이 걸리기 때문에 빠르게 수행하고 라는 부분에 충돌되게 된다고 생각이 들었다. (1초 라는 기준은 개인적으로 세운 것이며, 굳이 1초가 아니더라도 init 과정에는 30초 이상이 소요된다. TDD에서도 이야기 하는 것이 테스트를 빠르게 반복할 수 있어야 한다는 부분이다. 속도가 느리다면 자주 실행하기에 어려움이 생긴다.)

이런 상황에서 해당 로직의 코드를 보았고, repository와 같은 서비스의 다른 부분들을 사용하지 않고 있기 때문에 굳이 서비스에서 해당 로직을 처리하는 것이 아니라 해당 로직 부분을 분리하면 좋겠다는 생각이 들었다. 데이터만 구성하여 input으로 넣어주면 그에 대한 결과만 나오게 하면 좋겠다는 생각이 들었다.

이런 생각을 하면서 블라디미르의 단위테스트 책에서 이야기 하는 함수형 프로그래밍/아키텍처 부분이 떠올랐고, 저자가 왜 이런 이야기를 했는지 이번 상황을 통해서 실전적으로 이해가 되었던 것 같다.

우선은 내가 하고 있는 생각에 대해 백엔드 개발자 분들은 어떻게 생각하실까 의견을 듣고싶어서 아래와 같이 질문을 올렸다.

spring boot 프로젝트에서 서비스 클래스 안에 있는 로직에 대한 단위 테스트 코드를 작성하려는데

서비스 클래스의 함수를 직접 사용하려면 서비스 클래스에 들어있는 클래스들 (repository들) 까지 초기화를 해줘야 하는 이슈가 있습니다. (테스트를 하고자 하는 로직에서는 레포지토지를 사용하지 않습니다. 해당 서비스 클래스의 다른 메소드가 레포지토리를 사용하기 때문에 서비스 클래스에 선언되어 초기화 되고 있는 상황입니다.)

만약 이런 상황이면 테스트에서 해당 로직을 직접 사용할 수 있도록 해당 로직을 별도의 클래스로 분리를 하는게 좋을것 같다는 생각이 드는데 혹시 이 생각의 흐름이 바람직 한걸까요? (현재 상태에서는 전체 초기화를 해야해서 이러면 단위 테스트가 아니라 통합 테스트가 되어 버린다고 생각됨)

개발자 분들은 아래와 같은 의견을 주었다.

  1. 분리하는게 맞을 것 같다. 들어봤을 때 util 성격의 로직으로 보인다.
  2. 모킹하는 것도 좋을 것 같다.
  3. slice 테스트를 해보는 것이 좋을 것 같다. (scan 범위 조정)

ChatGPT한테도 물어봤는데 어느정도 비슷한 의견을 주는데 아래와 같은 답변을 주었다.

  1. 의존성 주입(Dependency Injection): 서비스 객체에 의존하는 Repository 등을 외부에서 주입할 수 있습니다. 테스트에서는 Mock 객체 등을 주입하여 특정 동작을 시뮬레이트할 수 있습니다.
  2. 테스트용 Configuration 사용: Spring에서는 @Profile 애너테이션을 사용하여 특정 환경에서만 사용되는 빈을 정의할 수 있습니다. 테스트용 Configuration 클래스를 만들어 테스트에서 필요한 빈들을 구성할 수 있습니다.
  3. Test Slice 사용: Spring에서는 특정 레이어(예: @Service, @Repository, @Controller 등)에 대한 슬라이스 테스트를 제공합니다. @SpringBootTest 애너테이션에 @DataJpaTest, @WebMvcTest 등을 조합하여 필요한 레이어만 테스트할 수 있습니다.

받은 답변에서 목의 경우에는 최대한 사용하지 않으려고 했다. 그 이유는 테스트 코드를 작성을 하는데 있어서 단위테스트에서는 목을 사용하지 않고자 하였고 통합테스트 에서도 제어가 불가능한 비관리 의존성에서만 사용하는 걸 목표로 하였기 때문이였다.(관련해서는 목 처리에 대한 모범 사례 참고)

그래서 일단 해당 로직을 별도의 클래스로 분리해서 순수함수 형태로 처리한 후 시간을 비교해 보았다.
Spring 초기화를 했을 때는 30초 정도 걸리던 테스트가 초기화 시간이 사라져 7초 정도면 완료 되었다. 1/3 이상 줄어들은 것이다.

test-result

사실 이 7초도 더 줄여보고 싶은데, Spring 초기화를 거치지 않더라도 빌드하고 테스트 환경을 초기화 하는데 시간이 걸리는건가 싶다.

위 테스트 결과 이미지에 빌드하는데 걸린 시간은 포함되지 않는다. 다만 콘솔에서는 아래와 같이 나오긴 한다 (5s.)

build-time

이 부분은 어떻게 줄일 수 있을지 더 알아봐야 할 것 같다.

돌아오는 월요일에 백엔드 개발자와 개선 사항에 대해서 같이 논의해보면 좋을 것 같다.

답변을 주시고 도움을 주신 분들께 감사하다는 말을 이 글을 통해서 남긴다.


블라디미르의 단위 테스트는 참 좋은 책이다. 읽으면서도 많은 것을 배울 수 있었고, 읽고 나서도 읽은 내용이 왜 그렇게 작성되었는지 실제적인 경험을 통해 이해되었을 때 새롭게 오는 재미가 있는 책인것 같다. 회사에서도 개발중에 종종 펴보게 되는 책이다. 테스트 작성에 대해 알아가고 싶은 개발자에게 추천한다.