라고 썼지만 사실 작업속도가 느린 것에 대한 변명이 아닐까… ㅋㅋㅋ

🧪 고민의 시작

작년 한 해 동안 우아한테크코스에서 정말 많은 테스트 코드를 작성했다. 테스트 코드에 대한 중요성을 거의 주입 받다시피 배웠고, 주변 모든 크루들도 테스트 코드를 작성하고 있으니 자연스럽게 습관으로 만들 수 있었다. 전체 로직이 마음에 안 들면 다 날려버리고 처음부터 다시 짜는 게 일상이던 나에게 테스트 코드를 믿고 특정 로직만 리팩터링(리팩터링 후 테스트 코드로 검증)하는 일련의 경험들은 정말 많은 인사이트를 남겨주었다.

1년간 만들어온 습관 덕분에 우아한테크코스를 수료한 후에도 계속해서 테스트 코드를 작성했다. ‘정말 단순한 로직은 테스트 하지 말자.’, ‘여러 로직이 겹쳐서 분기가 생기는 로직은 꼭 통합 테스트를 하자’ 와 같은 나만의 규칙을 세우기도 했다.

그러나 본격적인 회사 업무가 주어지고, 테스트 코드 작성 때문에 작업 시간이 지나치게 늘어짐을 느끼면서 고민이 시작됐다. 가령 비율을 이야기하자면 아래와 같다.

- 로직 구현 방식 고민 (2시간)
- 로직 구현 코드 작성 (1시간)
- 통합 테스트 코드 작성 방법 고민 (2시간)
- 통합 테스트 코드 작성 (2시간)
---------------------------------
2 + 1 + 2 + 2 = 총 7시간

한눈에 봐도 문제가 느껴진다. 테스트 코드를 생략하면 3시간 만에 끝낼 일을 7시간에 걸쳐서 진행하고 있다. 준비, 마무리가 복잡한 통합 테스트에서는 그 준비, 마무리 코드 작성 고민에 대부분의 시간을 소모하게 된다. (특히 JPA 영속성 관련해서)

위 상황만 보면 테스트 코드는 작업시간을 늘리는 근원이며, 굳이 작성할 필요가 없는 코드로 보인다. 그런데도 내가 테스트 코드에 집착하는 이유는 따로 있다.


🧪 테스트 코드는 왜 작성하는가?

대부분의 개발자는 ‘테스트 코드를 왜 작성하는가?’에 대한 교과서적인 정답을 이미 알고 있다. 이미 오래전부터 대두되어 왔고, 관련 책이나 레퍼런스가 굉장히 많다. 테스트 코드를 왜 작성하는가에 대한 간단한 이유를 몇 가지를 살펴보자.

  • 개발 과정에서 문제를 미리 발견할 수 있다.
  • 리팩터링을 안심하고 할 수 있다.
  • 애플리케이션을 꼭 실행하지 않고도 동작 결과를 확인할 수 있다.

구구절절 맞는 이야기고, 실제로 경험하고 있는 장점들이다. 그러나 대부분의 장점들이 개발자 한 사람에게 포커스가 맞춰져 있다. 그리고 그것들조차 good to have 로 느껴질 뿐, must have 로 느껴지진 않는다. 나는 조금 다른 이유를 더해서 테스트 코드를 작성한다. 주로 협업에 관련된 이유다.

  • 내가 짠 로직을 추후 다른 개발자가 리팩터링 할 가능성이 높다.
  • 로직 코드만으론 의도 파악이 어렵다.
  • 테스트 코드로 현재 로직의 의도를 제공한다.

옛날에야 비즈니스가 성장하면 기존 애플리케이션을 버리고 새로 만드는 일이 비일비재 했다고 하지만, 요즘은 애플리케이션도 함께 성장하며 발전해 나간다. 이 과정에서 (내가 생각하기에) 전체 기능 중 적어도 50% 이상은 리팩터링 대상이 된다. 즉, 내가 짠 코드를 추후 다른 개발자가 보고 있을 가능성이 높다는 이야기다. 다른 개발자가 내 코드를 한눈에 파악할 수 있을까? (저번 주에 내가 짠 코드도 내가 못 알아보는데…) 내가 제네럴한 코드 작성의 천재이거나, 노련한 개발자가 내 코드를 읽는 게 아니라면, 대부분의 개발자는 자신의 도메인에 컨텍스트가 맞춰져 있기 때문에(다른 업무 중에 내 코드를 함께 보기 때문에) 프로덕션 코드만으론 내 의도를 파악하는 데 많은 시간이 소모될 것이다. 이때 많은 도움을 줄 수 있는 것이 바로 테스트 코드다.

class SomeThings(
    private val values: List<SomeThing>
) {
    fun groupAndSortedBySomeThing(): List<SomeThingGroup> {
        val groupBySomeThing = values.groupBy { it.getSomeThing() }

        return groupBySomeThing.values
            .map {
                /**
                 * 쿵짝쿵짝 맵핑
                 */
            }
            .sortedWith(
                /**
                 * 쿵짝쿵짝 정렬
                 */
            )
    }
}
class SomeThingsTest : StringSpec({
    "SomeThing 기준에 따라 SomeThing을 분류하고 정렬해서 리스트로 반환한다." {
        /**
         * 쿵짝쿵짝 테스트
         */
    }
})

내부 구현이 복잡한 비즈니스 로직도 테스트 코드명을 통해 최소한 ‘이런 걸 하려 했구나~’ 라는 힌트를 제공해줄 수 있다. 알파벳으로 이루어진 코드를 아무리 말끔하게 작성한들, 한글로 작성된 설명을 이길 수 있을까? 결국 테스트 코드는 복잡한 비즈니스 로직을 검증하는 수단이자, 내 코드를 설명하는 문서가 된다.

image

앞서 이야기한 개발자 개개인에게 테스트 코드를 작성해야 하는 이유만 보았을 땐 ‘개발 시간을 2배로 늘려가면서까지 누릴만한 장점인가?’ 라는 생각에 고개를 갸우뚱했으나, 나 하나의 시간을 소비해서 팀원 모두의 시간을 아낄 수 있다고 생각하면 충분히 가치가 느껴진다. 팀의 규모가 크면 클수록 그 가치는 더 크게 늘어날 것이다.


🧪 테스트 코드는 유지보수 대상인가

흔히 테스트 코드에 대한 리뷰를 진행할 때 많이 주고받는 이야기가 있다.

  • 테스트 코드 그 자체로 코드 문서의 역할을 한다.
  • 테스트 코드도 리팩터링의 대상이다.

테스트 코드 그 자체가 코드 문서의 역할을 하는 것은 사실이지만, 몇백, 몇천, 몇만개에 달하는 테스트 코드를 자세히 읽기란 사실상 불가능하다. 테스트 프레임워크가 보여주는 수많은 통과 표시만 확인할 뿐…

image

게다가 테스트 코드만 리팩터링하는 일은 흔치 않다. (전체 테스트 구조가 불편해서 대규모 리팩터링 하는 경우가 아니라면) 대부분은 프로덕션 로직을 수정하면서, 바뀐 로직에 따라 테스트를 통과시키기 위해 리팩터링 하게 된다. 결국 ‘테스트 코드는 유지보수 대상인가’에 대한 나의 대답은 ‘아니’다.

단, ‘아니’가 ‘테스트 코드를 대충 짜고 넘어가도 된다.’는 아니다. ‘아니’기 때문에 처음 기능 개발이 이루어질 때 테스트 코드를 꼭 짜야 하며, 쉽고 간결하게 잘 짜야만 한다.

프로덕션 로직만 우선 배포를 진행하고 테스트 코드를 나중에 작성하는 것은 테스트 커버리지만 채우는 일에 불과하고, 테스트의 퀄리티도 비교적 낮을 수밖에 없다. 또한 테스트 코드를 쉽고 간결하게 짜지 못하면 대규모 리팩터링에서 큰 불편함을 겪게 된다. 결국 처음 프로덕션 로직을 개발할 때 테스트 코드에도 리소스를 투자해서 잘 짜는 것이 가장 좋은 방법이다.

테스트 코드는 유지보수 대상이 아니다. 그래서 더 잘 짜야 한다.


🧪 결국 내가 테스트 코드에 집착하는 이유

얼마 전 프로그래머의 뇌 책을 읽으면서 인상적인 대목이 있었다.

생소한 코드를 읽는 것은 왜 어려운가? 가장 결정적인 이유는 STM(Short Term Memory)의 용량에 제한이 있기 때문이다.

특정한 주제에 대해 LTM(Long Term Memory)에 더 많은 정보를 저장하고 있다면 입력된 정보들을 효율적으로 청크로 묶는 것이 수월해진다는 사실을 살펴봤다. 전문 체스 선수들은 LTM에 체스의 다양한 말의 위치에 대한 정보를 저장하고 있기 때문에 체스판에 대해 더 잘 기억할 수 있다.

유명한 디자인 패턴이나 팀 컨벤션을 지키는 것이 중요한 이유는 그것들이 동료 개발자들의 청크가 되어 시간을 아껴주기 때문이다. 나 하나가 디자인 패턴과 팀 컨벤션을 익히고 사용함으로써 팀원 모두의 시간을 아낄 수 있게 된다.

테스트 코드 또한 나 하나가 조금 더 공을 들임으로써 팀원 모두가 믿고 지나갈 수 있는, 하나의 청크가 된다. 결국 내가 테스트 코드에 집착하는 이유는 더 이타적인 코드를 완성하기 위함이다.


References

댓글남기기