우아한테크세미나 TDD, 리팩토링 - 자바지기 박재성

본 글은 우아한테크세미나를 정리한 글입니다.

목자

  1. 의식적인 연습이란?
  2. TDD, 리팩토링 적용 - 개인
  3. TDD, 리팩토링 적용 - 개인(주니어) -> 팀
  4. TDD, 리팩토링 적용 - 내가 리더
  5. 정리

본 영상은 TDD와 리팩토링같은 기술적인 사항뿐만 아니라 훌륭한 개발자가 되기위한 방법, 박재성님이 교육자로 일하시는 동안 고안하신 훌륭한 학습방법을 함께 설명합니다.

1. 의식적인 연습이란?

컴포트존에서 벗어나 의식적인 연습, 즉 본인이 자각하는 연습을 꾸준히하며 개발실력을 올리는 것이 중요합니다. 특정기간이 되면 학습없이, 연습없이 제품을 만들어 낼 수 있지만 이것을 넘어 발전하는 방법을 설명합니다. 이러한 과정을 통해야만 훌륭한 개발자가 될 수 있습니다.

의식적인 연습의 7가지 원칙(도서 - 1만 시간의 재발견)

  1. 효과적인 훈련 기법이 수립되어 있는 기술 연마
  2. 개인의 컴포트 존을 벗어난 지점에서 진행, 자신의 현재 능력을 살짝 넘어가는 작업을 지속적으로 시도
  3. 명확하고 구체적인 목표를 가지고 진행
  4. 신중하고 계회적이다. 즉 개인이 온전히 집중하고 ‘의식적’으로 행동할 것을 요구
  5. 피드백과 피드백에 따른 행동 변경을 수반
  6. 효과적인 심적 표상을 만들어내는 한편으로 심적 표상에 의존
  7. 기존에 습득한 기술의 특정 부분을 집중적으로 개선함으로써 발전시키고, 수정하는 과정을 수반

우아한테크에서는 의식적연습을 위해 아래와 같은 여러가지 제약사항들을 만들어 프로그래밍 과정을 진행합니다.

우아한테크 과정

1주차

2주차

  • 함수의 길이가 최대 15
  • else를 사용하지 마라

3주차

  • 함수는 길이가 최대 10
  • index depth 는 최대 1
  • 함수 인자는 최대 3개

공통

  • 개발전 기능을 markdown으로 명세
  • commit 주석 및 꾸준히 하며 피드백 받기

2. 의식적인 연습으로 TDD, 리팩토링 적용 - 개인

TDD와 리팩토링은 운동, 평생동안 연습하겠다는 마음가짐으로 시작합니다. 이를 위해 회사프로젝트보다는 사이드 프로젝트로 시작하여 꾸준히 연습하며 브래이크타임이 없이 연습하는것이 중요합니다. 회사프로젝트의 경우 중간에 우선순위0의 업무들이 주어져 TDD나 리팩토링을 못하는 순간들이 발생하는데 사이드 프로젝트를 통해 꾸준히 연습할 수 있는 환경을 만들어 놓는것입니다. 이 방법은 TDD,리팩토링이 아닌 다른요소들을 연습할때도 추천되는 방법이라고 생각합니다.

1단계, 단위테스트 연습

TDD에 대한 간단한 테스트 연습코드입니다. 자바 내장 API인 String API와 Collection API에 대한 테스트를 직관적으로 보여줍니다.

public class StringTest{
    @Test
    public void split() {
        String[] values = "1".split(",");
        assertThat(values).contains("1");
        values = "1,2".split(",");
        assertThat(values).containsExactly("1","2");
    }

    @Test
    public void substring() {
        String input = ("1,2");
        String result = input.substring(1, input.length() - 1);
        assertThat(result).isEqualTo("1,2")
    }
}

public class CollectionTest {
    @Test
    public void arrayList() {
        ArrayList<String> values = new ArrayList<>();
        values.add("first");
        values.add("second");
        assertThat(values.add("third")).isTrue();
        assertThat(values.size()).isEqualsTo(3);
        assertThat(values.get(0)).isEqualsTo("first");
        assertThat(values.contains("first")).isTrue();
        assertThat(values.remove(0)).isEqualTo("first");
        assertThat(values.size()).isEqualTo(2);
    }
}

위 연습의 효과는 다음과 같습니다.

  • 단위테스트 방법을 학습할 수 있다.
  • 단위테스트 도구(xUnit)의 사용법을 익힐 수 있다.
  • 사용하는 API에 대한 학습 효과가 있다.

내가 구현하는 함수에 단위 테스트를 적용을 시작하기 위해서는 Input과 Output이 명확한 클래스 메소드(보통 Util 성격의 메소드)부터 시작하면 됩니다.

2단계, TDD 연습

TDD를 간략히 설명하면 먼저 실패하는 테스트코드를 작성하고 패스, 이후 리펙토링이 진행됩니다.

tdd-cycle

항상 시작은 난의도가 낮은 문제부터 시작합니다. 이를 위해 임의의 테스트환경을 구성합니다. 아래는 문자열 덧셈 계산기(StringCalculator 클래스의 splitAndSum)로 쉼표 또는 콜론을 기준으로 분리한 숫자의 합을 반환하는 기능을 가지고 있습니다. null이나 빈문자열이면 0을 반환합니다.

아래의 테스트코드와 계산기에 대한 프로덕트코드를 가지고 리팩토링 시작을 시작해 봅니다. 여기서는 중관과정을 최대한 생략하고 최종 리펙토링 코드들을 보이는데 위의 원본영상을 보시면 변환되는 과정을 살펴볼 수 있습니다.

public class StringCalculatorTest {
    @Test
    public void null_또는_빈값() {
        assertThat(StringCalculator.splitAndSum(null)).isEqualsTo(0);
        assertThat(StringCalculator.splitAndSum("")).isEqualsTo(0);
    }

    @Test
    public void 값_하나() {
        assertThat(StringCalculator.splitAndSum("1")).isEqualsTo(1);
    }

    @Test
    public void 쉽표_구분자() {
        assertThat(StringCalculator.splitAndSum("1,2")).isEqualsTo(3);
    }

    @Test
    public void 쉼표_콜론_구분자() {
        assertThat(StringCalculator.splitAndSum("1,2:3")).isEqualsTo(6);
    }
}

여기서는 다음의 프로덕트코드에 대해 TDD를 연습합니다. 이 코드는 위 테스트를 전부 패스합니다.

public class StringCalculator {
    public static int splitAndSum(String text) {
        if (text == null || text.isEmpty()) {
            return 0;
        }
        String[] values = text.split(",:");
        return sum(values);
    }

    private static int sum(String[] values){
        int result = 0;
        for (String value : values) {
            result += Integer.parseInt(value);
        }
        return result;
    }
}

3단계 - 리팩토링 연습(메소드 분리)

리팩토링에서는 항상 정성적인 기준보다는 정량적인 기준으로 판단하고 연습해야합니다. 다음은 위 코드에서 한 메서드에 오직 한 단계의 들여쓰기(indent)사용하기와 else사용안하기를 만족하는 리팩토링 결과 코드입니다.

public class StringCalculator {
    public static int splitAndSum(String text) {
        if (text == null || text.isEmpty()) {
            return 0;
        }
        // 로컬변수 최소화
        return sum(toInts(text.split(",|:")));
    }

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

    private static int sum(int[] numbers) {
        int result = 0;
        for (int number : numbers) {
            result += number;
        }
        return result;
    }
}

한 메서드는 한가지 일만 하도록 리팩토링하기 위해 문자열을 숫자로 변환하는 부분과 합치는 부분을 메서드로 각각 분리합니다.

public class StringCalculator {
    public static int splitAndSum(String text) {
        if (test == null || test.isEmpty()) {
            return 0;
        }

        // 로컬변수 최소화
        return sum(toInts(text.split(",|:")));
    }

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

    private static int sum(int[] numbers){
        int result = 0;
        for (int number : numbers) {
            result += numbers;
        }
        return result;
    }
}

compose method패턴 적용하여 동등한 수준의 작업을 하는 여러 단계로 나눈다.

public class StringCalculator {
    public static int splitAndSum(String text){
        if(isBlank(text)) {
            return 0;
        }
        return sum(toInts(split(text)));
    }

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

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

    private static String[] split(String text) {
        return text.split(",|:");
    }

    private static int sum(int[] numbers){
        int result = 0;
        for (int number : numbers) {
            result += number;
        }
        return result;
    }
}

위 코드를 보면 호출되는 splitAndSum 함수만 public이고 나머지는 전부 private로 구현되는 것을 알 수 있습니다. 이것은 분석하는 입장에서 public메서드를 먼저 분석하면되서 가독석을 높여줍니다. 특이 public메서드에서 호출되는 private메서드 또한 네이밍을 통해 동작작업을 충분히 유추할 수 있는 좋은 코드 입니다.

indent나 else등의 규칙을 한번에 적용하기 보다 한 가지씩 리팩토링하는 것을 추천합니다.

여기서 다시한번 말하지만 리팩토링전 코드, 즉 컴포트존을 제거하고 리팩토링을 통해 좋은 코드를 작성할 수 있도록 노력합시다!!

리팩토링 연습(클래스 분리)

숫자 이외의 값 또는 음수가 입력되면 RuntimeException 발생하도록 적용합니다. 이를 위해 다름과 같은 테스트 코드가 추가됩니다.

public class StringCalculatorTest {
    ...  
    @Test(expected = RuntimeException.class)
    public void 음수값() {
        StringCalculator.splitAndSum("-1,2:3");
    }
}

메서드 toInt를 클래스로 분리, 메서드로 분리된 부분은 클래스로 분리될 요지가 있다고 생각할 수 있습니다.

  • 양수를 보장
  • 메서드당 한가지일을 수행
public class Positive {
    private int number;

    public Positive(String value) {
        this(Integer.parseInt(value));
    }

    public Positive(int number) {
        if (number < 0){
            throw new RuntimeException();
        }
        this.number = number;
    }

    public Positive add(Positive other) {
        return new Positive(this.number + other.number);
    }

    public int getNunber() {
        return number;
    }
}

클래스를 분리하는 원칙

일급컬렉션 (위 Positive는 primitive type을 wrapper한 class)으로 분리가 가능할 경우 클래스로 분리가 가능합니다. 여기서 일급컬렉션이란 하나의 컬렌션을 주된 맴버로 가지는 클래스입니다. 이때 클래스에 대한 리펙토링 기준으로 3개 이상의 인스턴스 변수를 가지지 않도록 연습할 수 있습니다.

모든 연습기준은 정략적 기준입니다! 정량적 평가는 혼자 연습해도 정확하게 평가가능하고 피드백이 가능합니다.

4단계 - 장난감 프로젝트 난이도 높이기

다음과 같은 사항이 만족되면 TDD와 리팩토링을 연습하기 좋습니다.

  • 게임과 같이 요구사항이 명확안 프로그램으로 연습
  • 의존관계(DB등)이 없는 프로젝트
  • 약간은 복잡한 로직이 있는 프로그램

5단계 - DB, WebUI등 의존성있는 코드 리팩토링 연습

한 단계 더 나아간 연습하기

  • 컴파일 에러를 최소화하면서 리팩토링하기
  • ATDD 기반으로 응용 어플리케이션 개발하기
  • 레거시 애플리케이션에 테스트 코드 추가해 리팩토링하기

구체적인 연습 목표 찾기

이러한 방법들은 박재성님이 만드신게 아니라 아래의 책들에서 명시하는 방법이라고 합니다.

객체지향 생활체조 원칙 The ThoughtWorks Anthology

  • 규칙 1 : 한 메서드에 오직 한 단계의 들여쓰기만 한다.
  • 규칙 2 : else 예약어를 쓰지 않는다.
  • 규칙 3: 모든 원시값과 문자열을 포장한다.
  • 규칙 4 : 한 줄에 점을 하나만 찍는다.
  • 규칙 5 : 줄여쓰지 않는다(축약 금지).
  • 규칙 6 : 모든 엔티티를 작게 유지한다.
  • 규칙 7 : 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
  • 규칙 8: 일급 콜렉션을 쓴다.
  • 규칙 9 : 게터/세터/프로퍼티를 쓰지 않는다.

메소드 인자 원칙 CleanCode

  • 메소드 인자 개수
  • 메소드에서 이상적인 인자개수는 0개이다. 다음은 1개이고, 다음은 2개이다.
  • 3개는 가능한 피하는 편이 좋다.
  • 4개 이상은 특별한 이유가 있어도 사용하면 안된다.

같은 과제를 반복적으로 연습하며 TDD, 리팩토링 적용하 연습하도록 합니다.

3. TDD, 리팩토링 적용 - 개인(주니어) -> 팀

  • 변화를 요구, 안되면 퇴사

4. TDD, 리팩토링 적용 - 리더

  • 1:1로 공약
  • 팀원이 개선할 부분을 말하고, 해결책을 제안하도록 유도

References