본문 바로가기

TDD

TDD로 추상화 로직 설계하기

"TDD로 추상화 로직을 어떻게 설계해야 할까?" 2010년에 [테스트 주도 개발]을 읽고 TDD를 처음 알게 됐지만, 이 질문에 대한 답은 최근에서야 정리할 수 있었다. (그동안 꾸준히 TDD를 사용하지는 않았다. TDD에 대한 믿음과 의심을 반복하며 시간을 보냈다)
 

용어 정리

해당 포스트에서는 아래와 같은 용어를 사용한다.
 

  • 구현 전 설계: 일반적인 '사전 설계'를 의미한다. 코드를 구현하기 전에 코드 구조를 설계하는 것을 의미한다
  • 구현 후 설계: 테스트 코드와 구현 코드를 작성한 후, '리팩토링'으로 코드 구조를 설계하는 것을 의미한다

 

예시 문제

A 시스템과 B 시스템의 데이터를 조회해서 비즈니스 로직을 처리해야 하는 이슈가 있다고 하자.
 
A 시스템은 웹 소켓으로 데이터를 조회하고, B 시스템은 주기적으로 HTTP API를 호출해서 데이터를 조회한다고 가정하자. 구현 요구사항(또는 희망 사항)으로는 통신 모듈을 쉽게 사용하기 위해서, A 시스템과 B 시스템의 통신 인터페이스를 통일하고, 파라미터를 통해 통신할 시스템을 동적으로 선택한다고 가정하자.
 
이 경우에 TDD로 시스템을 어떻게 설계하는 것이 좋을까?
 

해결책

A 시스템 통신 로직을 먼저 TDD로 구현하고, B 시스템 통신 로직을 나중에 TDD로 구현한다. A 시스템을 구현하면서 얻은 인터페이스와 구현 코드를 B 시스템 구현에 적용하는 것이 중요하다. 최대한 인터페이스를 중복시키고, 가능하다면 구현 코드를 중복시킨다. 그다음 코드에서 중복된 부분을 찾아 공통 인터페이스나 공통 구현 코드로 뽑아낸다.
 
구현 순서는 의미가 있을 수도 있고 없을 수도 있다. 나는 보통 둘 중 복잡해 보이는 로직을 먼저 구현하고, 단순해 보이는 로직을 나중에 구현하는 편이다. 복잡해 보이는 로직은 인터페이스도 상대적으로 복잡해질 가능성이 높다. 두 인터페이스를 서서히 닮아가게 만들려면 복잡해 보이는 로직을 먼저 구현하는 게 더 편한 거 같다.
 
이런 작업 방식은 아래와 같은 장점이 있다.
 

장점

과도한 구현 전 설계를 방지한다

구현 전 설계를 하다 보면 자기 꾀에 자기가 빠지는 경우가 있다. 온갖 걱정들에 대한 해법을 설계에 반영하다 보면, 처음 예상했던 것보다 설계가 복잡해질 가능성이 커진다. 

TDD로 진행하는 개인 프로젝트에서 2, 3일 치 작업을 롤백한 경험이 있다. 예시 문제에 나온 로직을 구현할 때의 경험이다. 처음에 작업할 때는 구현 전 설계에 많은 시간을 사용했다. 구현 전 설계를 통해서 클래스 구조를 정하고, 정해진 구조를 따라서 구현했었다. 이렇게 나온 구현 코드는 뭔가 복잡해 보였다.

 
어느 날 "일부러 중복코드를 만들고, 눈에 보이는 중복코드를 하나씩 제거하면 어떨까?"라는 생각이 떠올랐다. 
 

사실을 기반으로 추상화 로직을 설계한다

중복 코드의 장점은 어느 부분이 중복인지 확실히 알 수 있다는 점이다. A 시스템 통신 로직과 B 시스템 통신 로직을 각각 구현하면, 같은 부분과 다른 부분을 쉽게 찾을 수 있다. 인터페이스나 구현 코드가 완전히 동일하거나, 부분적으로 동일할 수 있다. 이렇게 발견한 부분에 추상화 기법을 적용하면 공통 구현으로 뽑아낼 수 있다.
 
구현 전 설계는 '추측'을 기반으로 설계한다. 경험이 쌓일수록 추측이 맞는 경우가 많겠지만, 틀리는 경우도 많다. 따라서 결과가 불확실할 수 있다. 반면, 구현 후 설계는 '사실'을 기반으로 한다. 구현 코드를 보고 실제로 중복된 부분을 찾는다. 그렇게 발견한 중복은 한 번에 하나씩 제거할 수 있다.
 
구현 전 설계를 아예 안 할 수는 없지만, 많은 시간을 투자할 필요는 없다. 추측으로 만든 해결책은 사실과 다를 가능성이 있기 때문이다.
 

적정 아키텍처(설계)로 관리 비용이 절감한다

구현하다 보면 구현하기 전에 쉽게 알지 못했던 정보를 알게 된다. 이런 정보를 시스템 설계에 반영하면, 현재 요구사항에 꼭 맞는 설계를 만들 수 있다. 디자인패턴을 적용할 수도 있고, 더 단순한 방식으로 해결할 수도 있다. 핵심은 구현하며 알게 되는 문제에 맞춤형으로 해결책을 적용할 수 있다는 것이다.
 
미래에 요구사항이 변경된다면 어떻게 해야 할까? 그건 그때 가서 대응하면 된다. 작동하는 테스트 코드가 있고 리팩토링을 할 수 있다면, 미래의 변경을 미리 고민할 필요가 없다. (그리고 그런 변경은 실제로 발생하지 않을 수도 있다) 보통 추상화와 복잡도는 비례하고, 복잡한 설계는 관리 비용이 증가한다. 미래에 변경 가능한 모든 상황을 추측해서 미리 대비하는 일은 가성비가 떨어진다.
 
구현 전 설계는 '작업을 시작하는 방향을 제시한다'는 점에 의미가 있다. 그 이후의 설계는 구현하면서 알게 되는 정보를 활용하자. 현재 요구사항에 필요한 만큼의 설계를 적용하고, 미래의 요구사항은 테스트 코드와 리팩토링으로 대응하자.

심리적 안정감이 생긴다

추상화된 로직을 뽑아내지 못한다면 어떻게 될까? 최소한 작동하는 코드는 가질 수 있다. 최악의 경우는 정해진 일정 내에 기능 구현을 끝내지 못하는 상황이다. 작동하는 코드가 있다면, 그런 상황은 피할 수 있어서 마음에 여유가 생긴다. 심리적 안정감이 생기면 설계를 위해 더 많은 실험을 할 수 있다.
 

마무리

다른 포스트 [TDD 예제(점진적으로 설계하기)]에서 이야기한 것처럼, 프로그래밍은 글쓰기와 비슷하다고 생각한다. 여기서 글쓰기는 소설이나 시 같은 '문학적 글쓰기'가 아닌, 신문 기사나 블로그 포스트 같은 '실용적 글쓰기'를 의미한다.
 
나는 소제목이 적당히 정리되면 일단 초고를 작성하고, 여러 번 고쳐쓰기를 한다. 이렇게 작성한 글은 리뷰를 통해 피드백을 적용하고, 다시 쓰기를 반복한다. 단락의 위치를 조정하거나 내용을 추가, 삭제, 수정하는 등의 작업을 한다. 글의 순서와 내용을 머릿속으로 상상해서 정리하는 것보다, 눈으로 확인하고 정리하는 것이 더 쉽다.
 
TDD로 추상화된 로직을 설계할 때, '구현 후 설계'를 최대한 사용해 보자. 구현하기 전에 문제라고 생각했던 부분이 실제로는 문제가 아닐 가능성이 있고, 구현하면서 알게 되는 정보를 설계에 반영할 수 있다.