TDD와 리팩토링

임준영·2021년 4월 10일
2

TDD, 리팩토링

이화여대에서 Dev Festoval이 개최를 하여서 같은 개발직종에 종사하고 있는 대학교 친구랑 참가비 1만원을 내고 컨퍼런스에 참여하였습니다.

1. TDD, 리팩토리의 중요성

사실 많은 세션 중에서 이것을 듣기 위해 왔다고 해도 과언이 아닌 우아한 테스크코스 교육을 담당하고 계시고 TDD에 관한 책도 저술하신 박재성님의 의식적인 연습으로 TDD, 리팩토링 연습하기 세션을 들었습니다.

기대했던만큼 돈이 아깝지 않을정도로 훌륭한 퀄리티의 세션이였습니다.
제가 앉은 자리는 맨 뒤쪽이여서... 리팩토링을 적용할 코드가 보이지 않았지만 다행히 구글링을 통해서 오늘 세션에서 봤던 리팩토링 예제코드를 보고 분석할 수 있었습니다.

간단하게 리팩토링 예제를 리뷰해보겠습니다.

아래 코드는 어떤 특정 구분자를 가진 문자열 숫자들을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 return 하는 코드입니다.

박재성님은 웹, 모바일 UI나 DB에 의존관계를 가지지 않는 요구사항으로 연습을 해야하고, 회사 프로젝트에 연습하지 말고 장난감 프로젝트를 활용하라고 하셨습니다.

public class StringCalculator {

    public static void main(String[] args) {
        String text = "1,3,5,7,9";
    }

    public static int splitAndSum(String text){
        int result = 0;
        if (text == null || text.isEmpty()){
            return 0;
        } else{
            String[] values = text.split(","); 
            for (String value : values) {
                result += Integer.parseInt(value);
            }
        }
        return result;
    }
}

가장 먼저 위의 코드는 if문과 else문으로 구성이 되어있습니다. 리팩토링에서는 else 예약어를 쓰지 않는것을 지향하고 있습니다.
그리고 splitAndSum()에는 문자열이 비어있는지 검사하고 문자열을 구분자를 기준으로 분리하여 각 숫자의 합을 구하는 등 많은 일들을 하고 있는게 보이고 있습니다.

이 코드를 메소드가 한 가지 일만 하도록 아래와 같이 구현해보겠습니다.

메소드 분리로 리팩토링 적용 후 코드

public class StringCalculator {

    public static void main(String[] args) {
        String text = "1,3,5,7,9";
        System.out.println(splitAndSum(text));
    }

    // 아래 메소드가 한 가지 일만 하도록 구현하고 있습니다.
    public static int splitAndSum(String text){
        if(isBlank(text)){
            return 0;
        }

        return sum(toInt(text.split(",")));
    }

    public static int[] toInt(String[] values){

        int[] numbers = new int[values.length];

        for (int i = 0; i < values.length; i++) {
            numbers[i] = Integer.parseInt(values[i]);
        }
        
        return numbers;
    }

    public static int sum(int[] numbers){

        int sum = 0;

        for (int number : numbers){
            sum += number;
        }

        return sum;
    }

    public static boolean isBlank(String text){
        return text == null || text.isEmpty();
    }
}

메소드 분리를 통해 리팩토링된 코드를 보면 각 메소드가 하나의 일만 수행하는 것을 알 수가 있습니다.
이것은 compose method 패턴을 적용하여 메소드(함수)의 의도가 잘 드러나도록 동등한 수준의 작업을 하는 여러 단계로 나누었습니다.

확실히 리팩토링을 통해서 이전 코드랑 비교했을때 가독성이 높아진 것을 알 수가 있습니다.

개발자들이 개발을 잘하는것도 중요하지만 돌아가는 프로그램에 초점을 맞추기 보다는 유지보수성과 기능 확장을 위해서 유연한 코드를 작성하는게 중요하다고 다시 한번 이번 세션을 통해 느꼈습니다.

유연한 코드를 작성하가 위해서는 협업하는 사람들이 코드를 읽기 쉽게 작성하는 거랑 일맥상통하는데 이러한 능력을 갖추기 위해서는 한 번에 한 가지 명확하고 구체적인 목표를 가지고 리팩토링을 연습하는 습관을 기르는게 중요하다고 생각합니다.

2. TDD란?

TDD에서는 테스트 자동화를 통해서 개발이 시작된 시점부터 완료될 때까지 가능한 한 빠른 시점 내에 그리고 자주 실패를 경험하도록 유도합니다. TDD는 실패를 통해 배움을 늘려가는 기법입니다. OK 조건을 사전에 정해두고 빠르게 실패를 경험하며, 그 조건을 등대로 삼아 실패 상황을 최대한 빨리 극복하고자 노력합니다. 성공한 항목과 실패한 항목이 명확하고, 작업해야 하는 부분이 확실해집니다. 성공에 필요한 조건을 만들고, 실패하는 조건 항목을 성공시킵니다. 그래서 빨리 실패하면 실패할수록 좀 더 성공에 가까워지는 묘한 개발 방식입니다.

3. 리팩토링

  • 리팩토링을 수행하게 되는 정제 단계에서는 일반적으로 아래와 같은 질문에 대해 고민해야 합니다.
  • 소스의 가독성이 적절한가?
  • 중복된 코드는 없는가?
  • 이름이 잘못 부여된 메소드나 변수명은 없는가?
  • 구조의 개선이 필요한 부분은 없는가?

TDD의 긍정적인 부가효과 중 하나는 테스트 케이스를 수행할 때 Java 언어의 문법적 측면으로만 바라보지 않고, 좀 더 업무적으로 더 생각할 수도 있습니다. 예를 들어 계좌를 초기에 생성할 때 예치금 없이 계좌를 만들 수 있게 할 것인지 말 것인지와 같은 식으로 말입니다. 초반에 별 생각 없이 만들었던 테스트 케이스 자체가 계좌 클래스의 구조에 대해 조금 생각해보라고 개발자에게 말을 걸고 있습니다.

4. 오류와 실패

실패는 AssertEquals 등 테스트 조건식을 만족시키지 못했다는 것을 의미합니다. 또, 그로 인해 내부적으로 fail()이 호출됐다는 의미이기도 합니다.
오류는 테스트 케이스 수행 중 예상치 못한 예외가 발생해서 테스트 수행을 멈췄다는 것을 뜻합니다. 만일 작성한 메소드 내부에서 fail()대신 예외가 발생하였다면, 실패가 아니라 오류로 간주됩니다. 즉, 아래와 같은 코드는 오류가 됩니다.

@Test
public void testGetBalance() throw Exception{

    Account account = new Account(10000);
    if(account.getBalance() != 10000){
        throw new Exception();
    }
}

테스트 케이스가 가치를 지니기 위해서는, 어떠한 경우에도 테스트 케이스 그 자체는 정상적으로 끝까지 수행되어야 합니다. 그래서 단정문을 실행한 결과 실제값이 예상값과는 다르다는 신호인 실패가 나오도록 테스트 케이스를 작성해야 합니다. 일반적으로 오류는 작성자가 의도하지 않은 예상치 못한 실패(unexpected failure)를 뜻하며, 이 경우 테스트 케이스 자체에 문제가 있음을 시사합니다. 따라서 본인이 작성한 테스트 케이스오류로 인한 실패가 발생하고 있다면, 빠른 시일 내에 실패로 카운트 될 수 있게 만들어야 합니다.

부끄러운 테스트 케이스

일반적으로 생성자 메소드에서 특별한 업무로직을 처리하지 않는다면 굳이 테스트 케이스를 작성하지 않아도 무방합니다. 다만, 0원으로 계정 생성 금지라던가 마이너스 통장으로 개설 같은 식의 업무로직이 생성자 메소드와 관련 있다면 그때는 생성자에 대한 테스트 케이스도 만들어야 합니다. 어떻게 보면 모순처럼 들릴 수도 있겠지만, 생성자 메소드에 대한 테스트 케이스는 작성하는데 큰 노력이 들지 않기 때문입니다. 테스트 케이스의 부수적인 효과중 메소드 사용에 대한 설명서적인 측면에서 해당 클래스를 사용하게 될 다른 개발자들에게 도움이 되기 때문입니다.

5. TDD의 장점

  • 개발의 방향을 잃지 않게 해줍니다.

현재 자신이 어떤 기능을 개발하고 있고, 또 어디까지 와 있는지를 항상 살펴볼 수 있습니다. 그리고 남은 단계와 목표를 잊지 않게 도와줍니다. 어찌 생각하면 별 것 아니라고 여길 수 있지만, 굉장히 중요한 장점입니다. 개발이란 앉은 자리에서 일어날 때까지 처음부터 끝까지 한 번에 이뤄지는 일도 아니고, 자기 혼자만으로 진행되리라는 법도 없습니다. TDD를 진행할 때 만들어지는 테스트 케이스들은, 자신이 어디까지 왔고, 앞으로 나아가야 하는 곳이 어디인지를 알려주는 나침반이 됩니다. 그래서 일부 개발자는 개발도중 자리를 비우게 될 때, 작성하는 테스트 케이스를 일부로 실패하도록 만들어 놓기도 합니다. 다음에 자리로 돌아왔을 때 재시작 시점을 바로 알 수 있도록 말입니다.

  • 품질 높은 소프트웨어 모듈 보유

TDD를 통해 만들어진 애플리케이션은 필요한 만큼 테스트를 거친 품질이 검증된 부품을 갖게 되는 것과 마찬가지 입니다. 품질 좋은 부품이 꼭 품질 좋은 제품을 보장해주는 건 아니지만, 좋은 제품을 만드는데 있어 기본 조건임에는 틀림없기 때문입니다.
TDD를 사용하지 않은 개발팀에 비해 TDD를 적용한 팀의 결함률이 최대 1/10 정도까지 감소했습니다.

  • 자동화된 단위 테스트 케이스를 갖게 됩니다.

TDD의 부산물로 나오는 자동화된 단위 테스트 케이스들은, 개발자가 필요한 시점에 언제든지 수행해 볼 수 있습니다. 그리고 그 즉시 현재까지 작성된 시스템에 대한 이상 유무를 바로 확인할 수 있습니다. 또한 기능을 추가하든가, 수정하게 됐을 때 수행해야 하는 회귀 테스트에 대한 부담도 줄어듭니다.

회귀 테스트란 이미 개발과 테스트가 완료된 모듈에 수정을 가하게 될 경우, 기존에 동작하던 다른 부분 도 정상적으로 동작하는지 확인하기 위해 수행하는 테스트. 원칙적으로 기존 모듈에 수정이 가해질 때마다, 해당 모듈뿐 아니라 그 모듈과 연관되어 있는 다른 모든 모듈도 변함없이 목표대로 동작하는지를 매번 테스트해야 합니다.

  • 사용 설명서 & 의사소통의 수단

TDD로 작성된 각 모듈에는 테스트 케이스라고 하는 테스트 코드가 개발 종료와 함꼐 남게 됩니다. 비록 테스트 케이스 코드는 고객이 돈을 지불하는 코드에 해당하지 않지만, 품질을 고려한다면 놓쳐서는 안 되는 귀중한 코드들입니다. 그리고 그 테스트 코드들의 가치는 시간이 지나면서 두고두고 빛을 발합니다. 그 가치안에는 고객만을 위한 것이 아니라, 현재 자신과 주위의 개발자, 그리고 미래의 개발자에게 제공되는 상세화된 모듈 사용 설명서라는 부분도 포함되어 있습니다. TDD를 통해 작성된 테스트 케이스는 사용 설명서이자, 그와 동시에 다른 개발자와 소통하는 커뮤니케이션 통로가 됩니다.

  • 설계 개선

테스트 케이스 작성 시에는 클래스나 인터페이스, 접근제어자, 이름 짓기, 인자 등에 이르는 개발에 포함된 다양한 설계 요소들에 대해 미리 고민하게 됩니다. 흔히 테스트하기 어렵다고 생각되는 코드들은 객체 설계 원리 중 기본에 해당하는 원칙들이 잘못 적용됐거나 충분히 고려되지 않았을 가능성이 높습니다. TDD를 진행해나가면서, 테스트가 가능하도록 설계 구조를 고민하다 보면 자연스럽게 디자인으르 개선하게 됩니다. 즉 누가 모니터 옆에 손가락으로 개선해야 하는 부분을 가리켜주는 것이 아니라, 스스로의 사고 와 수련으로 발전해나갈 수 있게 도와줍니다.

  • 보다 자주 성공한다

기본적으로, TDD는 매 주기(cycle)를 짧게 설정하도록 권장합니다. 그렇게 하면 앞서 말한 녹색 막대를 자주 볼 수 있고, 그때마다 목표를 이뤘다는 성취감을 느낄 수 있습니다. 이런 성취감은 개발자에게 큰 힘이 되곤 합니다. 또한 이런 성공 습관은 개발의 기초를 바꿀 수 있게 도와줍니다. 기존 개발 가이드 담론의 최고 덕목 중 하나가 분할하여 정복(Divde & Conquer)이었다면, 앞으로 분할하여 테스트 후 정복을 문제 해결의 기본 원칙으로 삼아야 합니다.

엉클 밥의 TDD 원칙
1. 실패하는 테스트를 작성하기 전에는 절대로 제품 코드를 작성하지 않는다.
2. 실패하는 테스트 코드를 한 번에 하나 이상 작성하지 않는다.
3. 현재 실패하고 있는 테스트를 통과하기에 충분한 정도를 넘어서는 제품 코드를 작성하지 않는다.

로버트 마틴은 이 세가지 TDD 개발의 주기를 30초에서 1분 사이로 유지시켜 준다고 말합니다. TDD의 주기가 짧아진다는 건 그만큼 리듬을 타고 빠르게 진행할 수 있도록 만들어준다는 의미이기도 합니다. 위 원칙은 TDD를 시작하는 사람들에게 TDD가 습관이며 개발의 한 부분으로 자리잡을 수 있도록 도와주는 방법입니다. 적극적으로 실천합시다!

0개의 댓글