최근 자바로만 프로그래밍을 진행하면서 많은 고민을 하게 되었다.
좋은 코드란 무엇인지, 객체지향적인 코드는 무엇인지, 역할과 책임은 어떻게 나눠야 하는지에 대해 깊이 생각해보는 계기가 되었다. 그중에서도 가장 익숙하면서도 낯설게 느껴졌던 주제는 바로 테스트 코드였다.
테스트 코드에 대해 많이 들어왔고, 실제로도 AssertJ와 JUnit 5를 활용해본 적이 있다.
테스트 코드의 필요성을 느끼긴 했지만, 정작 프로젝트를 진행할 때는 주로 Postman이나 Swagger를 활용해 DB에 직접 요청을 보내고 결과를 눈으로 확인하는 방식에 의존했다.
그러던 중, 이동욱(인프랩 CTO)님께서 구름 커밋에서 테스트 코드를 주제로 세미나를 진행한다는 소식을 접하고 참여하게 되었다.
이 글은 해당 세미나를 통해 배운 내용 중 특히 인상 깊었던 테스트하기 좋은 코드란 무엇인가에 대한 내용을 정리하고, 배운 점을 공유하기 위해 작성되었다.
2024년 초, 어느 컨퍼런스에서 테스트 코드에 대한 부정적인 의견들이 많이 나왔던 기억이 있다.
특히, 한 스타트업에 재직 중인 2년 차 개발자가 마이크를 잡고 했던 말이 떠오른다.
“저희 팀은 테스트 코드를 절대 작성할 계획이 없고, 그럴 시간도 없습니다.”
이번 테스트 코드 컨퍼런스를 들으면서 그때 들었던 말들이 다시 떠올랐다.
하지만 이제는 그것이 단순한 오해였다는 것을 깨닫게 되었다.
“시간이 없어서 테스트를 못 만들었다”는 말은 “나는 테스트 코드를 만드는데 시간이 많이 걸린다”는 말과 동일합니다.
테스트 코드를 작성하는 데 걸리는 시간이 기존 방식의 (2) ~ (7) 과정보다 더 오래 걸린다면, 기존 방식이 더 효과적이라고 판단할 수도 있다. 하지만 테스트 코드를 도입하면 작업 효율과 숙련도가 높아지는 데 크게 기여한다.
테스트 코드를 작성하는 문화가 조직에 자리 잡지 않았다면,
먼저 팀 문화를 존중하고 신뢰를 쌓은 후 테스트 코드 도입을 고려하자.
테스트 코드를 통해 더 빠르고 안전한 제품을 출시하고,
핵심 기능과 연관된 작업에서도 다른 코드에 영향을 주지 않는 개발 방식을 구현하자. 🚀
테스트 코드 프레임워크 숙련도가 높아도 테스트 코드 작성은 어렵다.
테스트 하기 좋은 코드는 결과가 인수에만 의존하는 부작용이 없는 코드를 말한다. 다시 말해서 멱등성이 보장되는 순수함수를 말한다. 예를 들어 a를 넣으면 b가 return 되는 것이 보장되는 것 그렇지 않은 경우는 어떤 날짜, 상황이 변하면 b의 값이 달라지는 것. 구체적으로는, 일요일에 할인 정책이 적용되는 함수가 다른 요일에 테스트를 하면 날짜가 달라져 통과를 못하는 경우.
new Date() 등 실행할 때마다 결과가 다른 함수에 의존하는 경우 테스트가 어렵다. 브라우저 함수, 전역 변수 등에 함수 밖의 영역에 의존하는 경우 테스트가 어렵다. PG사 라이브러리 등 외부 상황에 의존하는 경우 테스트가 어렵다. 예를 들어, 주문일이 일요일이면 주문 금액의 10%를 할인하는 함수. 이런 경우에는 테스트 실행 일자에 따라 테스트 결과가 달라짐. 테스트 하기 어려운 부분과 테스트 하기 쉬운 부분을 나누자. 테스트가 어려운 부분은 LocalDate.now() 이부분이다. 위 함수 부분을 변수로 받아서 테스트로 만든다. 즉, 테스트 하기 어려운 부분과 쉬운 부분을 나누는 것.
log), 이메일 발송, DB 연동 등이 포함된 코드 제어할 수 없는 걸 개선해보자. 예를 들어, 주문하는 상황에서 discount() 를 호출하는 경우, 모든 부분이 테스트가 어려워지는 상황을 생각해보자. 테스트의 어려움은 전파가 되는데 특히 Core에 있을 떄 어려워진다.
Controller - Service - Domian - Repository
제어할 수 없는 값을 외부에서 주입받도록 하면 원하는 상황(제어할 수 있음) 테스트 가능하게 된다. 이 상황이 조삼모사처럼 보일 수 있다. JS/TS, Kotlin의 기본 인자를 활용할 수 있다면 기본인자를 활용한다.
java에는 기본 인자 기능이 없는데 어떻게할까. 구조를 해치지 않는 범위 내에선 의존하는 코드가 가장 적은 영역까지 밀어내는 것이 좋다. 제어하기 어려운 값을 계층의 바깥으로 밀어낸다. (Domain에 있는 어려운 걸 Controller로 밀어내는 것) 이렇게 되면 Controller만 테스트가 어렵고 Service 등 나머지는 테스트가 쉬움. Domain도 안전해지는 상황을 만들 수 있다.
그러나 Controller의 테스트는 여전히 어렵다. Service, Repository, Component 등 많은 함수가 제어할 수 없는 값을 항상 인자로 받아야만 한다.
이럴 때는 의존성 주입을 사용한다.
Controller - Service - Time(Interface) - Jodal Time, Sub Time
실제는 Jodal Time을 사용하지만 테스트는 Sub Time을 사용. 중간에 Interface를 사용해서 의존성을 주입 받을 수 있게 한다. static을 사용하지 말고 의존성으로 주입 받을 수 있게 한다.
의존성 주입 활용
로직 분리
private 메서드 검증
좋은 테스트 코드는 자연스럽게 좋은 함수와 구조를 만들게 한다. 비즈니스 로직은 순수함수로 구현하고, 부수효과는 외부로 밀어내어 격리하는 것이 중요하다. 이를 통해 프레임워크나 라이브러리에 의존하지 않고도 테스트하기 쉬운 코드를 작성할 수 있다.
결국 객체지향, 좋은 코드를 작성하려고 노력하다 보면 테스트 코드 역시 작성하기 쉽고, 이렇게 되면 개발의 생산성도 높아진다는 걸 느꼈다. 이전까지 눈으로 검증을 해왔지만 눈으로 한다는 생각을 별로 하지 않았는데 앞으론 가능하면 Mocking, Postman, Swagger 등 사용을 최소화하면서 개발의 효율, 안정성을 챙겨보려고 한다.