2024 12월 Commit 오프라인, 테스트하기 좋은 코드 작성하기

이전에 구름 공식 블로그arrow-up-right를 보면서 너무 좋은 주제와 내용들을 제공해주어, 소식을 구독했다.

"테스트하기 좋은 코드 작성하기" 주제로 진행 예정된 세션 소식을 받았다.. 최근 개발 트렌드에 테스트 코드가 자리 잡고 있다고 생각을 했었고, 나 또한 테스트 코드를 작성하면서 내가 무엇을 위해 작성하고 어떤 이점이 있는지 명확하지 않았으며 제대로 작성하고 있는지 항상 의문이 들었다.

많은 테스트 코드 영상을 봤지만, 온라인으로 보는 것과 오프라인으로 보는 것에는 현장 경험과 집중도에 차이가 있다고 느껴 오프라인 세션에 참가 신청을 했다. (유명하신 이동욱님을 처음 영접할 수 있는 기회이기도 했다.)

참가 자격은 추첨을 통해 이루어졌고, 당근이나 인프런 등 여러 오프라인 행사에 신청해 봤지만 한 번도 당첨되지 않아 지원은 했으나 큰 기대는 하지 않았다.

하지만 정말 감사하게도 구름에서 오프라인으로 세션에 참여할 수 있는 자격을 주셨고, 지인도 함께 당첨 되어서 외롭지 않게 다녀올 수 있었다.

발표하시는 내용을 아래에 정리했지만 매우 주관적이기 때문에 틀린 내용이 있을 수 있다.

테스트 코드에 대한 오해

Q: 개발하기 바쁜데 언제 테스트 코드를 작성하나요?

복잡하게 얽힌 프로그램을 개발할 때, 최종적으로 테스트를 진행하려면 힘들다. 하지만 테스트 코드를 작성하면 빠르게 확인할 수 있다.

기존 방식

내가 기능을 완성하고 해당 기능을 테스트 하는 방식과 99% 유사했다.

이 방식의 단점은 아래와 같다.

  1. 단건 호출에만 적용된다.

  2. 테스트 코드가 없을 때 발생하는 과정이다.

  3. 테스트 코드를 작성하는 것보다 오래 걸린다. 검증 로직에 if 문이 많으면 모든 if 문을 확인하는 API를 작성하는 상황이 발생한다.

  4. 포스트 맨 항목을 작성하고, DB 데이터를 적재하는 과정 자체가 더 느릴 수 있다.

테스트 코드 방식

이 방식의 장점은 아래와 같다.

  1. 숙련도가 높아질수록 작업 임계점이 높아진다. 연습을 통해 숙달되면 더 빨라질 가능성이 있다.

  2. 코어 로직을 작성했을 때, 발생하는 사이드 이펙트를 빠르게 확인할 수 있다. 이로 인해 추가로 발생하는 사이드 이펙트를 고려하는 시간이 줄어든다.

  3. 목표까지 더 빨리 도달할 수 있으며 유지보수 기간, 생산성 향상, 코드 변경에 의한 버그 트래킹에 도움이 된다.

테스트 코드 작성의 어려움은 도구(라이브러리)의 활용 방법 문제가 아니라 코드 자체가 작성하기 어려운 코드 구조를 가지고 있을 경우가 많다.

테스트하기 쉬운 구현 코드를 작성하는 방법

테스트 코드 작성이 쉬운 코드라면, Mock 라이브러리 없이도 할 수 있다. (라이브러리 도움을 최소화 한다.)

테스트하기 좋은 코드

결과가 인수에만 의존하는 부작용 없는 코드 (= 멱등성이 보장되는 순수함수) 이다.

상황에 따라 결과가 무엇이 나올지 예측이 안되면 멱등성이 보장되지 않는다.

피해야 할 코드

  1. 제어할 수 없는 값에 의존하는 코드

  • new Date()와 같이 실행할 때마다 결과가 다른 함수에 의존

  • 브라우저 함수, 전역 변수 등 함수 밖 영역에 의존

  • PG사 라이브러리 등 외부 라이브러리에 의존

예를 들어, discount()라는 요일에 따라 할인을 진행하는 함수가 존재한다. 이 함수는 테스트 실행 일자에 따라 테스트 결과가 달라진다.

멱등성을 유지하기 위해서 날짜를 모킹하는 방법에 의존하지 말고, 다른 방법을 모색하는 것이 좋다.

  1. 외부에 영향을 주는 코드, 받는 코드

  • System.out.print()와 같은 외부 출력, Logger, 이메일 발송, 메시지 큐 등 외부 메시지 발송, DB 의존, 외부 API에 의존하는 경우

예를 들어, DB가 필수로 하는 코드(DB 작업이 수행되는 부분이 테스트하기 어려운 부분이다.)

좋은 코드 (피해야 할 코드를 개선하는 방법)

제어할 수 없는 값에 의존하는 코드 개선

함수 실행마다 달라지는 LocalDate 함수가 포함된 함수는 전부 테스트가 어려워진다.

다른곳에서 테스트하기 어려운 부분을 의존하고 있다면 그 부분까지 어려움이 전파된다.

해결책:

  1. 제어할 수 없는 값은 외부에서 파라미터로 주입받는다.

적절한 날짜 값을 만을어서 테스트 함수에 넘겨주면 검증하기 쉬워진다.

그럼 기존 코드가 다 바뀌는 것은 어쩌지? 코틀린, 타스는 기본 인자로 디폴트 값을 설정할 수 있다. 그럼 자바는?

  1. 자바같은 경우, 구조를 해치지 않는 범위 내에서 의존하는 코드가 가장 적은 영역까지 밀어내는 것이 좋다.

이 의미는, 계층의 바깥(Controller) 쪽으로 밀어내면, Core 부분은 테스트가 쉬워지게 만들 수 있다. (컨트롤러만 어려워짐)

이렇게 하면 서비스, 도메인에서는 인자로 받도록 변경해야 한다.

이 방식이 싫다면 의존성 주입으로도 해결이 가능하다. (테스트에서 인터페이스를 의존하도록 하는 구조)

외부에 의존하는 코드 개선

테스트 DB 환경 세팅, 커넥션 종료, 테이블 초기화 같은 문제를 해결하기 위해 데이터베이스를 모킹하곤 한다. 이는 낮은 테스트 리팩토링 내구성, 지키기 어려운 일관성(같은 값이 DB에 존재), 느린 테스트 문제를 야기한다.

또, 함수의 책임을 최소화하기 위해 서비스에서 여러 private로 분리한 함수의 검증은?

알다시피 private 함수는 일반적으로 테스트할 수 없기 때문에 리플렉션 등을 활용한다.

해결책:

  1. DB에 저장하는 로직을 제외하면 테스트가 쉬워진다.

DB 의존성을 분리하면, 사이드 이펙트 없는 테스트를 구현 가능하다.

구체적으로 얘기하면, DB 의존 테스트는 서비스 통합 테스트에서만 사용하면 도메인은 테스트하기 쉬워지고 서비스만 어렵게 바뀔 수 있다. DB에 종속된 테스트는 단위 테스트로 분리한다.

  1. 클래스를 생성해서 테스트로 검증한다.

서비스 로직에 포함된 여러 valiation 로직이 존재할 때, 이를 private로 변경하면 검증 테스트가 어려워진다. 그래서 다양한 validate 케이스는 클래스로 생성하고 테스트하는 서비스 함수는 이 객체를 받아서 테스트를 한다.

결론은, 테스트하기 쉬운 순수 함수를 최대한 늘리고 부수효과는 최대한 바깥쪽으로 밀어내는 것이다. 라이브러리, 프레임워크에 의존하는 경우 이것이 변경될 때 테스트 코드가 바뀔 확률이 매우 크다.

위와 같은 규칙을 지키면서 테스트 코드를 작성하면 기술의 발전에 대해 코드를 지킬 수 있고, 테스트 코드를 완벽하게 작성했다고 확신이 있으면 에러가 발생했을 때 버그 찾는 시간을 줄일 수 있다. 백엔드가 완벽하게 검증했다면 프론트쪽에서 발생하는 문제일 확률이 크다는 의미이다.

이런 부분이 작업 시간을 단축시키면서 개발 생산성을 향상시켜 줄 것이다.

Summary

결론은, 테스트하기 쉬운 순수 함수를 최대한 늘리고 부수효과는 최대한 바깥쪽으로 밀어내는 것이다. 라이브러리, 프레임워크에 의존하는 경우 이것이 변경될 때 테스트 코드가 바뀔 확률이 매우 크다.

위와 같은 규칙을 지키면서 테스트 코드를 작성하면 기술의 발전에 대해 코드를 지킬 수 있고, 테스트 코드를 완벽하게 작성했다고 확신이 있으면 에러가 발생했을 때 버그 찾는 시간을 줄일 수 있다. 백엔드가 완벽하게 검증했다면 프론트쪽에서 발생하는 문제일 확률이 크다는 의미이다.

이런 부분이 작업 시간을 단축시키면서 개발 생산성을 향상시켜 줄 것이다.

내가 작성한 내용 보다 더 많은 내용들이 있었지만, 테스트를 작성하면서 어려움을 겪으면서 공감되는 내용 위주로 작성했다.

그리고 세션을 들으면서 공감되는 부분들이 많았다. 나도 프로젝트를 진행하면서 QA 중 에러가 발생했을 때, 서버의 문제인지 프론트의 문제인지 며칠을 고생한 적이 있다. 당시에 테스트 코드에 대한 이해도가 있었다면 겪는 어려움을 최소화할 수 있었다고 생각한다.

세션에 참석해 주신 분들의 질문도 매우 훌륭했고 이에 대한 동욱님의 답변도 매우 인상 깊었다.

참여하는 시간은 빠르게 흘러갔으며 정말 값진 시간이라고 느껴졌다. 추가로, 최근에 출간된 단위 테스트의 기술arrow-up-right 책을 추천해 주셔서 기회가 되면 읽어봐야겠다.

Last updated