스터디/Tech

[TDD 리팩토링 세미나]우아한 테크 세미나 TDD와 리팩토링

_leezoee_ 2022. 5. 15. 23:17

유튜브를 통해 TDD의 중요성을 설명하는 강의를 보았다.

TDD란 테스트 코드를 먼저 작성하는 개발 방법론으로, 테스트 주도 개발(Test-Driven Development, TDD)의 약자이다.

 

TDD가 무엇인지, 곧 회사에서 새 프로젝트를 만들어야하는데 어떻게 하면 좋은 프로젝트 설계가 될 지 고민하다

우아한 테크 세미나 중 TDD와 리펙토링을 설명해주는 세미나를 보게 되었다.

 

좋은 내용이니까 공유

https://www.youtube.com/watch?v=bIeqAlmNRrA&t=2109s 

 

 


세미나의 박재성님은 5-6년 동안 TDD를 연습하셨다고한다.

쉽지않다고 하니 어떻게 해야 효과적으로 연습할 수 있는지 소개.

 

목적의식이 있는 연습에 얼마나 많은 시간을 투자 했는가?

1. 효과적인 훈련 기법이 수립되어있는 기술 연마

2. 개인의 컴포트 존을 살짝 벗어난 지점에서 진행, 자신의 현재 능력을 살짝 넘어가는 작업을 지속적으로 시도

3. 명확하고 구체적인 목표를 가지고 진행

4. 개인이 온전히 집중하고 의식적으로 행동

5. 피드백과 피드백에 따른 행동 변경을 수반

6. 기존에 습득한 기술의 특정부분을 집중적으로 개선함으로써 발전시키고, 수정해 나가는 과정을 수반

 

의식적인 연습에 대한 예시(ex.우테코 프리코스)

ex_1 ) 3주 동안 매주 해결해야할 미션을 부여

ex_2 ) 미션을 완료한 후 github에 제출

ex_3 ) 공통 피드백

 

1주차 - 프로그래밍 제약사항

- 자바 코드 컨벤션을 지키면서 프로그래밍한다.

- indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.

- 함수(또는 메소드)가 한 가지 일만 하도록 최대한 작게 만들어라.

 

2주차 - 프로그래밍 제약사항

- 자바 코드 컨벤션을 지키면서 프로그래밍한다.

- indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.

- 함수(또는 메소드)가 한 가지 일만 하도록 최대한 작게 만들어라.

+ 함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다.

+ else 예약어를 쓰지 않는다.

   (if 조건절에서 값을 return 하는 방식으로 구현, switch/case도 당연히 허용하지 않는다)

 

3주차 - 프로그래밍 제약사항

- 함수(또는 메소드)의 길이가 10라인을 넘어가지 않도록 구현한다.

- indent(인덴트, 들여쓰기) depth를 2이 넘지 않도록 구현한다. 1까지만 허용한다.

 

기능 구현 전에 마크다운(readme)에 구현할 기능 목록을 먼저 리스트업 한다.

기능 목록에 맞게 커밋 내용을 정리한다.

 


평생 연습하겠다는 마음가짐으로 시작.

 

시작하기 - 회사 프로젝트가 아닌 토이프로젝트로 시작

=> 주변 환경에 영향을 받지 않고 꾸준히 연습하기 위함

 

1단계 - 단위테스트 연습

- 내가 사용하는 api 사용법을 익히기 위한 학습 테스트에서 시작

   ex) 자바 String 클래스의 다양한 메소드 사용법

   ex) 자바 ArrayList에 데이터를 추가, 수정, 삭제 하는 방법

=> 연습 효과 : 단위 테스트 방법을 학습할 수 있다. 단위테스트 도구(xUnit)의 사용법을 익힐 수 있다. 

사용하는 api 에 대한 학습효과가 있따

 

- 현업에서 활용해 볼 수 있는 방법

=> 내가 구현하는 메소드 중 input과 output이 명확한 클래스 메소드(Util 성격의 static 메소드들?) 에 대한 단위 테스트를 연습 해보면 좋다. 

=> 알고리즘을 학습한다면 알고리즘 구현에 대한 검증을 단위테스트로 진행해봐도 좋다.

//java string split, substring 에 대한 단위테스트코드 작성예시
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;

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");
    }
}​
//java arrayList 에 대한 단위테스트 코드 예시
import org.junit.Test;
import java.util.ArrayList;
import static org.assertj.core.api.Assertions.asserThat;

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

 

2단계 - TDD연습

 

난이도가 낮고, 자신에게 익숙한 문제로 시작하는 것을 추천.

웹, 모바일 UI나 DB에 의존관계를 가지지 않는 요구사항으로 연습!!

 

[TDD cycle]

TDD cycle 순서

1. 실패하는 단위 테스트 추가

2. 테스트에 성공하는 프로덕션 코드 구현

3. 리펙토링 *****중요한 과정*****

4.반복

 

 

ex)문자열 덧셈 계산기 요구사항

: 쉼표(,) 또는 클론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환.

input output
null 또는 "" 0
"1" 1
"1,2" 3
"1,2:3" 6

 

//테스트코드작성
public class StringCalculatorTest(){
	@Test
    public void null_또는_빈값(){
    	assertThat(StringCalculator.splitAndSum(null)).isEqualTo(0);
        assertThat(StringCalculator.splitAndSum("")).isEqualTo(0);
    }
    @Test
    public void 값_하나(){
    	assertThat(StringCalculator.splitAndSum("1")).isEqualTo(1);
    }
    @Test
    public void 쉽표_구분자(){
    	assertThat(StringCalculator.splitAndSum("1,2")).isEqualTo(3);
    }
    @Test
    public void 쉽표_콜론_구분자(){
    	assertThat(StringCalculator.splitAndSum("1,2:3")).isEqualTo(6);
    }
}
//프로덕션코드, 리펙토링 전
public class StringCalculator(){
	public static int splitAndSum(String text){
    	int result = 0;
        if(text == null || text.isEmpty()){
        	result = 0;
        } else {
        	String[] values = text.split(",|:");
            for(String value : values){
            	result += Integer.parseInt(value);
            }
        }
        return result;
    }
}

 

3단계 - 1) 메소드 분리 리펙토링

테스트 코드는 변경X

테스트 대상인 프로덕션 코드를 개선하는 연습에 집중 (의식적인 연습!!!!)

 

ex)1차 리펙토링

//프로덕션코드, 리펙토링 전
/*
1. else를 사용하지 않는다
2. indent 최대2까지 허용.
=>
1. 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다. 즉 들여쓰기가 길어지면 메소드를 분리한다
2. else 쓰지말고 if 안에서 리턴.
*/
public class StringCalculator(){
	public static int splitAndSum(String text){
    	int result = 0;
        if(text == null || text.isEmpty()){
        	result = 0;
        } else {//else 빼!!!!
        	String[] values = text.split(",|:");
            for(String value : values){//들여쓰기 2이상!!!
            	result += Integer.parseInt(value);
            }
        }
        return result;
    }
}
//프로덕션코드, 리펙토링 후
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;
    }
}

ex)2차 리펙토링

//메소드가 한 가지 일만 하도록 구현하기
    
    private static int sum(String[] values){
    //String으로 된 values들을 int로 바꾸고, 바꾼 int 값들을 모두 더함. 두 가지 일 하는 중. 분리!!
    	int result = 0;
        for(String value : values){
        	result += Integer.parseInt(value);
        }
        return result;
    }
    
//리펙토링 후
public class StringCalculator {
	public static int splitAndSum(String text){
    	...
    }
    
    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;
    }
}

ex) 3차 리펙토링

public class StringCalculator{
	public static int splitAndSum(String text){
    	if(text == null || text.isEmpty()){
        	return 0;
        }
        String[] values = text.split(",|:"); //로컬변수가 필요할까??????????
        int[] numbers = toInts(values);
        return sum(numbers);
    }
    
    private static int[] toInts(String[] values){
    	...
    }
    
    private static int sum(int[] numbers){
    	...
    }
}

//리펙토링 후
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){
    	...
    }
    
    private static int sum(int[] numbers){
    	...
    }
}

ex)4차 리펙토링

compose method 패턴 적용 : 함수의 의도가 잘 드러나도록 동등한 수준의 작업을 하는 여러단계로 나눔.

=> 추상화 레벨을 맞추기.

* OOP 개념 도입 객체지향프로그래밍은 추후에 포스팅예정.

public class StringCalculator{
	public static int splitAndSum(String text){
    	if(text == null || text.isEmpty()){//여긴 추상화 안되어있음. compose method 패턴을 적용해보자.
        	return 0;
        }
       return sum(toInts(text.split(",|:")));//toInts와 sum으로 1차 추상화가 완료.
    }
    
    private static int[] toInts(String[] values){
    	...
    }
    
    private static int sum(int[] numbers){
    	...
    }
}

//리팩토링 후

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 String[] split(String text){
    	return text.split(",|:");
    }
    
    private static int[] toInts(String[] values){
    	...
    }
    
    private static int sum(int[] numbers){
    	...
    }
}

 

* 한번에 모든 원칙을 지키면서 리팩토링 하려고 연습하지 말기.

* 한번에 한 가지 명확하고 구체적인 목표를 가지고 연습!

* 연습은 극단적인 방법으로 연습하는 것도 좋음. (ex. 한 메소드 라인 수 제한을 15 -> 10으로 줄이기)

 

3단계 - 2) 클래스 분리 리펙토링

위에 문자열 덧셈 계산기에 하나 추가

쉼표(,) 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환

+ 숫자 이외의 값 또는 음수를 전달하는 경우 RuntimeException 예외를 throw 한다

input output
null 또는 "" 0
"1" 1
"1,2" 3
"1,2:3" 6
"-1,2:3" RuntimeException
//테스트코드추가
public class StringCalculatorTest(){
    @Test
    public void null_또는_빈값(){
    	assertThat(StringCalculator.splitAndSum(null)).isEqualTo(0);
        assertThat(StringCalculator.splitAndSum("")).isEqualTo(0);
    }
    @Test
    public void 값_하나(){
    	assertThat(StringCalculator.splitAndSum("1")).isEqualTo(1);
    }
    @Test
    public void 쉽표_구분자(){
    	assertThat(StringCalculator.splitAndSum("1,2")).isEqualTo(3);
    }
    @Test
    public void 쉽표_콜론_구분자(){
    	assertThat(StringCalculator.splitAndSum("1,2:3")).isEqualTo(6);
    }
    
    @Test(expected = RuntimeException.class)
    public void 음수값(){
    	StringCalculator.splitAndSum("-1,2:3");
    }
}

 

* 모든 원시값과 문자열을 포장한다.

* 클래스 분리 연습을 위해 활용할 수 있는 원칙 => 일급 콜렉션을 쓴다, 3개 이상의 인스턴스 변소를 가진 클래스를 쓰지 않는다.

 

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

점진적으로 요구사항이 복잡한 프로그램을 구현.

앞서 지킨 기준을 지키면서 프로그래밍 연습을 한다.

 

TDD.리팩토링 연습하기 좋은 프로그램 요구사항

1. 게임같이 요구사항이 명확한 프로그램으로 연습

2. 의존관계(모바일 UI, 웹 UI, 데이터베이스, 외부 API 같은 의존관계)가 없이 연습

3. 약간은 복잡한 로직이 있는 프로그램

 

 

5단계 - 의존관계 추가를 통한 난이도 높이기

웹 UI, 모바일 UI, 데이터베이스와 같은 의존관계를 추가

이때 필요한 역량은 => 테스트하기 쉬운 코드와 어려운 코드를 보는 눈, 테스트하기 어려운 코드를 테스트하기 쉬운 코드로 설계하는 센스.

 

 

한단계 나아가기

- 컴파일 에러를 최소화하면서 리팩토링하기

- ATDD 기반으로 응용 애플리케이션 개발하기

- 레거시 애플리케이션에 테스트 코드 추가해 리팩토링하기

 

구체적인 연습 목표 찾기

ex)  객체지향 생활체조 원칙 책 (소트웍스 앤솔러지)

 rule 1. 한 메서드에 오직 한 단계의 들여쓰기만 한다.

 rule 2. else 예약어를 쓰지 않는다.

 rule 3. 모든 원시값과 문자열을 포장한다.

 rule 4. 한 줄에 점 하나만 찍는다.

 rule 5. 줄여쓰지 않는다 (축약금지)

 rule 6. 모든 엔티티를 작게 유지한다.

 rule 7. 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.

 rule 8. 일급 콜렉션을 쓴다.

 rule 9. 게터/세터/프로퍼티를 쓰지 않는다.

 

ex) 클린코드

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

 

 


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

 

내가 맡은 기능 구현에 TDD.리팩토링 적용, 묵묵히 혼자 진행(ㅋㅋㅋㅋㅋ)

관심있는 사람이 생기면 전파한다

내가 구현한 코드 또는 동료의 관심에서 작은 성공을 맛본다(ㅋㅋㅋㅋ)

계속 묵묵히 해 내가고, 될때까지 전파하면서 본인 역량을 쌓아가자

 

TDD.리팩토링 적용 - 리더

1:1 공략

팀원이 개선할 부분을 말하고, 해결책을 제안하도록 유도