상세 컨텐츠

본문 제목

[TDD] TDD로 SoundEX개발하기 첫번째

IT/프로그래밍

by James Lee. 2015. 12. 1. 12:53

본문

SoundEx 개발, Git로 버전관리


TDD의 단계

  1. 실패하는 테스트 코드 작성 (말그대로 실패하는 코드)
  2. 실패하는 테스트 코드를 가장 간단하게 통과 (간단하지만 수단을 가리지 않고 통과되도록)
  3. Refactoring(코드구조개선)으로 설계 개선
  4. Intergration(통합, Git에서 commit을 의미, remote 저장소가 있는 경우에는 git push 까지를 의미함) (git에서 관련 폴더들 add후 commit, 편집기 창에 어떤 작업을 했는지 표시, 이후 git log를 통하여 확인할 수 있음)

Eclipse 에서 SoundEx Java Project 를 생성하기

  1. Eclipse 에서 단축키(Alt+Shift+N)를 눌러 Project 생성 창을 띄운다.
  2. 프로젝트 목록 창에서 Java Project 를 선택한다.
  3. New Java Project 창에서 Project name  에 "SoundEx"  를 입력한 후에, Finish 버튼을 클릭한다.

SoundEx 프로젝트에서 테스트 소스 폴더 생성하기

  1. SoundEx 프로젝트를 선택한다.
  2. New  단축키 (Ctrl+N) 를 클릭해서 생성 Wizard 를 띄운다.
  3. type filter text 에 "Source Folder" 를 입력한다.
  4. Source Folder 가 선택되면, Next 버튼을 클릭한다. ( 엔터를 치면 된다. )
  5. New Source Folder 창에서 Folder name 을 "test" 로 입력한다.
  6. Finish 버튼을 클릭한다. ( 엔터를 치면 된다. )


기존에 생성되어 있는 Git Repository 제거

  1. 기존에 있는 Git Repository 디렉토리로 이동
  2. Git Repository에 관한 정보를 .git에서 관리하기 때문에 .git 파일을 제거하면 된다.
  3. 제거 방법은 명령어 창에 rm .git 혹은 mv .git git을 입력한다.

생성된 SoundEx 프로젝트에 Git Repository 생성하기

  1. Git Bash 실행
  2. SoundEx가 있는 디렉토리로 이동 (이 컴퓨터의 경우는 cd /c/Users/anyone/work/SoundEx/)
  3. 이동한 SoundEx 폴더에서 git 디렉토리 생성 초기화 명령어인 git init을 입력
  4. SoundEx 폴더에 git repository가 생성되었다.



문제의 요구사항

  • 각 알파벳은 다음과 같이 치환한다.

    • b, f, p, v → 1
    • c, g, j, k, q, s, x, z → 2
    • d, t → 3
    • l → 4
    • m, n → 5
    • r → 6
  • 모음(a, e, i, o, u)와 h, w는 생략한다.

  • 중복된 값이 오면 제거한다. 단, 조건이 있다.

    • 값 사이에 모음이 오는 경우, 앞 자리와 뒷 자리를 비교하지 않고 넘어간다. ex) 1a1 -> 11
    • 값 사이에 h, w가 오는 경우, 앞 자리와 뒷 자리를 비교하여 중복이면 제거한다. ex) 1h1 -> 1

첫번째 TDD Cycle


테스트 클래스 만들기

  • 테스트 클래스 이름을 어떻게 지을 것인가?
    • SoundExTest, TestSoundEx ?? 어느 것이 좋을까?
    • SoundExTest 가 좋은 경우?
    • TestSoundEx 가 좋은 경우?
  • 여기서는 SoundExTest 로 짓는다.


  1. New ( Alt+Shift+N) 을 클릭해서 New Wizard 창을 띄운다.
  2. JUnit Test Case 를 선택하고, Next 버튼을 클릭한다. ( 엔터를 쳐도 된다.. )
  3. Test Class 이름으로 "SoundExTest" 를 입력한다.


public class SoundExTest {
 @Test
 public void failTest()
 {
  fail("...");
 }
}


실패하는 테스트 케이스 만들기

public class SoundExTest {
 @Test
 public void singleLetterName()
 {
  assertEquals("A000", se.encodedName("A"));
 }
}


Caller Create 방식으로 local variable se 를 선언한다.

  1. 오류가 발생하는 라인으로 이동한다.
  2. Quick Fix 키 (Ctrl+1) 을 클릭한다.
  3. 맨 첫번째... ."Create Local variable se" 를 선택한다.
  4. Tab  키를 이용하셔 "Object" 로 이동한다.
  5. Object 를 SoundEx 로 변경한다.
  6. Esc 키를 눌러서, Quick Fix 변경 모드에서 빠져나온다.


public class SoundExTest {
 @Test
 public void singleLetterName()
 {
  Object se;
  assertEquals("A000", se.encodedName("A"));
 }
}


SoundEx 의 객체를 생성한다.


public class SoundExTest {
 @Test
 public void singleLetterName()
 {
  SoundEx se = new SoundEx();
  assertEquals("A000", se.encodedName("A"));
 }
}


SoundEx 클래스 만들기 ( Production Code 만들기 )

SoundEx 클래스를 아짓 생성하지 않았기 때문에, 컴파일 오류가 발생한다.

단기간에 컴파일 오류를 제거하기 위해서, Caller Create 방식으로 Production Code ( SoundEx ) 를 생성한다.


  1. 컴파일 오류가 발생하는 라인으로 이동한다.
  2. Quick Fix (Ctrl+1) 을 클릭한다.
  3. Creat a new class "SoundEx" 를 선택한다. 
     
public class SoundEx {
}


SouneEx 의 메소드 만들기

다시 SoundExTest 클래스로 이동한다.


  1. 컴파일 오류가 발생하는 라인으로 이동한다.
  2. Create a new method "encodedName" 을 선택해서, SoundEx 의 메소드를 생성한다.
  3. encodedName 의 Signature 중에서 return 타입을 Object 에서 String 으로 변환한다.


public class SoundEx {
 public String encodedName(String string) {  
  return null;
 }
}


첫번째 테스트 코드 실행하기

  1. encodedName의 반환값 null과 "A000"이 일치하지 않는다.
  2. 테스트코드는 실패한다.



public class SoundEx {
 public String encodedName(String string) {  
  return null;
 }
}



public class SoundExTest {
 @Test
 public void singleLetterName()
 {
  SoundEx se = new SoundEx();
  assertEquals("A000", se.encodedName("A"));
 }
}


 

 


첫번째 테스트 코드 통과시키기

  1. encodedName의 반환값을 "A000"으로 설정
  2. encodedName의 반환값인 "A000"과 비교값인 "A000"이 일치하기 때문에 테스트 코드는 통과된다.


public class SoundEx {
 public String encodedName(String string) {  
  return "A000";
 }
}



public class SoundExTest {
 @Test
 public void singleLetterName()
 {
  SoundEx se = new SoundEx();
  assertEquals("A000", se.encodedName("A"));
 }
}


 


첫번째 코드 리팩토링

Production Code 에 리팩토링할 부분이 있는가?

Test Code 에 Refactoring 할 부분이 있는가?

첫번째 사이클에서는 리팩토링 할 부분이 존재하지 않았다.

코드 커밋하기

  1. git 명령어 창에 git status를 입력하여 tracked된 파일과 untracked된 파일을 확인
  2. untracked에 bin, src, test 세가지 폴더가 있다.
  3. git add src 명령어로 src 폴더를 tracked 설정
  4. git add test 명령어로 test 폴더를 tracked 설정 (bin폴더는 이클립스 설정 관련 파일이기 때문에 tracked할 필요가 없다.)
  5. git status 명령어를 입력해 보면 src, test폴더가 커밋 준비가 된 staged 상태인 것을 알 수 있다
  6. git commit 명령어를 입력하여 커밋한다.
  7. 편집기 창이 나온다
  8. 노란색으로 된 글씨 commitTestToLeanGit처럼 해당 커밋이 어떤 의미인지를 기술한다
  9. 저장하고 나갈때는 esc키를 누른후 :wq를 입력한다.

두번째 TDD Cycle


TDD 사이클은 항상 실패하는 테스트 케이스를 도출하는 데서 출발한다.

현재 상태에서 실패하는 테스트 케이스 중 가장 간단한 것은 무엇일까?

아마도 이름이 한자 짜리이면서 A 가 아닌 B 일 것이다.

실패하는 테스트 케이스 만들기

이름이 "B" 인 경우를 테스트하는 assert 문을 하나 추가한다.


public class SoundExTest {
 @Test
 public void singleLetterName()
 {
  SoundEx se = new SoundEx();
  assertEquals("A000", se.encodedName("A"));
  assertEquals("B000", se.encodedName("B"));
 }
}


테스트를 실행하면, singleLetterName 테스트가  assertEquals("B000", se.encodedName("B")); 라인에서 실패하는 것을 볼 수 있다.


테스트 통과 시키기

추가된 assert 문을 통과시키기 위해서 production code 를 다음과 같이 수정한다.

public class SoundEx {
 public String encodedName(String string) {
  return string + "000";
 }
}


테스트 케이스를 실행시킨다. ( 단축키 : Ctrl+F11 )


리팩토링 하기

프로덕션 코드 구조를 개선할 점이 있는가?

테스트 코드 구조를 개선할 점이 있는가?

자신의 관점.. 어떤 것이 좋은 코드인가에 대한 관점(더 일반적인 솔루션, 확장성, 가독성)

어떤 코드가 좋은 코드인가?

어떤 설계가 좋은 설계인가?

  • 읽기 쉬운가?
  • 이해하기 쉬운가?
  • 변경하기 쉬운가?
  • 더 짧은 코드는 없는가?
  • 변수 이름은 의도를 정확히 반영하고 있는가? ( 나는 왜 그 이름을 사용했는가? )
  • 변수 이름은 비지니스 도메인의 용어인가? 구현 용어인가?
  • 함수 이름은 의도를 정확히 반영하고 있는가?
  • 함수 이름은 비즈니스 도메인 용어를 사용하고 있는가? 구현 용어를 사용하고 있는가?
  • 각각의 기능이 독립적인가? ( SRP: Single Responsibility Principle 을 위반하고 있지 않은가? )
  • 코드에서 냄새가 나지 않는가? ( Code Smell 27 가지에 해당되는 부분이 있는가? )


기존에 매개변수로 사용한 string이라는 변수명은 의미가 모호하다. ( 변수 이름만으로는 string 에 어떤 값이 들어올지 전혀 예측할 수 없다. string 에 이메일이 들어올 수도 있고, 동물이름이 들어올 수도 있는데... 이름만으로는 전혀 파악할 수 없다. )

SoundEx 의 encodedName 은 사람 이름을 영문으로 받아서, 코드로 만드는 기능을 수행한다.

따라서, string 보다는 name ( 혹은 personName ) 이라는 변수를 사용한다면, 변수의 용도를 더 명확히 전달할 수 있다.

이런 이유로 string 이라는 패러미터 이름을 name 으로 교체하였다. ( string 은 구현에 가까운 언어이고, name 은 비즈니스 도메인에 가까운 언어다... 대부분의 경우, 의도를 드러내기 위해서는 구현언어보다 비즈니스 언어에 가까운 것을 쓰는 것이 좋다. )


변수의 이름을 바꿀 때에는 변수 이름을 선택하고, Rename Refactoring (단축키 : Alt+Shift+R) 을 적용한다.

public class SoundEx {
 public String encodedName(String name) {
  return name + "000";
 }
}


코드 커밋하기

변경된 테스트 코드와 프로덕션 코드를 커밋하기 위해 git bash를 켠다

git add로 테스트 코드와 프로덕션 코드를 추가한다.

git commit으로 테스트 코드와 프로덕션 코드를 커밋한다.

코멘트를 작성한다.



세번째 TDD Cycle


TDD 사이클은 항상 실패하는 테스트 케이스를 도출하는 데서 출발한다.

현재 상태에서 실패하는 테스트 케이스 중 가장 간단한 것은 무엇일까?

이름이 "C" 인 경우를 테스트하면 실패할까?

이름이 "C" 인 경우에 대한 assert 문을 추가해본다. ( assertEquals("C000", se.encodedName("C")); )

테스트를 돌리면, 테스트가 성공한다.

이름인 한자인 경우, 실패하는 테스트 케이스를 만들 수 있을까?

아마도 이름인 한 글자인 경우에는 실패하는 테스트 케이스를 만들 수 없을 것이다. 

즉, 한 글자짜리 이름 두 개("A","B") 에 대한 테스트 케이스를 작성하고, 이를 Refactoring 함으로써, 한 글자 짜리 이름에 대해서는 일반화된 솔류션(general solution)을 얻게 된 것이다. 

TDD 에서는 이를 "specifict 한 예제들이 많아지면, solution 은 일반화된다고 표현한다."


한자 짜리의 이름은 일반화하는데 성공했으니 두자리의 이름을 시도해보자.

실패하는 테스트 케이스 만들기

이름이 두자리 수인 경우를 테스트하는 메소드문을 하나 추가한다.



public class SoundExTest {
 @Test
 public void singleLetterName()
 {
  SoundEx se = new SoundEx();
  assertEquals("A000", se.encodedName("A"));
  assertEquals("B000", se.encodedName("B"));
  assertEquals("C000", se.encodedName("C"));
 }
 
 @Test
 public void doubleLetterName()
 {
  SoundEx se = new SoundEx();
  assertEquals("A100", se.encodedName("Ab"));
 }
}

 

테스트를 실행하면, doubleLetterName 테스트가  assertEquals("A100", se.encodedName("Ab")); 라인에서 실패하는 것을 볼 수 있다.

테스트 통과 시키기

가장 간단하게 통과시키기

ppublic class SoundEx {
 public String encodedName(String name) {
  if ( name.length() == 2 ) {
   return "A100";
  }
  return name + "000";
 }
}


테스트 코드를 실행하면, 성공한다. ( 초록불 )


리팩토링 :

리팩토링 할 것이 있는가? 없다.

코드 통합하기 


실패하는 테스트 케이스 만들기

public class SoundExTest {
 @Test
 public void singleLetterName()
 {
  SoundEx se = new SoundEx();
  assertEquals("A000", se.encodedName("A"));
  assertEquals("B000", se.encodedName("B"));
  assertEquals("C000", se.encodedName("C"));
 }
 
 @Test
 public void doubleLetterName()
 {
  SoundEx se = new SoundEx();
  assertEquals("A100", se.encodedName("Ab"));
  assertEquals("B100", se.encodedName("Bb"));
 }
}


테스트를 실행하면, 테스트가 실패한다.


테스트 통과시키기


ppublic class SoundEx {
 public String encodedName(String name) {
  if ( name.length() == 2 ) {
   if ( name == "Bb" )
    return "B100";     
   return "A100";
  }
  return name + "000";
 }
}


테스트를 실행하면, 테스트를 통과한다. ( 초록불 )


리팩토링하기

중복된 코드가 있는가?

"B100" 과 "A100" 간에 중복된 코드가 존재한다.

위의 코드를 아래와 같이 정리해 보면, 중복된 부분을 파악하기가 더 쉽다.


public class SoundEx {
 public String encodedName(String name) {
  if ( name.length() == 2 ) {
   String result = "";
   if ( name == "Bb" ) {
    result += "B";
    result += "1";
    result += "0";
    result += "0";   
   }
   else {  
    result += "A";
    result += "1";
    result += "0";
    result += "0";   
   }
  }
  return name + "000";
 }
}


이제 중복을 제거해 보자.

result += "B",
result += "A";

에서 "B' 와 "A" 는 name 의 첫번째 문자에 해당되는 것이다.

따라서, result += name.charAt(0) 와 같이 쓸 수 있다.


public class SoundEx {
 public String encodedName(String name) {
  if ( name.length() == 2 ) {
   String result = "";
   result += name.charAt(0);
   result += "1";
   result += "0";
   result += "0";   
  }
  else {
   result += name;
   result += "0";
   result += "0";
   result += "0";
  }
 }
}


중복되는 부분이 더 없는가? result += "0" 부분이 중복이다.

result += "0" 부분은 다음과 같이 쓸 수 있다.


public class SoundEx {
 public String encodedName(String name) {
  if ( name.length() == 2 ) {
   String result = "";
   result += name.charAt(0);
   result += "1";
   for ( int i=2; i < 4 ; i++ )
    result += "0";   
  }
  else {
   result += name;
   for ( int i=1; i < 4 ; i++ )
    result += "0";
  }
 }
}


코드 중복이 더 있는가?

있다... for 문이 중복이다.

for 문의 초기값으로 사용된, 2와 1은 각각 result 의 길이를 의미한다. 따라서, 아래와 같이 변경할 수 있다.

public class SoundEx {
 public String encodedName(String name) {
  String result = "";
  if ( name.length() == 2 ) {
   result += name.charAt(0);
   result += "1";
   for ( int i=result.length(); i < 4 ; i++ )
    result += "0";
  }
  else {
   result += name;
   for ( int i=result.length(); i < 4 ; i++ )
    result += "0";
  }
 }
}

초기값을 result.length() 로 변경하면, if 문 안에 있는 for 문이 완전히 동일한 형태를 갖는다. 


이런 경우 ( 동일한 코드가 서로 다른 곳에 중복된 경우 ) 에  "메소드 추출하기(Extract Method : 단축키: Alt+Shift+M)" 리팩토링을 적용할 수 있다. 

메소드로 추출할 역역을 설정한 다음, 메소드 이름을 fillZero 로 입력하면 다음과 같다.

public class SoundEx2 {
 public String encodedName(String name) {
  String result = "";
  if ( name.length() == 2 ) {
   result += name.charAt(0);
   result += "1";
   result = fillZero(result);
  }
  else {
   result += name;
   result = fillZero(result);
  }
 }
 private String fillZero(String result) {
  for ( int i=result.length(); i < 4 ; i++ )
   result += "0";
  return result;
 }
}


리팩토링할 것이 더 있는가?

없다..... 


여기서 느낀 점..

종호: TDD는 SoundEx라는 문제를 '해결하는 것'보다는 문제를 '해결하는 과정'에 초점을 둔 것 같다.

SoundEx는 간단한 문제이다.

하지만 이 간단한 문제를 TDD를 이용하여 해결함으로써 TDD의 과정을 학습하였다.

이때 학습한 TDD를 활용하여 나중에 큰 프로젝트를 수행하게 되었을때도 TDD를 적용할 수 있다.

.....

재훈: 위의 과정이 지루하고 장황하게 보일 수도 있다. 

대부분의 프로그래머들이 그렇게 생각한다.

하지만, 대부분의 프로그래머들은 위와 같은 무의식적으로 과정의 사고를 하고 있는 것이다.

다만, 인지하지 못할 뿐.

TDD 는 프로그래머들이 무의식적으로 사고하는 이런 과정을 분석적으로 접근하도록 만든다.

무의식에서 일어나고 있는 사고과정을 의식화함으로써 분석적 사고를 훈련하게 되는 것이다.

우리가 하는 행동을 추상화된 레벨이 아니라 더 구체화된 레벨에서 사고를 하다 보면, 분석적인 사고가 저절로 길러진다.

분석적 사고력이 향상되면, 더 어렵고 더 추상화된 문제를 일관성 있는 방법으로 분석하고 해결할 수 있게 된다.


테스트 실행....

코드 통합하기 ( commit )



다음 테스트


public class SoundExTest {
 @Test
 public void singleLetterName()
 {
  SoundEx se = new SoundEx();
  assertEquals("A000", se.encodedName("A"));
  assertEquals("B000", se.encodedName("B"));
  assertEquals("C000", se.encodedName("C"));
 }
 
 @Test
 public void doubleLetterName()
 {
  SoundEx se = new SoundEx();
  assertEquals("A100", se.encodedName("Ab"));
  assertEquals("B100", se.encodedName("Bb"));
  assertEquals("B200", se.encodedName("Bc"));
 }
}


테스트 코드 통과시키기

public class SoundEx2 {
 public String encodedName(String name) {
  String result = "";
  if ( name.length() == 2 ) {
   if ( name == "Bc" ) {
    return "B200";
   }
   else {
    result += name.charAt(0);
    result += "1";
    result = fillToZero(result);
   }
  }
  else {
   result += name;
   result = fillToZero(result);
  }
 }
 private String fillToZero(String result) {
  for ( int i=result.length(); i < 4 ; i++ )
   result += "0";
  return result;
 }
}


테스트 코드 실행....


리팩토링

public class SoundEx2 {
 public String encodedName(String name) {
  String result = "";
  if ( name.length() == 2 ) {
   if ( name == "Bc" ) {
    result += name.charAt(0);
    result += "2";
    result = fillZero(result);
   }
   else {
    result += name.charAt(0);
    result += "1";
    result = fillZero(result);
   }
  }
  else {
   result += name;
   result = fillZero(result);
  }
  return result;
 }
 private String fillZero(String result) {
  for ( int i=result.length(); i < 4 ; i++ )
   result += "0";
  return result;
 }
}


리팩토링할 것이 더 있는가? 

코드 중복이 존재하는가?

있다.

result += "2";, result +="1"; 부분이 다르고 

나머지 부분인 result += name.charAt(0);, result += fillZero(result); 는 중복된다.

   if ( name == "Bc" ) {
    result += name.charAt(0);
    result += "2"; //중복
    result = fillZero(result);
   }
   else {
    result += name.charAt(0);
    result += "1"; //중복
    result = fillZero(result);
   }

요기서 느낀점


두개의 동일한 코드가 있는데 A와 B라는 부분만 각각 다른 경우

A와 B를 메소드화시켜서 C 안에서 쓰면 된다.

그러면 나머지 C의 부분은 재작성하지 않아도 되므로 코드의 중복을 줄일 수 있다.

 TDD 로 SoundEx 개발하기 > 그림2.png" class="confluence-embedded-image" style="cursor: move; max-width: 100%;" src="http://wiki.wiselog.com/download/attachments/18226263/%EA%B7%B8%EB%A6%BC2.png?version=1&modificationDate=1423118740000&api=v2" data-mce-src="http://wiki.wiselog.com/download/attachments/18226263/%EA%B7%B8%EB%A6%BC2.png?version=1&modificationDate=1423118740000&api=v2" data-location="[개발본부] Study > TDD 로 SoundEx 개발하기 > 그림2.png" data-linked-resource-container-id="18226263" data-base-url="http://wiki.wiselog.com" data-linked-resource-default-alias="그림2.png" data-linked-resource-type="attachment" data-linked-resource-id="18320617" data-image-src="/download/attachments/18226263/%EA%B7%B8%EB%A6%BC2.png?version=1&modificationDate=1423118740000&api=v2">


반대로 두개의 코드에 D라는 공통된 부분이 있는 경우

D 코드를 Extract Method (단축키 : Alt + Shift + M)로 추출하여 두개의 코드에 각각 D메소드를 써주면

D 코드 부분의 중복을 줄일 수 있다.


 TDD 로 SoundEx 개발하기 > 그림3.png" class="confluence-embedded-image" style="cursor: move; max-width: 100%;" src="http://wiki.wiselog.com/download/attachments/18226263/%EA%B7%B8%EB%A6%BC3.png?version=1&modificationDate=1423118900000&api=v2" data-mce-src="http://wiki.wiselog.com/download/attachments/18226263/%EA%B7%B8%EB%A6%BC3.png?version=1&modificationDate=1423118900000&api=v2" data-location="[개발본부] Study > TDD 로 SoundEx 개발하기 > 그림3.png" data-linked-resource-container-id="18226263" data-base-url="http://wiki.wiselog.com" data-linked-resource-default-alias="그림3.png" data-linked-resource-type="attachment" data-linked-resource-id="18320619" data-image-src="/download/attachments/18226263/%EA%B7%B8%EB%A6%BC3.png?version=1&modificationDate=1423118900000&api=v2">

변하는 코드는 앞으로도 계속 변할 가능성이 높고

변하지 않는 코드는 앞으로도 변하지 않을 가능성이 크다.

따라서 이들을 별개로 구분해서 메소드화 시켜놓고 필요에 따라 조합하면 코드의 중복을 줄일 수 있다. 

리팩토링의 초기 단계는 코드의 중복을 무자비하게 줄이는 것이다.


위의 경우는 첫번째 케이스에 해당된다고 볼 수 있다.

따라서 result+="1";, result+="2";의 부분을 하나로 통합할 수 있는 코드가 필요하다.

"1", "2"는 name의 두번째 문자를 숫자로 치환한 값이다.

따라서 숫자 치환 메소드를 아래와 같이 작성하고, 중복되었던 부분을 치환 코드로 묶었다.

public class SoundEx2 {
 public String encodedName(String name) {
  String result = "";
  if ( name.length() == 2 ) {
   if ( name == "Bc" ) {
    result += name.charAt(0);
    result += getCode(name.charAt(1));
    result = fillZero(result);
   }
   else {
    result += name.charAt(0);
    result += getCode(name.charAt(1));
    result = fillZero(result);
   }
  }
  else {
   result += name;
   result = fillZero(result);
  }
  return result;
 }
 
 private String fillZero(String result) {
  for ( int i=result.length(); i < 4 ; i++ )
   result += "0";
  return result;
 }
 public String getCode(char ch) {
  switch (ch) {
  case 'b':
  case 'f':
  case 'p':
  case 'v':
   return "1";
  case 'c':
  case 'g':
  case 'j':
  case 'k':
  case 'q':
  case 's':
  case 'x':
  case 'z':
   return "2";
  case 'd':
  case 't':
   return "3";
  case 'l':
   return "4";
  case 'm':
  case 'n':
   return "5";
  case 'r':
   return "6";
  case 'a':
  case 'e':
  case 'i':
  case 'o':
  case 'u':
   return "a";
  case 'h':
  case 'w':
   return "h";
  }
  return "";
 }
}


리팩토링할 부분이 있는가? (위에서도 말했듯이 초기 리팩토링의 기준은 중복을 무자비하게 제거하는 것이다.)

있다.

아래의 if 내부 코드가 완전히 똑같아 졌다.

   if ( name == "Bc" ) {
    result += name.charAt(0);
    result += getCode(name.charAt(1));
    result = fillZero(result);
   }
   else {
    result += name.charAt(0);
    result += getCode(name.charAt(1));
    result = fillZero(result);
   }

따라서 중복된 코드를 통합하고 의미가 없어진 if문을 제거한다.

   public class SoundEx2 {
 public String encodedName(String name) {
  String result = "";
  if (name.length() == 2) {
   result += name.charAt(0);
   result += getCode(name.charAt(1));
   result = fillZero(result);
  } else {
   result += name;
   result = fillZero(result);
  }
  return result;
 }
 private String fillZero(String result) {
  for (int i = result.length(); i < 4; i++)
   result += "0";
  return result;
 }
 public String getCode(char ch) {
  switch (ch) {
  case 'b':
  case 'f':
  case 'p':
  case 'v':
   return "1";
  case 'c':
  case 'g':
  case 'j':
  case 'k':
  case 'q':
  case 's':
  case 'x':
  case 'z':
   return "2";
  case 'd':
  case 't':
   return "3";
  case 'l':
   return "4";
  case 'm':
  case 'n':
   return "5";
  case 'r':
   return "6";
  case 'a':
  case 'e':
  case 'i':
  case 'o':
  case 'u':
   return "a";
  case 'h':
  case 'w':
   return "h";
  }
  return "";
 }
}

리팩토링할 것이 있는가?

있다.

if문 안의 result = fillZero(result); 부분이 중복되었다.

이는 if문의 실행여부와 상관없이 실행된다는 뜻이므로 if문 밖으로 이동시킨다.

변경 전

 public String encodedName(String name) {
  String result = "";
  if (name.length() == 2) {
   result += name.charAt(0);
   result += getCode(name.charAt(1));
   result = fillZero(result);
  } else {
   result += name;
   result = fillZero(result);
  }
  return result;
 }

변경 후

 public String encodedName(String name) {
  String result = "";
  if (name.length() == 2) {
   result += name.charAt(0);
   result += getCode(name.charAt(1));
  } else {
   result += name;
  }
   result = fillZero(result);
  return result;
 }

리팩토링할 부분이 있는가?

없다.


코드 통합하기 ( commit )


다음 테스트

 public void doubleLetterName()
 {
  SoundEx se = new SoundEx();
  assertEquals("A100", se.encodedName("Ab"));
  assertEquals("B100", se.encodedName("Bb"));
  assertEquals("B200", se.encodedName("Bc"));
  assertEquals("E100", se.encodedName("Ef"));
  assertEquals("E000", se.encodedName("Ee"));
 }


프로덕션 코드에 assertEquals("E000", se.encodedName("Ee")); 문장을 추가한다.

모음인 e를 공백으로 처리하는 과정이 없었기 때문에 테스트 결과는 실패

테스트 코드 통과시키기

들어오는 값이 모음이 아니면 붙이도록 수정 

 public String encodedName(String name) {
  String result = "";
  if (name.length() == 2) {
   result += name.charAt(0);
   if(!getCode(name.charAt(1)).equals("a")) 
   {
    result += getCode(name.charAt(1));
   }
  } else {
   result += name;
  }
  result = fillZero(result);
  return result;
 }

테스트 통과


리팩토링

리팩토링 할 부분이 없다.

코드 통합하기 ( commit )


다음 테스트

모음까지 섞은 2글자 테스트도 통과하였으므로 2글자에 대한 일반화된 솔루션(general solution)을 구현했다고 판단했다.

다음은 3글자를 테스트한다.

 public void threeLetterName()
 {
  SoundEx2 se = new SoundEx2();
  assertEquals("A120", se.encodedName("Abc"));
 }


테스트 실패. (빨간불)


테스트 통과시키기

3글자일때도 통과하도록 가장 간단하게 코드 작성 (2글자 코드를 copy & paste)

 public String encodedName(String name) {
  String result = "";
  if (name.length() == 2) {
   result += name.charAt(0);
   if (!getCode(name.charAt(1)).equals("a")) {
    result += getCode(name.charAt(1));
   }
  } 
  else if (name.length()==3) {
   result += name.charAt(0);
   if (!getCode(name.charAt(1)).equals("a")) {
    result += getCode(name.charAt(1));
   }
   if (!getCode(name.charAt(2)).equals("a")) {
    result += getCode(name.charAt(2));
   }
  }
  else {
   result += name;
  }
  result = fillZero(result);
  return result;
 }

테스트 통과 (초록불)


리팩토링

리팩토링 할 것이 있는가?

있다.

아래에 if문의 코드에는 규칙성과 중복이 존재한다.

따라서 for문으로 작성할 수 있다.

  if (name.length() == 2) {
   result += name.charAt(0);
   if (!getCode(name.charAt(1)).equals("a")) {
    result += getCode(name.charAt(1));
   }
  } else if (name.length() == 3) {
   result += name.charAt(0);
   if (!getCode(name.charAt(1)).equals("a")) {
    result += getCode(name.charAt(1));
   }
   if (!getCode(name.charAt(2)).equals("a")) {
    result += getCode(name.charAt(2));
   }
  } else {
   result += name;
  }


변경 후

  if (name.length() == 2) {
   result += name.charAt(0);
   for (int i = 1; i < name.length(); i++) {
    if (!getCode(name.charAt(i)).equals("a")) {
     result += getCode(name.charAt(i));
    }
   }
  } else if (name.length() == 3) {
   result += name.charAt(0);
   for (int i = 1; i < name.length(); i++) {
    if (!getCode(name.charAt(i)).equals("a")) {
     result += getCode(name.charAt(i));
    }
   }
  } else {
   result += name;
  }


리팩토링 할 것이 있는가?

있다.

if문 내부의 내용이 완전히 중복되므로 if문을 통합한다.

  result += name.charAt(0);
  for (int i = 1; i < name.length(); i++) {
   if (!getCode(name.charAt(i)).equals("a")) {
    result += getCode(name.charAt(i));
   }
  }

리팩토링 할 것이 있는가?

없다.


다음 테스트

몇가지 케이스를 더 추가시켜 보았다.

모음을 섞은 케이스

 public void threeLetterName()
 {
  SoundEx2 se = new SoundEx2();
  assertEquals("A120", se.encodedName("Abc"));
  assertEquals("d100", se.encodedName("def"));
 }

테스트 통과(초록불)


중간에 h를 집어넣은 케이스

  public void threeLetterName()
 {
  SoundEx2 se = new SoundEx2();
  assertEquals("A120", se.encodedName("Abc"));
  assertEquals("d100", se.encodedName("def"));
  assertEquals("g000", se.encodedName("ghi"));
 }

테스트가 통과되지 않는다.

h를 공백으로 처리하는 부분이 없기 때문이다.

테스트 통과시키기

모음이 아닌 경우 중 h, w도 아닌 경우에만 문자가 추가되도록 수정

 public String encodedName(String name) {
  String result = "";
  result += name.charAt(0);
  for (int i = 1; i < name.length(); i++) {
   if (!getCode(name.charAt(i)).equals("a")) {
    if (!getCode(name.charAt(i)).equals("h")) {
     result += getCode(name.charAt(i));
    }
   }
  }
  result = fillZero(result);
  return result;
 }

테스트가 통과된다.


리팩토링할 부분이 있는가?

코드를 문자로 변환하는 기능의 단위가 완성되었다.

가독성과 중복 제거를 위하여 해당 코드를 메소드로 추출(Extract Method, 단축키 : Alt + Shift + M)한다


public String encodedName(String name) {
  String result = "";
  result += name.charAt(0);
  result = encodedTail(name, result);
  result = fillZero(result);
  return result;
 }
 private String encodedTail(String name, String result) {
  for (int i = 1; i < name.length(); i++) {
   if (!getCode(name.charAt(i)).equals("a")) {
    if (!getCode(name.charAt(i)).equals("h")) {
     result += getCode(name.charAt(i));
    }
   }
  }
  return result;
 }


리팩토링할 부분이 있는가?

없다.

코드 통합하기 ( commit )


실패하는 코드 작성하기

3글자에서 h,w 밑 모음의 통과도 성공했으므로 3글자 변환에 대한 일반화된 솔루션 (general solution)이 제작되었다고 판단.

4글자 코드에 대한 솔루션을 제작한다.

4글자 코드에 대하여 실패하는 코드를 작성해본다.

 public void fourLetterName()
 {
  SoundEx2 se = new SoundEx2();
  assertEquals("A123", se.encodedName("Abcd"));
  assertEquals("e120", se.encodedName("efgh"));
  assertEquals("i240", se.encodedName("ijkl"));
 }
assertEquals("i240", se.encodedName("ijkl")); 문장은 중복되는 숫자가 오면 제거해야되는 요구사항을 만족하지 못하므로 통과하지 못한다.

i240이 기댓값이지만 결과값은 1224가 나오게 된다.


중복을 제거하는 코드를 실패하도록 작성한다.


잠깐 느낀점..

중복을 제거하는 부분은 글자를 인코딩하는과는 별개의 기능이다.

그렇기 때문에 이 부분은 메소드를 따로 만들어서 테스트를 해야한다. (SRP : 1메소드는 1개의 기능만 가져야 한다)

그렇지 않으면 각 기능의 경우의 수가 곱해지기 때문의 테스트 조합의 수가 폭발적으로 많아진다.

예를들면 encodedName은 다음과 같은 기능으로 구성된다.

1. 첫번째 글자를 그대로 출력
2. 나머지는 숫자로 바꿈
3. 네자리로 맞춤
  3-1. 넘으면 자르고
  3-2. 적으면 0을 채운다.
4. 모음을 제거한다.
5. 이웃된 중복을 제거한다.

이것을 프로시저 방식 (절차지향)으로 작성한다면

한 메소드에 모든 코드를 다 쓰게 된다.

이때 오류가 난다면 어디서부터 수정을 해야할 것인가..

이를 각 메소드로 분리하여 아래와 같은 메소드들로 분할시키고 각 메소드의 테스트를 완료한다면

getCode()
encodedTail()
removeDuplication()
fillZero()

나머지는 기능 테스트가 완료되었기 때문에 테스트가 실패하는 부분만 점검하면 오류를 잡아낼 수 있다.

즉 디버깅 작업에 걸리는 시간이 단축된다.

이렇게 메소드에 있는 기능들을 분리시킬때 각 메소드의 추상화 레벨은 같아야 한다.

추상화란 무엇인가?

name.charAt(0); -> how 어떻게 구현?

getFirstLetterOfName(name); -> what 무엇을 구현?, 추상화된 표현

무엇을 구현해야 할지 추상화 시켜놓으면 주석이 없어도 읽기 편하고 나중에 자신이 무엇을 했는지도 읽을 수 있다. (비즈니스적 표현)


추상화 레벨이란?

소스를 얼마나 추상적으로 표현했는가를 단계적으로 구분하는 것이다.

예를들어 encodedName 메소드는 이름을 인코딩하는 기능을 가지고 있다.

이 기능을 단계별로 추상적으로 메소드로 묶어 표현하고 있다. (같은 추상화 레벨)

getCode()
encodedTail()
removeDuplication()
fillZero()

그런데 갑자기 여기에서 중복을 제거할 목적으로 아래와 같은 소스를 중간에 첨부하면 안된다는 말이다. (같은 추상화 레벨이 아님)

 char previousChar;
  previousChar = getCode(Character.toLowerCase(name.charAt(0))).charAt(0);
  if (previousChar != name.charAt(1)) { 
   result += name.charAt(1);
  } else {
   if (previousChar != name.charAt(2)) {
    result += name.charAt(2);
   } else {
    if (previousChar != name.charAt(3)) {
     result += name.charAt(3);
    } else {
     if (previousChar != name.charAt(4)) {
      result += name.charAt(4);
     }
    }
   }
  }
  previousChar = name.charAt(1);
  if (previousChar != name.charAt(2)) {
   result += name.charAt(2);
  } else {
   if (previousChar != name.charAt(3)) {
    result += name.charAt(3);
   }
  }
  previousChar = name.charAt(2);
  if (previousChar != name.charAt(3)) {
   result += name.charAt(3);
  }
  previousChar = name.charAt(3);


어쨌든, 중복을 제거하는 실패된 테스트 코드를 만들어 보기로 한다.


public String removeDuplication(String name) { 
 return "";
 }



프로덕션 코드는 다음과 같다.

encodedTail()을 거쳐서 인코딩된 문자열이 나오므로 input에는 인코딩된 문자열을 넣는다.

 public void removeDuplicationTest()
 {
 SoundEx2 se = new SoundEx2();
 assertEquals("A1", se.removeDuplication("A111"));
 }


"A1"과 테스트코드의 반환값인 ""은 같지 않기 때문에 테스트 코드는 실패한다. (빨간불)


테스트 코드 통과시키기

제일 단순한 방법으로 테스트코드를 통과시킨다.

public String removeDuplication(String name) {
  String result = "";
  result += name.charAt(0);
  char previousChar;
  
  previousChar = getCode(Character.toLowerCase(name.charAt(0))).charAt(0);
  if (previousChar != name.charAt(1)) { // A가 들어옴
   result += name.charAt(1);
  }
  else if (previousChar != name.charAt(2)) {
   result += name.charAt(2);
  }
  else if (previousChar != name.charAt(3)) {
   result += name.charAt(3);
  }
  previousChar = name.charAt(1);
  if (previousChar != name.charAt(2)) {
   result += name.charAt(2);
  }
  else if (previousChar != name.charAt(3)) {
   result += name.charAt(3);
  }
  previousChar = name.charAt(2);
  if (previousChar != name.charAt(3)) {
   result += name.charAt(3);
  }
  previousChar = name.charAt(3);
  return result;
 }


리팩토링

반복되는 부분을 찾아서 중복을 제거한다.

public String removeDuplication(String name) {
  String result = "";
  result += name.charAt(0);
  char previousChar;
  previousChar = getCode(Character.toLowerCase(name.charAt(0))).charAt(0);
  for (int i = 1; i < 4; i++) {
   if (previousChar != name.charAt(i)) {
    result += name.charAt(i);
    previousChar = name.charAt(1);
    break;
   }
  }
  for (int i = 2; i < 4; i++) {
   if (previousChar != name.charAt(i)) {
    result += name.charAt(i);
    previousChar = name.charAt(2);
    break;
   }
  }
  for (int i = 3; i < 4; i++) {
   if (previousChar != name.charAt(i)) {
    result += name.charAt(i);
    previousChar = name.charAt(3);
    break;
   }
  }
  return result;
 }

리팩토링 할 부분이 남아 있는가?

있다. 아직 중복이 완전히 제거되지 않았다.

public String removeDuplication(String name) {
  String result = "";
  result += name.charAt(0);
  char previousChar;
  previousChar = getCode(Character.toLowerCase(name.charAt(0))).charAt(0);
  for(int j=1;j<=3;j++)
  {
   for (int i = 1; i < 4; i++) {
    if (previousChar != name.charAt(i)) {
     result += name.charAt(i);
     previousChar = name.charAt(j);
     break;
    }
   }
  }
  return result;
 }

리팩토링 할 부분이 남아 있는가?

있다. for 반복문의 3과 4는 name의 길이(4)와 연관성이 있다.

따라서 아래와 같이 수정할 수 있다.

 public String removeDuplication(String name) {
  String result = "";
  result += name.charAt(0);
  char previousChar;
  previousChar = getCode(Character.toLowerCase(name.charAt(0))).charAt(0);
  for(int j=1;j<=name.length()-1;j++)
  {
   for (int i = 1; i < name.length(); i++) {
    if (previousChar != name.charAt(i)) {
     result += name.charAt(i);
     previousChar = name.charAt(j);
     break;
    }
   }
  }
  return result;
 }

리팩토링 할 부분이 남아 있는가?

없다.

코드 통합하기 ( commit )


테스트 코드 실행

public void removeDuplicationTest()
 {
  SoundEx2 se = new SoundEx2();
  assertEquals("A1", se.removeDuplication("A1111"));
  assertEquals("A1", se.removeDuplication("A111"));
  assertEquals("A1", se.removeDuplication("A11"));
  assertEquals("A1", se.removeDuplication("A11"));
  assertEquals("A1", se.removeDuplication("A1"));
 }

똑같은 문자는 문자의 길이에 상관없이 중복이 제거된다.

여러가지 문자에 대하여 중복이 제거되는지를 테스트해보지 않았기 때문에 여러가지 문자를 추가해본다.

public void removeDuplicationTest()
 {
  SoundEx2 se = new SoundEx2();
  assertEquals("A1", se.removeDuplication("A1111"));
  assertEquals("A1", se.removeDuplication("A111"));
  assertEquals("A1", se.removeDuplication("A11"));
  assertEquals("A1", se.removeDuplication("A11"));
  assertEquals("A1", se.removeDuplication("A1"));
  assertEquals("A21", se.removeDuplication("A221")); 
 }

assertEquals("A21", se.removeDuplication("A221")); 문장을 추가하니 테스트가 실패한다.


느낀점..

시작을 단순한 케이스(A111)로 시작하고 어느정도 추상화 시키고 나니

다른 케이스 (A221)을 집어넣었을때 테스트를 통과시키기가 어렵다.

시작을 복잡한 케이스로 시작하고 추상화를 시키는것이 나을 것 같아서 다시 시작한다.

또한 최종 요구사항에 중간에 '모음'이 들어가면 중복되는 값이라도 일단 한번은 붙이고

'h'나'w'가 들어가면 계속 중복처리를 해줘야 하는데 encodeTail에서 이미 제거처리를 해줬기 때문에 중복을 제거하는 부분에서 구분해줄 수 없게 되었다.

따라서 encodedTail()메소드도 수정이 불가피하다.

우선 encodedTail()메소드부터 수정하기로 한다.

output은 어떤 name을 넣었을때 'h'나 'w' 혹은 '모음'에 상관없이 변환된 값을 붙여서 출력해주는 것으로 한다.

지금 느낀점은 코딩을 시작할때부터 요구사항 전체를 숙지하고 신중하게 프로그래밍 해야 한다는 것이다.

그나마 메소드들이 기능별로 분리되었기에 encodedTail부분만 간단하게 수정할 수 있다.


실패하는 테스트 코드 작성하기

기존의 encodedTail 메소드(모음과 'h', 'w'를 제거해버림)

public String encodedTail(String name) {
  String result="";
  for (int i = 1; i < name.length(); i++) {
   if (!getCode(name.charAt(i)).equals("a")) {
    if (!getCode(name.charAt(i)).equals("h")) {
     result += getCode(name.charAt(i));
    }
   }
  }
  return result;
 }



프로덕션 코드는 다음과 같다.

input을 모음과 h,w의 생략 없이 getCode로 치환한 값이다


public void encodedTailTest()
 {
  SoundEx2 se = new SoundEx2();
  assertEquals("123a12ha22455a12623a1h2a2", se.encodedTail("Abcdefghijklmnopqrstuvwxyz"));
 }


기존의 encodedTail메소드는 모음과 'h','w'를 제거하기 때문에 테스트 코드는 실패한다. (빨간불)

통과하는 테스트 코드 만들기

기존의 encodedTail메소드를 아래와 같이 수정

 public String encodedTail(String name) {
  String result = "";
  for (int i = 1; i < name.length(); i++) {
   result += getCode(name.charAt(i));
  }
  return result;
 }

모음, 'h', 'w'를 걸러내는 if문을 제거했기 때문에 테스트코드는 통과한다.

리팩토링

없다.

코드 통합하기 ( commit )


실패하는 테스트 코드 만들기

removeDuplication()메소드를 재작성한다.

 public String removeDuplication2(String name) {
  String result = "";
  return result;
 }


테스트 코드를 작성한다.

 public void removeDupicationTest2() {
  // 중복 테스트
  assertEquals("A1", se.removeDuplication2("A11"));
 }


"A1"과 ""는 같지 않기 때문에 테스트 코드는 실패한다.

테스트 코드 가장 간단하게 통과시키기

  public String removeDuplication2(String name) {
  String result = "";
  result ="A1";
  return result;
 }

리팩토링

없다.

코드 커밋하기


다음 실패하는 테스트코드

  public void removeDupicationTest2() {
  // 중복 테스트
  assertEquals("A1", se.removeDuplication2("A11"));
  assertEquals("A2", se.removeDuplication2("A22")); //통과 실패
 }

테스트 코드 통과시키기

  public String removeDuplication2(String name) {
  String result = "";
  if (name == "A11") {
   result = "A1";
  }
  if (name == "A22") {
   result = "A2";
  }
  return result;
 }

테스트코드는 통과한다


리팩토링

있다.

 public String removeDuplication2(String name) {
  String result = "";
  if (name == "A11") {
   result += "A";
   result += "1";
  }
  if (name == "A22") {
   result += "A";
   result += "2";
  }
  return result;
 }

리팩토링

 public String removeDuplication2(String name) {
  String result = "";
  if (name == "A11") {
   result += name.charAt(0);
   result += name.charAt(1);
  }
  if (name == "A22") {
   result += name.charAt(0);
   result += name.charAt(1);
  }
  return result;
 }


리팩토링

중요.. 원래는 result에 3번째 문자도 더해지는 것이 규칙이지만 중복 제거의 조건에 의하여 2번째 문자와 같으므로 더해주지 않는다.

(공백이었지만 조건을 발견한 것)

 public String removeDuplication2(String name) {
  String result = "";
  if (name == "A11") {
   result += name.charAt(0);
   result += name.charAt(1);
   if (name.charAt(1) != name.charAt(2)) {
    result += name.charAt(2);
   }
  }
  if (name == "A22") {
   result += name.charAt(0);
   result += name.charAt(1);
   if (name.charAt(1) != name.charAt(2)) {
    result += name.charAt(2);
   }
  }
  return result;
 }


리팩토링

나머지 첫번째와 두번째 문자에도 이 조건은 동일하게 적용할 수 있다.

이제 완전히 중복되는 코드가 발생하였다.

 public String removeDuplication2(String name) {
  String result = "";
  if (name == "A11") {
   result += name.charAt(0);
   if (name.charAt(0) != name.charAt(1)) {
    result += name.charAt(1);
   }
   if (name.charAt(1) != name.charAt(2)) {
    result += name.charAt(2);
   }
  }
  if (name == "A22") {
   result += name.charAt(0);
   if (name.charAt(0) != name.charAt(1)) {
    result += name.charAt(1);
   }
   if (name.charAt(1) != name.charAt(2)) {
    result += name.charAt(2);
   }
  }
  return result;
 }


리팩토링

나머지 첫번째와 두번째 문자에도 이 조건은 동일하게 적용할 수 있다.

이제 완전히 중복되는 코드가 발생하였다.

 public String removeDuplication2(String name) {
  String result = "";
  if (name == "A11") {
   result += name.charAt(0);
   if (name.charAt(0) != name.charAt(1)) {
    result += name.charAt(1);
   }
   if (name.charAt(1) != name.charAt(2)) {
    result += name.charAt(2);
   }
  }
  if (name == "A22") {
   result += name.charAt(0);
   if (name.charAt(0) != name.charAt(1)) {
    result += name.charAt(1);
   }
   if (name.charAt(1) != name.charAt(2)) {
    result += name.charAt(2);
   }
  }
  return result;
 }

리팩토링

중복된 코드를 통일하였다.

 public String removeDuplication2(String name) {
  String result = "";
  if (name.length() == 3) {
   result += name.charAt(0);
   if (name.charAt(0) != name.charAt(1)) {
    result += name.charAt(1);
   }
   if (name.charAt(1) != name.charAt(2)) {
    result += name.charAt(2);
   }
  }
  return result;
 }


리팩토링

name.charAt(0);은 비교대상이 알파벳으로 들어오므로 인코딩해주는 과정을 별개로 거쳐야 한다.

previousChar이라는 문자 변수를 만들어 여기에 저장한다.

 public String removeDuplication2(String name) {
  String result = "";
  char previousChar = getCode(Character.toLowerCase(name.charAt(0)));
  if (name.length() == 3) {
   result += name.charAt(0);
   if (previousChar != name.charAt(1)) {
    result += name.charAt(1);
   }
   if (name.charAt(1) != name.charAt(2)) {
    result += name.charAt(2);
   }
  }
  return result;
 }


코드 커밋하기


실패하는 테스트 코드 작성

public void removeDupicationTest2() {
  // 중복 테스트
  assertEquals("A1", se.removeDuplication2("A11"));
  assertEquals("A2", se.removeDuplication2("A22"));
  assertEquals("B34", se.removeDuplication2("B334")); //4글자 입력은 실패
 }


테스트 코드 통과시키기

3글자에서 이용하던 알고리즘을 4글자일때 적용시켰더니 테스트가 통과했다.

 public String removeDuplication2(String name) {
  String result = "";
  char previousChar = getCode(Character.toLowerCase(name.charAt(0)));
  if (name.length() == 3) {
   result += name.charAt(0);
   if (previousChar != name.charAt(1)) {
    result += name.charAt(1);
   }
   if (name.charAt(1) != name.charAt(2)) {
    result += name.charAt(2);
   }
  }
  if(name.length()==4)
  {
   result += name.charAt(0);
   if (previousChar != name.charAt(1)) {
    result += name.charAt(1);
   }
   if (name.charAt(1) != name.charAt(2)) {
    result += name.charAt(2);
   }
   if (name.charAt(2) != name.charAt(3)) {
    result += name.charAt(3);
   }
  }
  return result;
 }

리팩토링

if문이 규칙적으로 중복되므로 for문을 이용하여 중복을 제거한다.

 public String removeDuplication2(String name) {
  String result = "";
  char previousChar = getCode(Character.toLowerCase(name.charAt(0)));
  if (name.length() == 3) {
   result += name.charAt(0);
   if (previousChar != name.charAt(1)) {
    result += name.charAt(1);
   }
   if (name.charAt(1) != name.charAt(2)) {
    result += name.charAt(2);
   }
  }
  if(name.length()==4)
  {
   result += name.charAt(0);
   if (previousChar != name.charAt(1)) {
    result += name.charAt(1);
   }
   for(int i=2;i<name.length();i++)
   {
    if (name.charAt(i-1) != name.charAt(i)) {
     result += name.charAt(i);
    }
   }
  }
  return result;
 }


리팩토링

3글자인 경우에도 적용할 수 있다. (공백도 중복으로 보는 것)

 public String removeDuplication2(String name) {
  String result = "";
  char previousChar = getCode(Character.toLowerCase(name.charAt(0)));
  if (name.length() == 3) {
   result += name.charAt(0);
   if (previousChar != name.charAt(1)) {
    result += name.charAt(1);
   }
   for(int i=2;i<name.length();i++)
   {
    if (name.charAt(i-1) != name.charAt(i)) {
     result += name.charAt(i);
    }
   }
  }
  if(name.length()==4)
  {
   result += name.charAt(0);
   if (previousChar != name.charAt(1)) {
    result += name.charAt(1);
   }
   for(int i=2;i<name.length();i++)
   {
    if (name.charAt(i-1) != name.charAt(i)) {
     result += name.charAt(i);
    }
   }
  }
  return result;
 }


리팩토링

3글자와 4글자인 경우의 처리 구문이 완전히 중복되므로 통합한다.

 public String removeDuplication2(String name) {
  String result = "";
  char previousChar = getCode(Character.toLowerCase(name.charAt(0)));
  result += name.charAt(0);
  if (previousChar != name.charAt(1)) {
   result += name.charAt(1);
  }
  for (int i = 2; i < name.length(); i++) {
   if (name.charAt(i - 1) != name.charAt(i)) {
    result += name.charAt(i);
   }
  }
  return result;
 }


리팩토링

name.charAt(i-1);은 이전 문자와 현재 문자를 비교하는 것이므로 previousChar와 의미가 통한다.

따라서 name.charAt(i-1);부분을 previousChar로 치환하고 previousChar에 현재 값을 넣어주면

코드의 가독성이 더 좋아진다.

 public String removeDuplication2(String name) {
  String result = "";
  char previousChar = getCode(Character.toLowerCase(name.charAt(0)));
  result += name.charAt(0);
  for (int i = 1; i < name.length(); i++) {
   if (previousChar != name.charAt(i)) {
    result += name.charAt(i);
   }
   previousChar = name.charAt(i);
  }
  return result;
 }


코드 커밋하기


숫자 중복 코드 일반화

마지막 수정에서 기존의 하드코딩방식을 name의 길이를 기준으로 반복문을 사용하여 동적으로 변화시켰다.

이제 글자의 길이에 따라 유동적으로 중복을 검사하는 코드가 되었을 것이다.

테스트가 일반화되었는지 확인하기 위하여 "B334"말고도 여러가지 코드를 추가한다.

잘 통과되는것을 확인할 수 있다.

따라서 숫자의 중복을 제거하는 솔루션이 일반화 되었다고 판단할 수 있다.

 public void removeDupicationTest2() {
  // 중복 테스트
  assertEquals("A1", se.removeDuplication2("A11"));
  assertEquals("A2", se.removeDuplication2("A22"));
  assertEquals("B34", se.removeDuplication2("B334"));
  assertEquals("B43", se.removeDuplication2("B433"));
  assertEquals("B343", se.removeDuplication2("B343"));
  assertEquals("B21", se.removeDuplication2("B112211"));
  assertEquals("B3121", se.removeDuplication2("B33112211"));
 }

이제 숫자의 중복을 제거하는 부분은 해결되었다.

이제 아래의 과정이 남아있다.

  1. 모음이 들어올때의 처리.
  2. h, w가 들어올때의 처리.

이미 지나온 과정에서 모음이 들어왔을때는 a로 치환, h또는 w가 들어왔을 때는 h로 치환해주는 getCode를 작성하였다.

따라서 a가 들어왔을때에는 모음이 들어왔다고 생각할 수 있고

h가 들어왔을 때에는 h혹은 w가 들어왔다고 생각할 수 있다.

위의 두 경우는 각자 처리과정이 다르므로 별도로 작성을 한다음 합치는 것이 수월하다. (한번에 여러가지를 추상화 해서 해결하려면 어렵고, 작은 문제부터 일반화시킨다음 합치는 것이 쉽다는 오이사님의 가르침)

먼저 h(h, w가 들어온 경우)부터 처리해주는 기능을 작성해본다.
 

실패하는 테스트 코드 작성하기

아직 h를 처리하는 부분이 없기 때문에 assertEquals("A1", se.removeDuplication2("A1h1"));는 실패한다.

 public void removeDupicationTest2() {
  // 중복 테스트
  assertEquals("A1", se.removeDuplication2("A11"));
  assertEquals("A2", se.removeDuplication2("A22"));
  assertEquals("B34", se.removeDuplication2("B334"));
  assertEquals("B43", se.removeDuplication2("B433"));
  assertEquals("B343", se.removeDuplication2("B343"));
  assertEquals("B21", se.removeDuplication2("B112211"));
  assertEquals("B3121", se.removeDuplication2("B33112211"));
 
  //h 테스트
  assertEquals("A1", se.removeDuplication2("A1h1"));
 }

테스트 코드 통과시키기

들어온 문자가 h를 포함하고 있으면 기존에 처리했던 코드와 별개로 분기시켜서 작성한다.

테스트 코드는 통과한다.

  public String removeDuplication2(String name) {
  String result = "";
  char previousChar = getCode(Character.toLowerCase(name.charAt(0)));
  if (name.contains("h")) {
   if (name == "A1h1") {
    result = "A1";
   }
  } else {
   result += name.charAt(0);
   for (int i = 1; i < name.length(); i++) {
    if (previousChar != name.charAt(i)) {
     result += name.charAt(i);
    }
    previousChar = name.charAt(i);
   }
  }
  return result;
 }

리팩토링

하드코딩된 부분을 분해한다.

 public String removeDuplication2(String name) {
  String result = "";
  char previousChar = getCode(Character.toLowerCase(name.charAt(0)));
  if (name.contains("h")) {
   if (name == "A1h1") {
    result += "A"; //분해
    result += "1"; //분해
   }
  } else {
   result += name.charAt(0);
   for (int i = 1; i < name.length(); i++) {
    if (previousChar != name.charAt(i)) {
     result += name.charAt(i);
    }
    previousChar = name.charAt(i);
   }
  }
  return result;
 }


리팩토링

바뀐 값에서 공통점(들어온 name의 n번째 글자)를 찾아서 일반화시킨다.

첫번째, 두번째. 뭔가 반복의 느낌이 난다.

  public String removeDuplication2(String name) {
  String result = "";
  char previousChar = getCode(Character.toLowerCase(name.charAt(0)));
  if (name.contains("h")) {
   if (name == "A1h1") {
    result += name.charAt(0); //"A"는 들어온 name의 첫번째 글자
    result += name.charAt(1); //"1"는 들어온 name의 두번째 글자
   }
  } else {
   result += name.charAt(0);
   for (int i = 1; i < name.length(); i++) {
    if (previousChar != name.charAt(i)) {
     result += name.charAt(i);
    }
    previousChar = name.charAt(i);
   }
  }
  return result;
 }

리팩토링

그렇다면 세번째와 네번째 글자는 왜 추가되지 못했는지 원인을 분석한다..

세번째 글자는 h가 들어왔기 때문에 추가되지 못했고

네번째 글자는 h 이전 글자인 두번째 글자와 중복되기 때문에 들어오지 못했다.

분석한 원인을 코드로 작성한다.

원래는 공백이었지만 코드가 생겼다. (공백도 중복화시킬 수 있어야 한다.)

 public String removeDuplication2(String name) {
  String result = "";
  char previousChar = getCode(Character.toLowerCase(name.charAt(0)));
  if (name.contains("h")) {
   if (name == "A1h1") {
    result += name.charAt(0);
    result += name.charAt(1);
    if(name.charAt(2)!='h')
    {
     result += name.charAt(2);
    }
    if(name.charAt(3)!=name.charAt(1))
    {
     result += name.charAt(3);
    }
   }
  } else {
   result += name.charAt(0);
   for (int i = 1; i < name.length(); i++) {
    if (previousChar != name.charAt(i)) {
     result += name.charAt(i);
    }
    previousChar = name.charAt(i);
   }
  }
  return result;
 }


리팩토링

h가 아니어야 result에 추가된다는 것은 모든 글자가 동일할 것이다. (첫번째 문자는 h가 되더라도 result에 들어오므로 첫번째 문자는 비교 대상에서 제외한다.)

따라서 각 문자를 더하는 부분에 조건문을 동일하게 추가시켜준다. 이제 중복의 냄새가 나기 시작한다..

 public String removeDuplication2(String name) {
  String result = "";
  char previousChar = getCode(Character.toLowerCase(name.charAt(0)));
  if (name.contains("h")) {
   if (name == "A1h1") {
    result += name.charAt(0);
    if (name.charAt(1) != 'h') {
     result += name.charAt(1);
    }
    if (name.charAt(2) != 'h') {
     result += name.charAt(2);
    }
    if (name.charAt(3) != 'h') {
     if (name.charAt(3) != name.charAt(1)) {
      result += name.charAt(3);
     }
    }
   }
  } else {
   result += name.charAt(0);
   for (int i = 1; i < name.length(); i++) {
    if (previousChar != name.charAt(i)) {
     result += name.charAt(i);
    }
    previousChar = name.charAt(i);
   }
  }
  return result;
 }


리팩토링

if (name.charAt(3) != name.charAt(1)) {
 result += name.charAt(3);
}

위의 코드의 의미를 보자.

name.charAt(3)은 현재 문자

name.charAt(1)은 비교 대상이다.

따라서 previousChar 변수에 대입하여 의미를 명확하게 하는 것이 좋다.

그렇다면 이 비교대상은 어떻게 정의하는가?

기본적으로는 현재 문자가 통과되면, 현재 문자를 비교대상으로 삼는다.

하지만 h가 들어올 경우에는, 현재 문자를 비교대상으로 삼지 않는다 (그렇게 되면, 그 전 문자가 비교대상이 된다.)

 public String removeDuplication2(String name) {
  String result = "";
  char previousChar = getCode(Character.toLowerCase(name.charAt(0)));
  if (name.contains("h")) {
   if (name == "A1h1") {
    result += name.charAt(0);
    if (name.charAt(1) != 'h') { //현재 문자가 h가 아니면
     result += name.charAt(1);
     previousChar = name.charAt(1); //비교 문자에 현재 문자를 대입
    }
    if (name.charAt(2) != 'h') { //현재 문자가 h이기 때문에
     result += name.charAt(2);
     previousChar = name.charAt(2); //비교 문자에 현재 문자(h)가 들어가지 않아서 이 전 값인 1이 비교 대상으로 남아있다.
    }
    if (name.charAt(3) != 'h') {
     if (name.charAt(3) != previousChar) { //값을 넣을때에는 비교 대상 문자와 비교한다.
      result += name.charAt(3);
     }
     previousChar = name.charAt(3);
    }
   }
  } else {
   result += name.charAt(0);
   for (int i = 1; i < name.length(); i++) {
    if (previousChar != name.charAt(i)) {
     result += name.charAt(i);
    }
    previousChar = name.charAt(i);
   }
  }
  return result;
 }



리팩토링

이 조건은 다른 문자를 추가시킬때에도 동일하게 적용된다.

이제 완전히 중복된 코드가 발생하였다.

 public String removeDuplication2(String name) {
  String result = "";
  char previousChar = getCode(Character.toLowerCase(name.charAt(0)));
  if (name.contains("h")) {
   if (name == "A1h1") {
    result += name.charAt(0);
    if (name.charAt(1) != 'h') {
     if (name.charAt(1) != previousChar) {
      result += name.charAt(1);
     }
     previousChar = name.charAt(1);
    }
    if (name.charAt(2) != 'h') {
     if (name.charAt(2) != previousChar) {
      result += name.charAt(2);
     }
     previousChar = name.charAt(2);
    }
    if (name.charAt(3) != 'h') {
     if (name.charAt(3) != previousChar) {
      result += name.charAt(3);
     }
     previousChar = name.charAt(3);
    }
   }
  } else {
   result += name.charAt(0);
   for (int i = 1; i < name.length(); i++) {
    if (previousChar != name.charAt(i)) {
     result += name.charAt(i);
    }
    previousChar = name.charAt(i);
   }
  }
  return result;
 }


리팩토링

반복문을 사용하여 중복을 제거하였다.

 public String removeDuplication2(String name) {
  String result = "";
  char previousChar = getCode(Character.toLowerCase(name.charAt(0)));
  if (name.contains("h")) {
   if (name == "A1h1") {
    result += name.charAt(0);
    for(int i=1;i<name.length();i++)
    {
     if (name.charAt(i) != 'h') {
      if (name.charAt(i) != previousChar) {
       result += name.charAt(i);
      }
      previousChar = name.charAt(i);
     }
    }
   }
  } else {
   result += name.charAt(0);
   for (int i = 1; i < name.length(); i++) {
    if (previousChar != name.charAt(i)) {
     result += name.charAt(i);
    }
    previousChar = name.charAt(i);
   }
  }
  return result;
 }


리팩토링

for(int i=1;i<name.length();i++){
 if (name.charAt(i) != 'h') { //h면 아무 문장도 실행하지 않는다.
  if (name.charAt(i) != previousChar) {
   result += name.charAt(i);
   previousChar = name.charAt(i);
  }
}

위의 코드를 보자. 현재 들어온 문자가 h이면 아무것도 하지 않는다.

이럴때는 조건문을 역으로 뒤집어서 h일때 아무것도 실행해주지 않는 continue 문장을 넣는다.

이것을 가드 클로즈라고 한다.

가드 클로즈를 적용시키면 얻는 장점

  1. 코드가 정형화된다. (최종적으로 나오는 형태가 비슷해져서 다른 코드와 중복이 될 확률이 높다. 즉, 다른 솔루션들끼리 합치기가 쉽다)
  2. 읽기가 쉬워진다.
for (int i = 1; i < name.length(); i++) {
 if (name.charAt(i) == 'h') { //h면 아무 문장도 실행하지 않는다.
  continue;
 }
 if (name.charAt(i) != previousChar) {
  result += name.charAt(i);
 }
 previousChar = name.charAt(i);
}

가드 클로즈를 적용한 코드.

 public String removeDuplication2(String name) {
  String result = "";
  char previousChar = getCode(Character.toLowerCase(name.charAt(0)));
  if (name.contains("h")) {
   result += name.charAt(0);
   for (int i = 1; i < name.length(); i++) {
    if (name.charAt(i) == 'h') {
     continue;
    }
    if (name.charAt(i) == previousChar) {
     continue;
    }
    result += name.charAt(i);
    previousChar = name.charAt(i);
   }
  } else {
   result += name.charAt(0);
   for (int i = 1; i < name.length(); i++) {
    if (previousChar == name.charAt(i)) {
     continue;
    }
    result += name.charAt(i);
    previousChar = name.charAt(i);
   }
  }
  return result;
 }


리팩토링

가드 클로즈를 적용하니 숫자 중복 제거 코드와 h일때 기능 처리 부분의 코드가 동일해졌다.

for (int i = 1; i < name.length(); i++) {
 if (previousChar != name.charAt(i)) {
  result += name.charAt(i);
 }
 previousChar = name.charAt(i);
}

코드 커밋하기.


코드 테스트하기

h일때 기능을 처리하는 구간 또한 name의 길이와 반복문을 통하여 동적으로 작성되었다.

일반화가 잘 되었는지 확인하기 위하여 테스트 코드에 여러가지 코드를 추가하여 테스트 해본다.

 public void removeDupicationTest2() {
  // 중복 테스트
  assertEquals("A1", se.removeDuplication2("A11"));
  assertEquals("A2", se.removeDuplication2("A22"));
  assertEquals("B34", se.removeDuplication2("B334"));
  assertEquals("B43", se.removeDuplication2("B433"));
  assertEquals("B343", se.removeDuplication2("B343"));
  assertEquals("B21", se.removeDuplication2("B112211"));
  assertEquals("B3121", se.removeDuplication2("B33112211"));
  
  //h 테스트
  assertEquals("A1", se.removeDuplication2("A1h1"));
  assertEquals("A1", se.removeDuplication2("A1hh"));
  assertEquals("A1", se.removeDuplication2("A1hh11"));
  assertEquals("A121", se.removeDuplication2("A1h1221"));
 }

모두 통과한다. 

이로써 h가 들어갔을때의 기능 처리가 일반화 되었다고 판단할 수 있다.

이제 a문자가 포함되었을때의 기능을 처리하는 코드를 작성한다.


실패하는 테스트 코드 작성

 public void removeDupicationTest2() {
  // 중복 테스트
  assertEquals("A1", se.removeDuplication2("A11"));
  assertEquals("A2", se.removeDuplication2("A22"));
  assertEquals("B34", se.removeDuplication2("B334"));
  assertEquals("B43", se.removeDuplication2("B433"));
  assertEquals("B343", se.removeDuplication2("B343"));
  assertEquals("B21", se.removeDuplication2("B112211"));
  assertEquals("B3121", se.removeDuplication2("B33112211"));
  
  //h 테스트
  assertEquals("A1", se.removeDuplication2("A1h1"));
  assertEquals("A1", se.removeDuplication2("A1hh"));
  assertEquals("A1", se.removeDuplication2("A1hh11"));
  assertEquals("A121", se.removeDuplication2("A1h1221"));
 
  // a테스트
  assertEquals("A11", se.removeDuplication2("A1a1"));
 }

a가 들어갔을때 처리하는 코드를 아직 작성하지 않았기 때문에 테스트는 실패한다.



중간에 오이사님께서 가르쳐주신 것들


TDD는 설계 방법론이라고 불린다.

이유는 TDD를 반복하다보면 좋은 설계에 가까워지기 때문이다.

테스트 코드가 있기 때문에 반복적인 시도가 가능하다.

설계는 무엇인가?

코드가 설계이다.

코더는 누구인가? 컴파일러가 코더이다.

건축에서는 설계도를 주면 일꾼들이 건물을 만든다.

프로그래밍에서는 코드를 돌리면 컴파일러가 프로그램을 만든다.

건물 = 프로그램

설계도 = 코드

일꾼 = 컴파일러


무의식적으로 머릿속으로 생각하여 코드를 짤 수는 있지만

좋은 코드가 나오지 않을 수 있다.

TDD를 반복해서 연습하면 여러가지 코드를 짤 수 있게 되고 좋은 코드를 가려낼 수 있다.


TDD는 작은 문제를 해결하여 일반화시키고

조금 더 큰 문제들을 간단하게 해결하여 일반화시키고

일반화시킨 부분에서 공통점을 찾아 다시 일반화시키고

결국 커다란 하나의 일반화된 솔루션을 만들어내는 방법이다.


SoundEx를 예로 들면

전체 문제는 이름을 여러가지 조건에 맞춰서 코드로 변환시키는 것이다.

이는 작게 나눠서 1글자짜리 문제를 해결하는것부터 시작한다.

A를 해결한다.

그다음 B를 해결한다.

그러고 나니까 C가 알아서 해결된다.

1글자에 대한 일반화된 솔루션이 완성된것이다.

그다음은 2글자 Ab를 해결한다

다음엔 Ac를 해결한다

일반화된 솔루션이 완성되었다.

1글자와 2글자 솔루션에 공통점이 보인다.

이를 합치면 1글자와 2글자에 대한 일반화된 솔루션을 만들 수 있다.

이러한 방법을 반복하며 일반화된 솔루션을 만드는 것이 TDD의 과정이다.


※ 두가지 코드를 합칠때에는 소스를 완벽하게 똑같이 만든 상태에서 합쳐야 한다.

※ 공백 또한 중복으로 볼 수 있어야 한다.

※ 똑같은 코드도 여러가지 형태로 바꿀 수 있어야 한다. (ex : 가드 클로즈)


일반화된 솔루션에 새로운 것을 바로 추가하는것은 쉽지 않다.

분기하여 처리한 후 공통점을 찾아서 합치는 것이 바람직한 방법이다.


숫자만 중복처리하는 일반화된 솔루션을 만들었는데 모음, h, w도 중복처리가 가능하도록 만드려고 한다.

나는 일반화된 솔루션을 수정하려고 하였기에 추상적인 생각의 한계에 부딫쳤다.

이때 if문으로 모음, h, w가 포함되어 있으면 따로 분기를 해서 간단하게 처리를 한다. (이때 테스트코드를 따로 만들어야 한다.)


..오늘 후기

TDD의 개념에 대하여 머릿속으로는 배웠어도 실제로 적용하려니 실수도 많고 생각이 안나는 부분이 있었다.

손에 익을때까지 반복해서 해야할 것 같다.







오늘의 느낀점

프로그램을 작성할때 설계를 먼저 해야할까, 작성을 먼저 해야할까

SoundEx를 예로 들면 한글자 테스트, 두글자 테스트, 우선 작성하면서 수정하는 것이 좋을까?

아니면 미리 심사숙고하여 설계를 한 후 프로그램을 작성하는 것이 좋을까? (요구사항을 숙지한 후, 처음엔 인코딩을 하고 encodeTail(), 중복을 제거하고 removeDuplication(), 0을 채운다. fillZero())

내 생각에는 설계를 먼저 하고 프로그램을 작성하는게 좋은 것 같다.

이유는 요구사항을 숙지하지 않고, 손이 가는데로 코드를 쓰다보면 나중에 코드의 대대적인 수정이 필요하게 될 수도 있다.

예를들면, Aa, Bb, Ee, A, B, C등 한두글자 나오는 것을 작성하는데에 모음을 제거하는 기능까지 적용시킨다면 나중에 모음을 제거하는 부분을 추가로 작성하기가 애매해지는 문제가 발생한다.

미리 설계를 해놓고 기능별로 메소드를 분류해놓고 코드를 작성하면 이러한 문제를 방지할 수 있다.

따라서 설계를 먼저 하는것이 좋다고 생각한다.


솔루션이 일반화된다는 기준이 무엇인가? (얼마만큼 테스트를 통과해야 일반화되었다고 볼 수 있는가? )

예를들어 한글자 짜리를 검증할때는

A 통과
B 통과

C 도 통과
일반화다! 라고 쉽게 생각할 수 있지만


중복을 없애는 경우를 보면

A11 통과
A121 통과
A11312 통과 

여기까지 하더라도 다음 코드가 통과할 거라는 보장이 있는가?

A111313112111 이것도 통과한다고 다음것도 통과할 거라는 보장이 있는가?

이런식으로 비교 케이스가 매우 많아질 수 있기 때문에 검증해야 되는 케이스의 수가 많아질 수 있다.
이 문제는 어떻게 해결하는가?



중복을 발견하라! (매우 중요)

공백 또한 중복이라고 볼 수 있어야한다.

없는 공간에 같은 코드를 추가해서 테스트가 통과된다면 코드를 추가시키고
이 코드를 중복으로 보아서 일반화시킬수 있다.
코드를 추가하면 다른 문장에도 적용될 수 있는지 보자.

(비슷한 코드중 하나에 어떠한 코드를 추가시키면 다른 비슷한 코드들에도 적용되는지 볼 것. ex) 4글자짜리 이름에 중복 제거를 할 때, 2번째 글자에 'a'를 필터링하는 기능을 넣었다면, 1,3,4번째에도 'a'를 필터링하는 기능을 추가할 수 있다.

그러면 규칙과 중복이 발생하게 되고 이를 반복문을 통하여 간결화시킬 수 있다.


가드클로즈의 필요

가드클로즈란 if문에서 else문이 별도로 없을때 if문 내부의 값을 else문으로 옮기고, if문의 조건식을 반대로 바꾼다음 continue문을 써서 분기시키는 것이다.

if문에 걸리면 나머지문장은 실행되지 않기 때문에 if문이 가드 역할을 해서 이런 이름이 붙은 것 같다.

지금까지 가드클로즈를 써보면서 느낀점은

  1. 코드가 간결해지고 정형화된다.
  2. 가독성이 증가한다.

이 정도인것 같다. 


마지막으로..

SoundEx문제에서의 TDD 적용은 어느정도 이해를 한 것 같다.

하지만 실제 프로젝트는 SoundEx처럼 간단하지 않을텐데 실제 프로젝트에서는 어떻게 적용될 것인지 궁금하다.


------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

기타 Tip들

  1. 프로젝트는 src(프로덕션 코드)폴더, test(테스트 코드)폴더로 나누어서 관리한다. (패키지로 분할하지 않는 이유는 product.com.nethru.SoundEx와 같은 Import문을 일일히 쓰면 번거롭기 때문이다)
  2. 작업을 수행할때에는 Alt + Shift + M (메소드 추출) 과 같은 단축키를 쓰는 것이 개발 시간을 효율적으로 단축시킬 수 있다.
  3. 프로그램을 만들때 수행 코드부터 작성한 후에 단축키 (ctrl + 1)로 클래스, 메소드를 만들어나가는 과정을 '역접근' 이라고 한다.


관련글 더보기

댓글 영역